@plasius/gpu-shared 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +55 -3
- package/dist/{chunk-NCPJWLX3.js → chunk-2GM64LB6.js} +1 -9
- package/dist/{chunk-NCPJWLX3.js.map → chunk-2GM64LB6.js.map} +1 -1
- package/dist/{chunk-DABW627O.js → chunk-3ARPGHCQ.js} +8 -2
- package/dist/chunk-3ARPGHCQ.js.map +1 -0
- package/dist/chunk-4ZJ24VRS.js +402 -0
- package/dist/chunk-4ZJ24VRS.js.map +1 -0
- package/dist/{chunk-DQX4DXBR.js → chunk-W5GA3VA6.js} +79 -6
- package/dist/chunk-W5GA3VA6.js.map +1 -0
- package/dist/gltf-loader-YDPLZS5Q.js +8 -0
- package/dist/index.cjs +1230 -6198
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +31 -5
- package/dist/index.js.map +1 -1
- package/dist/product-studio-runtime-HDAUDWYO.js +11 -0
- package/dist/showcase-inline-assets-WT4PSNKI.js +7 -0
- package/dist/showcase-inline-assets-WT4PSNKI.js.map +1 -0
- package/dist/showcase-runtime-SNCUFSSC.js +3785 -0
- package/dist/showcase-runtime-SNCUFSSC.js.map +1 -0
- package/package.json +6 -8
- package/src/feature-flags.js +1 -0
- package/src/gltf-loader.js +10 -2
- package/src/index.d.ts +72 -1
- package/src/index.js +33 -0
- package/src/product-studio-runtime.js +465 -0
- package/src/showcase-runtime.js +875 -72
- package/dist/chunk-2FIFSBB4.js +0 -74
- package/dist/chunk-2FIFSBB4.js.map +0 -1
- package/dist/chunk-DABW627O.js.map +0 -1
- package/dist/chunk-DQX4DXBR.js.map +0 -1
- package/dist/gltf-loader-WAM23F37.js +0 -9
- package/dist/showcase-inline-assets-B7U7VX5H.js +0 -7
- package/dist/showcase-runtime-PN7N3FZY.js +0 -9164
- package/dist/showcase-runtime-PN7N3FZY.js.map +0 -1
- /package/dist/{gltf-loader-WAM23F37.js.map → gltf-loader-YDPLZS5Q.js.map} +0 -0
- /package/dist/{showcase-inline-assets-B7U7VX5H.js.map → product-studio-runtime-HDAUDWYO.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plasius/gpu-shared",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Shared browser-safe demo runtime and asset helpers for the Plasius gpu-* package family.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"demo": "python3 -m http.server --directory ..",
|
|
34
34
|
"demo:example": "node demo/example.js",
|
|
35
35
|
"generate:assets": "node scripts/generate-showcase-assets.mjs",
|
|
36
|
-
"typecheck": "node --check src/index.js && node --check src/showcase-runtime.js && node --check src/gltf-loader.js && node --check demo/main.js",
|
|
36
|
+
"typecheck": "node --check src/index.js && node --check src/showcase-runtime.js && node --check src/gltf-loader.js && node --check src/product-studio-runtime.js && node --check demo/main.js",
|
|
37
37
|
"audit:eslint": "eslint . --max-warnings=0",
|
|
38
38
|
"audit:deps": "npm ls --all --omit=optional --omit=peer > /dev/null 2>&1 || true",
|
|
39
39
|
"audit:npm": "npm audit --audit-level=high --omit=dev",
|
|
@@ -57,17 +57,15 @@
|
|
|
57
57
|
"author": "Plasius LTD <development@plasius.co.uk>",
|
|
58
58
|
"license": "Apache-2.0",
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@plasius/gpu-cloth": "^0.1.4",
|
|
61
|
-
"@plasius/gpu-debug": "^0.1.5",
|
|
62
|
-
"@plasius/gpu-fluid": "^0.1.4",
|
|
63
|
-
"@plasius/gpu-lighting": "^0.1.16",
|
|
64
|
-
"@plasius/gpu-performance": "^0.1.6",
|
|
65
|
-
"@plasius/gpu-physics": "^0.1.13"
|
|
66
60
|
},
|
|
67
61
|
"peerDependencies": {
|
|
62
|
+
"@plasius/gpu-renderer": "^0.1.14",
|
|
68
63
|
"@plasius/translations": "^1.0.17"
|
|
69
64
|
},
|
|
70
65
|
"peerDependenciesMeta": {
|
|
66
|
+
"@plasius/gpu-renderer": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
71
69
|
"@plasius/translations": {
|
|
72
70
|
"optional": true
|
|
73
71
|
}
|
package/src/feature-flags.js
CHANGED
package/src/gltf-loader.js
CHANGED
|
@@ -139,6 +139,12 @@ function computeBounds(positions) {
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
function appendValues(target, values) {
|
|
143
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
144
|
+
target.push(values[index]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
function resolveBrowserRequestBaseUrl() {
|
|
143
149
|
if (
|
|
144
150
|
typeof document !== "undefined" &&
|
|
@@ -406,8 +412,10 @@ async function buildGltfModel(document, baseUrl) {
|
|
|
406
412
|
|
|
407
413
|
for (const primitive of scene.primitives) {
|
|
408
414
|
const vertexOffset = aggregatePositions.length / 3;
|
|
409
|
-
aggregatePositions
|
|
410
|
-
|
|
415
|
+
appendValues(aggregatePositions, primitive.positions);
|
|
416
|
+
for (const index of primitive.indices) {
|
|
417
|
+
aggregateIndices.push(index + vertexOffset);
|
|
418
|
+
}
|
|
411
419
|
}
|
|
412
420
|
|
|
413
421
|
const color = scene.primitives[0]?.material?.color ?? { r: 0.56, g: 0.33, b: 0.22, a: 1 };
|
package/src/index.d.ts
CHANGED
|
@@ -57,6 +57,23 @@ export type ShowcaseFocusMode =
|
|
|
57
57
|
| "performance"
|
|
58
58
|
| "debug";
|
|
59
59
|
|
|
60
|
+
export type ShowcaseDemoMode = "harbor" | "product-studio" | "product" | "studio" | "eames";
|
|
61
|
+
|
|
62
|
+
export interface ProductStudioMesh {
|
|
63
|
+
readonly id: number;
|
|
64
|
+
readonly positions: readonly number[];
|
|
65
|
+
readonly indices: readonly number[];
|
|
66
|
+
readonly normals?: readonly number[] | null;
|
|
67
|
+
readonly uvs?: readonly number[] | null;
|
|
68
|
+
readonly color: readonly number[];
|
|
69
|
+
readonly emission?: readonly number[];
|
|
70
|
+
readonly materialKind: string | number;
|
|
71
|
+
readonly materialRefId?: number;
|
|
72
|
+
readonly roughness?: number;
|
|
73
|
+
readonly metallic?: number;
|
|
74
|
+
readonly opacity?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
export type GpuSharedTranslationValue =
|
|
61
78
|
| string
|
|
62
79
|
| number
|
|
@@ -137,7 +154,17 @@ export function createGpuSharedTranslator(
|
|
|
137
154
|
): (key: GpuSharedTranslationKey, args?: GpuSharedTranslationArgs) => string;
|
|
138
155
|
|
|
139
156
|
export interface MountGpuShowcaseOptions {
|
|
157
|
+
__showcaseFeatureLoaders?: {
|
|
158
|
+
cloth?: () => Promise<unknown>;
|
|
159
|
+
fluid?: () => Promise<unknown>;
|
|
160
|
+
lighting?: () => Promise<unknown>;
|
|
161
|
+
performance?: () => Promise<unknown>;
|
|
162
|
+
debug?: () => Promise<unknown>;
|
|
163
|
+
physics?: () => Promise<unknown>;
|
|
164
|
+
};
|
|
140
165
|
root?: HTMLElement;
|
|
166
|
+
demoMode?: ShowcaseDemoMode;
|
|
167
|
+
mode?: ShowcaseDemoMode;
|
|
141
168
|
focus?: ShowcaseFocusMode | string;
|
|
142
169
|
packageName?: string;
|
|
143
170
|
title?: string;
|
|
@@ -145,6 +172,16 @@ export interface MountGpuShowcaseOptions {
|
|
|
145
172
|
translate?: GpuSharedTranslate;
|
|
146
173
|
captureMode?: boolean;
|
|
147
174
|
renderScale?: number;
|
|
175
|
+
productAssetUrl?: string | URL;
|
|
176
|
+
assetUrl?: string | URL;
|
|
177
|
+
width?: number;
|
|
178
|
+
height?: number;
|
|
179
|
+
maxDepth?: number;
|
|
180
|
+
tileSize?: number;
|
|
181
|
+
samplesPerPixel?: number;
|
|
182
|
+
denoise?: boolean;
|
|
183
|
+
lightingPreset?: string;
|
|
184
|
+
lightingIntensity?: number;
|
|
148
185
|
createState?: () => unknown;
|
|
149
186
|
updateState?: (state: unknown, scene: Record<string, unknown>, dt: number) => unknown;
|
|
150
187
|
describeState?: (state: unknown, scene: Record<string, unknown>) => Record<string, unknown> | null;
|
|
@@ -158,7 +195,29 @@ export interface MountGpuShowcaseResult {
|
|
|
158
195
|
destroy(): void;
|
|
159
196
|
}
|
|
160
197
|
|
|
198
|
+
export interface MountGpuProductStudioResult {
|
|
199
|
+
readonly state: Readonly<{
|
|
200
|
+
featureFlags: unknown;
|
|
201
|
+
modelName: string;
|
|
202
|
+
sourceTriangleCount: number;
|
|
203
|
+
meshCount: number;
|
|
204
|
+
geometryMode: string;
|
|
205
|
+
requiresTriangleMeshRenderer: boolean;
|
|
206
|
+
displayQuality: boolean;
|
|
207
|
+
requiresMeshBvhForDisplayQuality: boolean;
|
|
208
|
+
rendererStats: Record<string, unknown>;
|
|
209
|
+
}>;
|
|
210
|
+
readonly model: GltfModel;
|
|
211
|
+
readonly canvas: HTMLCanvasElement;
|
|
212
|
+
readonly renderer: unknown;
|
|
213
|
+
readonly meshes: readonly ProductStudioMesh[];
|
|
214
|
+
destroy(): void;
|
|
215
|
+
}
|
|
216
|
+
|
|
161
217
|
export const showcaseFocusModes: readonly ShowcaseFocusMode[];
|
|
218
|
+
export const showcaseDemoModes: readonly ShowcaseDemoMode[];
|
|
219
|
+
export const GPU_SHOWCASE_REALISTIC_MODELS_FEATURE: "gpu_showcase_realistic_models_v1";
|
|
220
|
+
export const GPU_SHOWCASE_PRODUCT_STUDIO_FEATURE: "gpu_showcase_product_studio_wavefront_v1";
|
|
162
221
|
|
|
163
222
|
export function resolveShowcaseAssetUrl(
|
|
164
223
|
baseUrlOrAssetName?: string | URL | ShowcaseAssetName,
|
|
@@ -167,6 +226,18 @@ export function resolveShowcaseAssetUrl(
|
|
|
167
226
|
|
|
168
227
|
export function loadGltfModel(url: string | URL): Promise<GltfModel>;
|
|
169
228
|
|
|
229
|
+
export function createProductStudioMeshes(
|
|
230
|
+
model: GltfModel,
|
|
231
|
+
options?: {
|
|
232
|
+
targetCenter?: readonly number[];
|
|
233
|
+
targetSize?: number;
|
|
234
|
+
}
|
|
235
|
+
): readonly ProductStudioMesh[];
|
|
236
|
+
|
|
237
|
+
export function mountGpuProductStudio(
|
|
238
|
+
options?: MountGpuShowcaseOptions
|
|
239
|
+
): Promise<MountGpuProductStudioResult>;
|
|
240
|
+
|
|
170
241
|
export function mountGpuShowcase(
|
|
171
242
|
options?: MountGpuShowcaseOptions
|
|
172
|
-
): Promise<MountGpuShowcaseResult>;
|
|
243
|
+
): Promise<MountGpuShowcaseResult | MountGpuProductStudioResult>;
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,14 @@ export {
|
|
|
6
6
|
translateGpuSharedText,
|
|
7
7
|
} from "./i18n.js";
|
|
8
8
|
export { gpuSharedEnGbTranslations } from "./translations/en-GB.js";
|
|
9
|
+
export {
|
|
10
|
+
GPU_SHOWCASE_PRODUCT_STUDIO_FEATURE,
|
|
11
|
+
GPU_SHOWCASE_REALISTIC_MODELS_FEATURE,
|
|
12
|
+
} from "./feature-flags.js";
|
|
13
|
+
export {
|
|
14
|
+
createProductStudioMeshes,
|
|
15
|
+
mountGpuProductStudio,
|
|
16
|
+
} from "./product-studio-runtime.js";
|
|
9
17
|
|
|
10
18
|
export const showcaseFocusModes = Object.freeze([
|
|
11
19
|
"integrated",
|
|
@@ -16,6 +24,7 @@ export const showcaseFocusModes = Object.freeze([
|
|
|
16
24
|
"performance",
|
|
17
25
|
"debug",
|
|
18
26
|
]);
|
|
27
|
+
export const showcaseDemoModes = Object.freeze(["harbor", "product-studio"]);
|
|
19
28
|
|
|
20
29
|
export async function loadGltfModel(url) {
|
|
21
30
|
const module = await import("./gltf-loader.js");
|
|
@@ -23,6 +32,29 @@ export async function loadGltfModel(url) {
|
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
export async function mountGpuShowcase(options = {}) {
|
|
35
|
+
const demoMode = options.demoMode ?? options.mode;
|
|
36
|
+
if (
|
|
37
|
+
demoMode === "product-studio" ||
|
|
38
|
+
demoMode === "product" ||
|
|
39
|
+
demoMode === "studio" ||
|
|
40
|
+
demoMode === "eames"
|
|
41
|
+
) {
|
|
42
|
+
const productRuntimeLoader =
|
|
43
|
+
typeof options.__productRuntimeLoader === "function"
|
|
44
|
+
? options.__productRuntimeLoader
|
|
45
|
+
: () => import("./product-studio-runtime.js");
|
|
46
|
+
const productModule = await productRuntimeLoader();
|
|
47
|
+
if (typeof productModule.mountGpuProductStudio !== "function") {
|
|
48
|
+
throw new Error("product runtime loader must provide mountGpuProductStudio.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const productOptions = { ...options, demoMode };
|
|
52
|
+
delete productOptions.__runtimeLoader;
|
|
53
|
+
delete productOptions.__productRuntimeLoader;
|
|
54
|
+
delete productOptions.__featureFlags;
|
|
55
|
+
return productModule.mountGpuProductStudio(productOptions, options.__featureFlags);
|
|
56
|
+
}
|
|
57
|
+
|
|
26
58
|
const runtimeLoader =
|
|
27
59
|
typeof options.__runtimeLoader === "function"
|
|
28
60
|
? options.__runtimeLoader
|
|
@@ -34,6 +66,7 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
34
66
|
|
|
35
67
|
const publicOptions = { ...options };
|
|
36
68
|
delete publicOptions.__runtimeLoader;
|
|
69
|
+
delete publicOptions.__productRuntimeLoader;
|
|
37
70
|
delete publicOptions.__featureFlags;
|
|
38
71
|
return module.mountGpuShowcase(publicOptions, options.__featureFlags);
|
|
39
72
|
}
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { loadGltfModel } from "./gltf-loader.js";
|
|
2
|
+
|
|
3
|
+
const STYLE_ID = "plasius-product-studio-wavefront-style";
|
|
4
|
+
const DEFAULT_PRODUCT_ASSET_URL =
|
|
5
|
+
"/data/models/eames-lounge-chair-ottoman/Eames_Lounge_Chair_Ottoman.gltf";
|
|
6
|
+
const DEFAULT_TARGET_CENTER = Object.freeze([0, 0.74, 0]);
|
|
7
|
+
const DEFAULT_TARGET_SIZE = 2.25;
|
|
8
|
+
|
|
9
|
+
function clamp(value, min, max) {
|
|
10
|
+
return Math.max(min, Math.min(max, value));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isFiniteVector(value) {
|
|
14
|
+
return (
|
|
15
|
+
Array.isArray(value) &&
|
|
16
|
+
value.length >= 3 &&
|
|
17
|
+
Number.isFinite(value[0]) &&
|
|
18
|
+
Number.isFinite(value[1]) &&
|
|
19
|
+
Number.isFinite(value[2])
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createEmptyBounds() {
|
|
24
|
+
return {
|
|
25
|
+
min: [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
|
26
|
+
max: [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function expandBounds(bounds, point) {
|
|
31
|
+
bounds.min[0] = Math.min(bounds.min[0], point[0]);
|
|
32
|
+
bounds.min[1] = Math.min(bounds.min[1], point[1]);
|
|
33
|
+
bounds.min[2] = Math.min(bounds.min[2], point[2]);
|
|
34
|
+
bounds.max[0] = Math.max(bounds.max[0], point[0]);
|
|
35
|
+
bounds.max[1] = Math.max(bounds.max[1], point[1]);
|
|
36
|
+
bounds.max[2] = Math.max(bounds.max[2], point[2]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getBoundsSize(bounds) {
|
|
40
|
+
return [
|
|
41
|
+
bounds.max[0] - bounds.min[0],
|
|
42
|
+
bounds.max[1] - bounds.min[1],
|
|
43
|
+
bounds.max[2] - bounds.min[2],
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getBoundsCenter(bounds) {
|
|
48
|
+
return [
|
|
49
|
+
(bounds.min[0] + bounds.max[0]) * 0.5,
|
|
50
|
+
(bounds.min[1] + bounds.max[1]) * 0.5,
|
|
51
|
+
(bounds.min[2] + bounds.max[2]) * 0.5,
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getModelBounds(model) {
|
|
56
|
+
if (isFiniteVector(model?.bounds?.min) && isFiniteVector(model?.bounds?.max)) {
|
|
57
|
+
return {
|
|
58
|
+
min: [...model.bounds.min],
|
|
59
|
+
max: [...model.bounds.max],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const bounds = createEmptyBounds();
|
|
64
|
+
for (const primitive of model?.primitives ?? []) {
|
|
65
|
+
for (let index = 0; index < primitive.positions.length; index += 3) {
|
|
66
|
+
expandBounds(bounds, [
|
|
67
|
+
primitive.positions[index],
|
|
68
|
+
primitive.positions[index + 1],
|
|
69
|
+
primitive.positions[index + 2],
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return bounds;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function transformPoint(point, modelCenter, scale, targetCenter) {
|
|
77
|
+
return [
|
|
78
|
+
(point[0] - modelCenter[0]) * scale + targetCenter[0],
|
|
79
|
+
(point[1] - modelCenter[1]) * scale + targetCenter[1],
|
|
80
|
+
(point[2] - modelCenter[2]) * scale + targetCenter[2],
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readMaterialColor(material) {
|
|
85
|
+
const color = material?.color ?? {};
|
|
86
|
+
return [
|
|
87
|
+
Number.isFinite(color.r) ? color.r : 0.62,
|
|
88
|
+
Number.isFinite(color.g) ? color.g : 0.56,
|
|
89
|
+
Number.isFinite(color.b) ? color.b : 0.48,
|
|
90
|
+
Number.isFinite(color.a) ? color.a : 1,
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readMaterialKind(material) {
|
|
95
|
+
const emissive = material?.emissive ?? {};
|
|
96
|
+
if ((emissive.r ?? 0) + (emissive.g ?? 0) + (emissive.b ?? 0) > 0.001) {
|
|
97
|
+
return "emissive";
|
|
98
|
+
}
|
|
99
|
+
if ((material?.metallic ?? 0) >= 0.5) {
|
|
100
|
+
return "metal";
|
|
101
|
+
}
|
|
102
|
+
const alpha = readMaterialColor(material)[3];
|
|
103
|
+
if (alpha < 0.9) {
|
|
104
|
+
return "transparent";
|
|
105
|
+
}
|
|
106
|
+
return "diffuse";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readEmission(material) {
|
|
110
|
+
const emissive = material?.emissive ?? {};
|
|
111
|
+
return [
|
|
112
|
+
Number.isFinite(emissive.r) ? emissive.r : 0,
|
|
113
|
+
Number.isFinite(emissive.g) ? emissive.g : 0,
|
|
114
|
+
Number.isFinite(emissive.b) ? emissive.b : 0,
|
|
115
|
+
1,
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createQuadMesh({
|
|
120
|
+
id,
|
|
121
|
+
corners,
|
|
122
|
+
color,
|
|
123
|
+
emission = [0, 0, 0, 1],
|
|
124
|
+
materialKind = "diffuse",
|
|
125
|
+
roughness = 0.72,
|
|
126
|
+
metallic = 0,
|
|
127
|
+
opacity = color[3] ?? 1,
|
|
128
|
+
}) {
|
|
129
|
+
const [a, b, c] = corners;
|
|
130
|
+
const edge1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
131
|
+
const edge2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
132
|
+
const normal = [
|
|
133
|
+
edge1[1] * edge2[2] - edge1[2] * edge2[1],
|
|
134
|
+
edge1[2] * edge2[0] - edge1[0] * edge2[2],
|
|
135
|
+
edge1[0] * edge2[1] - edge1[1] * edge2[0],
|
|
136
|
+
];
|
|
137
|
+
const length = Math.hypot(normal[0], normal[1], normal[2]) || 1;
|
|
138
|
+
const unitNormal = normal.map((value) => value / length);
|
|
139
|
+
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
id,
|
|
142
|
+
positions: Object.freeze(corners.flat()),
|
|
143
|
+
indices: Object.freeze([0, 1, 2, 0, 2, 3]),
|
|
144
|
+
normals: Object.freeze([unitNormal, unitNormal, unitNormal, unitNormal].flat()),
|
|
145
|
+
color: Object.freeze([...color]),
|
|
146
|
+
emission: Object.freeze([...emission]),
|
|
147
|
+
materialKind,
|
|
148
|
+
roughness,
|
|
149
|
+
metallic,
|
|
150
|
+
opacity,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createProductStudioEnvironmentMeshes() {
|
|
155
|
+
return [
|
|
156
|
+
createQuadMesh({
|
|
157
|
+
id: 1,
|
|
158
|
+
corners: [
|
|
159
|
+
[-3.2, -0.08, 2.4],
|
|
160
|
+
[3.2, -0.08, 2.4],
|
|
161
|
+
[3.2, -0.08, -3.1],
|
|
162
|
+
[-3.2, -0.08, -3.1],
|
|
163
|
+
],
|
|
164
|
+
color: [0.48, 0.55, 0.55, 1],
|
|
165
|
+
roughness: 0.82,
|
|
166
|
+
}),
|
|
167
|
+
createQuadMesh({
|
|
168
|
+
id: 2,
|
|
169
|
+
corners: [
|
|
170
|
+
[-3.2, -0.08, -2.45],
|
|
171
|
+
[3.2, -0.08, -2.45],
|
|
172
|
+
[3.2, 2.65, -2.45],
|
|
173
|
+
[-3.2, 2.65, -2.45],
|
|
174
|
+
],
|
|
175
|
+
color: [0.43, 0.42, 0.38, 1],
|
|
176
|
+
roughness: 0.86,
|
|
177
|
+
}),
|
|
178
|
+
createQuadMesh({
|
|
179
|
+
id: 3,
|
|
180
|
+
corners: [
|
|
181
|
+
[-2.85, -0.08, -2.45],
|
|
182
|
+
[-2.85, 2.55, -2.45],
|
|
183
|
+
[-2.85, 2.55, 2.15],
|
|
184
|
+
[-2.85, -0.08, 2.15],
|
|
185
|
+
],
|
|
186
|
+
color: [0.36, 0.42, 0.45, 1],
|
|
187
|
+
roughness: 0.8,
|
|
188
|
+
}),
|
|
189
|
+
createQuadMesh({
|
|
190
|
+
id: 4,
|
|
191
|
+
corners: [
|
|
192
|
+
[0.78, 2.55, -0.82],
|
|
193
|
+
[-0.78, 2.55, -0.82],
|
|
194
|
+
[-0.78, 2.55, -1.78],
|
|
195
|
+
[0.78, 2.55, -1.78],
|
|
196
|
+
],
|
|
197
|
+
color: [1, 0.94, 0.78, 1],
|
|
198
|
+
emission: [8.5, 7.2, 4.8, 1],
|
|
199
|
+
materialKind: "emissive",
|
|
200
|
+
roughness: 0,
|
|
201
|
+
}),
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function createProductStudioMeshFromPrimitive(primitive, primitiveIndex, transform) {
|
|
206
|
+
if (!Array.isArray(primitive?.positions) || primitive.positions.length < 9) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const positions = [];
|
|
211
|
+
for (let index = 0; index < primitive.positions.length; index += 3) {
|
|
212
|
+
const point = transform([
|
|
213
|
+
primitive.positions[index],
|
|
214
|
+
primitive.positions[index + 1],
|
|
215
|
+
primitive.positions[index + 2],
|
|
216
|
+
]);
|
|
217
|
+
positions.push(point[0], point[1], point[2]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const indices =
|
|
221
|
+
Array.isArray(primitive.indices) && primitive.indices.length >= 3
|
|
222
|
+
? [...primitive.indices]
|
|
223
|
+
: Array.from({ length: positions.length / 3 }, (_, index) => index);
|
|
224
|
+
const material = primitive.material ?? {};
|
|
225
|
+
const color = readMaterialColor(material);
|
|
226
|
+
|
|
227
|
+
return Object.freeze({
|
|
228
|
+
id: 1000 + primitiveIndex,
|
|
229
|
+
positions: Object.freeze(positions),
|
|
230
|
+
indices: Object.freeze(indices),
|
|
231
|
+
normals: Array.isArray(primitive.normals) ? Object.freeze([...primitive.normals]) : null,
|
|
232
|
+
color: Object.freeze(color),
|
|
233
|
+
emission: Object.freeze(readEmission(material)),
|
|
234
|
+
materialKind: readMaterialKind(material),
|
|
235
|
+
materialRefId: 1000 + primitiveIndex,
|
|
236
|
+
roughness: Number.isFinite(material.roughness) ? material.roughness : 0.72,
|
|
237
|
+
metallic: Number.isFinite(material.metallic) ? material.metallic : 0,
|
|
238
|
+
opacity: color[3],
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function createProductStudioMeshes(model, options = {}) {
|
|
243
|
+
const primitives = Array.isArray(model?.primitives) ? model.primitives : [];
|
|
244
|
+
if (primitives.length === 0) {
|
|
245
|
+
throw new Error("Product Studio model must contain at least one renderable primitive.");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const targetCenter = isFiniteVector(options.targetCenter)
|
|
249
|
+
? [...options.targetCenter]
|
|
250
|
+
: [...DEFAULT_TARGET_CENTER];
|
|
251
|
+
const targetSize = Number.isFinite(options.targetSize)
|
|
252
|
+
? Math.max(options.targetSize, 0.25)
|
|
253
|
+
: DEFAULT_TARGET_SIZE;
|
|
254
|
+
const modelBounds = getModelBounds(model);
|
|
255
|
+
const modelSize = getBoundsSize(modelBounds);
|
|
256
|
+
const modelCenter = getBoundsCenter(modelBounds);
|
|
257
|
+
const scale = targetSize / Math.max(modelSize[0], modelSize[1], modelSize[2], 0.000001);
|
|
258
|
+
const transform = (point) => transformPoint(point, modelCenter, scale, targetCenter);
|
|
259
|
+
const modelMeshes = primitives
|
|
260
|
+
.map((primitive, index) => createProductStudioMeshFromPrimitive(primitive, index, transform))
|
|
261
|
+
.filter(Boolean);
|
|
262
|
+
|
|
263
|
+
return Object.freeze([...createProductStudioEnvironmentMeshes(), ...modelMeshes]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function ensureStyles(documentRef) {
|
|
267
|
+
if (documentRef.getElementById?.(STYLE_ID)) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const style = documentRef.createElement("style");
|
|
271
|
+
style.id = STYLE_ID;
|
|
272
|
+
style.textContent = `
|
|
273
|
+
.plasius-product-studio-wavefront {
|
|
274
|
+
position: relative;
|
|
275
|
+
width: 100%;
|
|
276
|
+
min-height: 420px;
|
|
277
|
+
overflow: hidden;
|
|
278
|
+
background: #0f1418;
|
|
279
|
+
display: grid;
|
|
280
|
+
place-items: center;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.plasius-product-studio-wavefront canvas {
|
|
284
|
+
display: block;
|
|
285
|
+
width: 100%;
|
|
286
|
+
height: auto;
|
|
287
|
+
max-height: 100%;
|
|
288
|
+
aspect-ratio: 16 / 9;
|
|
289
|
+
min-height: 420px;
|
|
290
|
+
object-fit: contain;
|
|
291
|
+
}
|
|
292
|
+
`;
|
|
293
|
+
documentRef.head?.appendChild?.(style);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function resolveRoot(options) {
|
|
297
|
+
const documentRef = options.document ?? globalThis.document;
|
|
298
|
+
if (options.root) {
|
|
299
|
+
return options.root;
|
|
300
|
+
}
|
|
301
|
+
const root =
|
|
302
|
+
documentRef?.querySelector?.("[data-plasius-gpu-product-studio]") ?? documentRef?.body;
|
|
303
|
+
if (!root) {
|
|
304
|
+
throw new Error("Product Studio requires a DOM root.");
|
|
305
|
+
}
|
|
306
|
+
return root;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function resolveRenderSize(root, options) {
|
|
310
|
+
const rect = root.getBoundingClientRect?.() ?? { width: 1280, height: 720 };
|
|
311
|
+
const devicePixelRatio =
|
|
312
|
+
Number.isFinite(options.devicePixelRatio)
|
|
313
|
+
? options.devicePixelRatio
|
|
314
|
+
: Number.isFinite(globalThis.window?.devicePixelRatio)
|
|
315
|
+
? globalThis.window.devicePixelRatio
|
|
316
|
+
: 1;
|
|
317
|
+
const cssWidth = Number.isFinite(rect.width) && rect.width > 0 ? rect.width : 1280;
|
|
318
|
+
const cssHeight =
|
|
319
|
+
Number.isFinite(rect.height) && rect.height > 0 ? rect.height : cssWidth * (9 / 16);
|
|
320
|
+
const width = Number.isFinite(options.width)
|
|
321
|
+
? Math.trunc(options.width)
|
|
322
|
+
: clamp(Math.round(cssWidth * devicePixelRatio), 640, 1920);
|
|
323
|
+
const height = Number.isFinite(options.height)
|
|
324
|
+
? Math.trunc(options.height)
|
|
325
|
+
: clamp(Math.round(cssHeight * devicePixelRatio), 360, 1080);
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
width,
|
|
329
|
+
height,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function installSnapshotHook(state) {
|
|
334
|
+
if (typeof globalThis.window === "undefined") {
|
|
335
|
+
return () => {};
|
|
336
|
+
}
|
|
337
|
+
const previous = globalThis.window.render_game_to_text;
|
|
338
|
+
globalThis.window.render_game_to_text = () =>
|
|
339
|
+
JSON.stringify({
|
|
340
|
+
surface: "gpu-product-studio-wavefront",
|
|
341
|
+
model: state.modelName,
|
|
342
|
+
sourceTriangles: state.sourceTriangleCount,
|
|
343
|
+
meshCount: state.meshCount,
|
|
344
|
+
geometryMode: state.geometryMode,
|
|
345
|
+
requiresTriangleMeshRenderer: state.requiresTriangleMeshRenderer,
|
|
346
|
+
displayQuality: state.displayQuality,
|
|
347
|
+
requiresMeshBvhForDisplayQuality: state.requiresMeshBvhForDisplayQuality,
|
|
348
|
+
renderer: state.rendererStats,
|
|
349
|
+
});
|
|
350
|
+
return () => {
|
|
351
|
+
if (previous === undefined) {
|
|
352
|
+
delete globalThis.window.render_game_to_text;
|
|
353
|
+
} else {
|
|
354
|
+
globalThis.window.render_game_to_text = previous;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function countSourceTriangles(model) {
|
|
360
|
+
return (model.primitives ?? []).reduce(
|
|
361
|
+
(total, primitive) => total + Math.floor((primitive.indices?.length ?? 0) / 3),
|
|
362
|
+
0
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function resolveWavefrontLightingOptions(options) {
|
|
367
|
+
const fallback = {
|
|
368
|
+
environmentColor: [0.35, 0.43, 0.49, 1],
|
|
369
|
+
ambientColor: [0.02, 0.024, 0.028, 1],
|
|
370
|
+
};
|
|
371
|
+
const lightingLoader =
|
|
372
|
+
typeof options.__lightingLoader === "function"
|
|
373
|
+
? options.__lightingLoader
|
|
374
|
+
: () => import("@plasius/gpu-lighting").catch(() => null);
|
|
375
|
+
const lightingModule = await lightingLoader();
|
|
376
|
+
|
|
377
|
+
if (
|
|
378
|
+
typeof lightingModule?.createWavefrontEnvironmentLightingOptions !== "function"
|
|
379
|
+
) {
|
|
380
|
+
return fallback;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return lightingModule.createWavefrontEnvironmentLightingOptions({
|
|
384
|
+
preset: options.lightingPreset ?? "product-studio",
|
|
385
|
+
intensity: options.lightingIntensity,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function mountGpuProductStudio(options = {}, featureFlags = null) {
|
|
390
|
+
const root = resolveRoot(options);
|
|
391
|
+
const documentRef = options.document ?? root.ownerDocument ?? globalThis.document;
|
|
392
|
+
ensureStyles(documentRef);
|
|
393
|
+
const previousMarkup = root.innerHTML;
|
|
394
|
+
root.innerHTML = "";
|
|
395
|
+
root.classList?.add?.("plasius-product-studio-wavefront");
|
|
396
|
+
|
|
397
|
+
const canvas = documentRef.createElement("canvas");
|
|
398
|
+
canvas.dataset.plasiusGpuProductStudio = "wavefront";
|
|
399
|
+
root.appendChild(canvas);
|
|
400
|
+
|
|
401
|
+
const modelLoader =
|
|
402
|
+
typeof options.__modelLoader === "function" ? options.__modelLoader : loadGltfModel;
|
|
403
|
+
const rendererLoader =
|
|
404
|
+
typeof options.__rendererLoader === "function"
|
|
405
|
+
? options.__rendererLoader
|
|
406
|
+
: () => import("@plasius/gpu-renderer");
|
|
407
|
+
const assetUrl = options.productAssetUrl ?? options.assetUrl ?? DEFAULT_PRODUCT_ASSET_URL;
|
|
408
|
+
const model = await modelLoader(assetUrl);
|
|
409
|
+
const meshes = createProductStudioMeshes(model, {
|
|
410
|
+
targetCenter: options.targetCenter,
|
|
411
|
+
targetSize: options.targetSize,
|
|
412
|
+
});
|
|
413
|
+
const rendererModule = await rendererLoader();
|
|
414
|
+
if (typeof rendererModule.createWavefrontPathTracingComputeRenderer !== "function") {
|
|
415
|
+
throw new Error("Product Studio renderer loader must provide createWavefrontPathTracingComputeRenderer.");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const size = resolveRenderSize(root, options);
|
|
419
|
+
const lightingOptions = await resolveWavefrontLightingOptions(options);
|
|
420
|
+
const renderer = await rendererModule.createWavefrontPathTracingComputeRenderer({
|
|
421
|
+
canvas,
|
|
422
|
+
width: size.width,
|
|
423
|
+
height: size.height,
|
|
424
|
+
maxDepth: Number.isFinite(options.maxDepth) ? options.maxDepth : 6,
|
|
425
|
+
tileSize: Number.isFinite(options.tileSize) ? options.tileSize : 128,
|
|
426
|
+
samplesPerPixel: Number.isFinite(options.samplesPerPixel) ? options.samplesPerPixel : 8,
|
|
427
|
+
denoise: options.denoise !== false,
|
|
428
|
+
displayQuality: true,
|
|
429
|
+
meshes,
|
|
430
|
+
camera: {
|
|
431
|
+
position: [0, 1.12, 5.05],
|
|
432
|
+
target: [0, 0.72, 0],
|
|
433
|
+
up: [0, 1, 0],
|
|
434
|
+
fovYDegrees: 43,
|
|
435
|
+
},
|
|
436
|
+
...lightingOptions,
|
|
437
|
+
});
|
|
438
|
+
const rendererStats = renderer.renderOnce();
|
|
439
|
+
const state = Object.freeze({
|
|
440
|
+
featureFlags,
|
|
441
|
+
modelName: model.name,
|
|
442
|
+
sourceTriangleCount: countSourceTriangles(model),
|
|
443
|
+
meshCount: meshes.length,
|
|
444
|
+
geometryMode: "mesh-bvh-display-quality",
|
|
445
|
+
requiresTriangleMeshRenderer: true,
|
|
446
|
+
displayQuality: true,
|
|
447
|
+
requiresMeshBvhForDisplayQuality: true,
|
|
448
|
+
rendererStats,
|
|
449
|
+
});
|
|
450
|
+
const restoreSnapshotHook = installSnapshotHook(state);
|
|
451
|
+
|
|
452
|
+
return Object.freeze({
|
|
453
|
+
state,
|
|
454
|
+
model,
|
|
455
|
+
canvas,
|
|
456
|
+
renderer,
|
|
457
|
+
meshes,
|
|
458
|
+
destroy() {
|
|
459
|
+
restoreSnapshotHook();
|
|
460
|
+
renderer.destroy();
|
|
461
|
+
root.classList?.remove?.("plasius-product-studio-wavefront");
|
|
462
|
+
root.innerHTML = previousMarkup;
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
}
|