@plasius/gpu-shared 0.1.11 → 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 +58 -3
- package/README.md +110 -4
- package/assets/brigantine.gltf +549 -24
- package/assets/cutter.gltf +538 -0
- package/assets/harbor-dock.gltf +680 -0
- package/assets/lighthouse.gltf +604 -0
- package/dist/chunk-2GM64LB6.js +9 -0
- package/dist/chunk-2GM64LB6.js.map +1 -0
- package/dist/chunk-3ARPGHCQ.js +119 -0
- 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-W5GA3VA6.js +442 -0
- package/dist/chunk-W5GA3VA6.js.map +1 -0
- package/dist/gltf-loader-YDPLZS5Q.js +8 -0
- package/dist/index.cjs +2432 -6424
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +45 -6
- 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 +20 -8
- package/src/asset-url.js +62 -11
- package/src/feature-flags.js +2 -0
- package/src/gltf-loader.js +330 -32
- package/src/i18n.js +71 -0
- package/src/index.d.ts +187 -2
- package/src/index.js +42 -1
- package/src/product-studio-runtime.js +465 -0
- package/src/showcase-inline-assets.js +3 -0
- package/src/showcase-runtime.js +1779 -252
- package/src/translations/en-GB.js +55 -0
- package/dist/chunk-DGUM43GV.js +0 -11
- package/dist/chunk-OTCJ3VOK.js +0 -35
- package/dist/chunk-OTCJ3VOK.js.map +0 -1
- package/dist/chunk-QBMXJ3V2.js +0 -142
- package/dist/chunk-QBMXJ3V2.js.map +0 -1
- package/dist/gltf-loader-LKALCZAV.js +0 -8
- package/dist/showcase-runtime-2ZNPKD7D.js +0 -8593
- package/dist/showcase-runtime-2ZNPKD7D.js.map +0 -1
- /package/dist/{chunk-DGUM43GV.js.map → gltf-loader-YDPLZS5Q.js.map} +0 -0
- /package/dist/{gltf-loader-LKALCZAV.js.map → product-studio-runtime-HDAUDWYO.js.map} +0 -0
package/src/showcase-runtime.js
CHANGED
|
@@ -1,43 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clothGarmentKinds,
|
|
3
|
-
clothProfileNames,
|
|
4
|
-
createClothRepresentationPlan,
|
|
5
|
-
selectClothRepresentationBand,
|
|
6
|
-
} from "@plasius/gpu-cloth";
|
|
7
|
-
import {
|
|
8
|
-
fluidBodyKinds,
|
|
9
|
-
fluidProfileNames,
|
|
10
|
-
createFluidContinuityEnvelope,
|
|
11
|
-
createFluidRepresentationPlan,
|
|
12
|
-
selectFluidRepresentationBand,
|
|
13
|
-
} from "@plasius/gpu-fluid";
|
|
14
|
-
import {
|
|
15
|
-
createLightingBandPlan,
|
|
16
|
-
defaultLightingProfile,
|
|
17
|
-
getLightingProfile,
|
|
18
|
-
lightingDistanceBands,
|
|
19
|
-
} from "@plasius/gpu-lighting";
|
|
20
|
-
import {
|
|
21
|
-
createDeviceProfile,
|
|
22
|
-
createGpuPerformanceGovernor,
|
|
23
|
-
createQualityLadderAdapter,
|
|
24
|
-
} from "@plasius/gpu-performance";
|
|
25
|
-
import { createGpuDebugSession } from "@plasius/gpu-debug";
|
|
26
|
-
import {
|
|
27
|
-
createPhysicsSimulationPlan,
|
|
28
|
-
createPhysicsWorldSnapshot,
|
|
29
|
-
defaultPhysicsWorkerProfile,
|
|
30
|
-
getPhysicsWorkerManifest,
|
|
31
|
-
} from "@plasius/gpu-physics/browser";
|
|
32
|
-
|
|
33
1
|
import { resolveShowcaseAssetUrl } from "./asset-url.js";
|
|
34
2
|
import { loadGltfModel } from "./gltf-loader.js";
|
|
3
|
+
import { GPU_SHOWCASE_REALISTIC_MODELS_FEATURE } from "./feature-flags.js";
|
|
4
|
+
import {
|
|
5
|
+
createGpuSharedTranslator,
|
|
6
|
+
gpuSharedTranslationKeys,
|
|
7
|
+
} from "./i18n.js";
|
|
35
8
|
|
|
36
9
|
const STYLE_ID = "plasius-shared-3d-showcase-style";
|
|
37
10
|
const ROOT_CLASS = "plasius-showcase-root";
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
11
|
+
const CAPTURE_CLASS = "plasius-showcase-root--capture";
|
|
12
|
+
const DEFAULT_CANVAS_WIDTH = 1280;
|
|
13
|
+
const DEFAULT_CANVAS_HEIGHT = 720;
|
|
14
|
+
const CAPTURE_CANVAS_PIXEL_BUDGET = 1920 * 1080;
|
|
41
15
|
const SHIP_SCALE = 1.1;
|
|
42
16
|
const HARBOR_BOUNDS = Object.freeze({
|
|
43
17
|
minX: -11.2,
|
|
@@ -54,42 +28,848 @@ const CAMERA_PRESETS = Object.freeze({
|
|
|
54
28
|
performance: Object.freeze({ yaw: -0.65, pitch: 0.36, distance: 24, target: [0, 2.2, 0] }),
|
|
55
29
|
debug: Object.freeze({ yaw: -0.7, pitch: 0.32, distance: 24, target: [0, 2.2, 0] }),
|
|
56
30
|
});
|
|
57
|
-
export const showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
|
|
58
31
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
32
|
+
const FALLBACK_LIGHTING_DISTANCE_BANDS = Object.freeze([
|
|
33
|
+
Object.freeze({
|
|
34
|
+
band: "near",
|
|
35
|
+
primaryShadowSource: "ray-traced-primary",
|
|
36
|
+
rtParticipation: Object.freeze({
|
|
37
|
+
reflections: "full",
|
|
38
|
+
globalIllumination: "high",
|
|
39
|
+
directShadows: "premium",
|
|
40
|
+
}),
|
|
41
|
+
updateCadenceDivisor: 1,
|
|
42
|
+
}),
|
|
43
|
+
Object.freeze({
|
|
44
|
+
band: "mid",
|
|
45
|
+
primaryShadowSource: "ray-traced-secondary",
|
|
46
|
+
rtParticipation: Object.freeze({
|
|
47
|
+
reflections: "medium",
|
|
48
|
+
globalIllumination: "medium",
|
|
49
|
+
directShadows: "selective",
|
|
50
|
+
}),
|
|
51
|
+
updateCadenceDivisor: 2,
|
|
52
|
+
}),
|
|
53
|
+
Object.freeze({
|
|
54
|
+
band: "far",
|
|
55
|
+
primaryShadowSource: "baked",
|
|
56
|
+
rtParticipation: Object.freeze({
|
|
57
|
+
reflections: "low",
|
|
58
|
+
globalIllumination: "low",
|
|
59
|
+
directShadows: "none",
|
|
60
|
+
}),
|
|
61
|
+
updateCadenceDivisor: 4,
|
|
62
|
+
}),
|
|
63
|
+
Object.freeze({
|
|
64
|
+
band: "horizon",
|
|
65
|
+
primaryShadowSource: "impression",
|
|
66
|
+
rtParticipation: Object.freeze({
|
|
67
|
+
reflections: "off",
|
|
68
|
+
globalIllumination: "off",
|
|
69
|
+
directShadows: "none",
|
|
70
|
+
}),
|
|
71
|
+
updateCadenceDivisor: 8,
|
|
72
|
+
}),
|
|
64
73
|
]);
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const FALLBACK_LIGHTING_PROFILE = "cinematic";
|
|
75
|
+
const FALLBACK_PHYSICS_PROFILE = "cinematic";
|
|
76
|
+
const FALLBACK_PERFORMANCE_LEVELS = Object.freeze({
|
|
77
|
+
fluid: Object.freeze([
|
|
78
|
+
Object.freeze({
|
|
79
|
+
id: "low",
|
|
80
|
+
config: Object.freeze({ nearResolution: 10, midResolution: 6, splashCount: 10 }),
|
|
81
|
+
estimatedCostMs: 0.8,
|
|
82
|
+
}),
|
|
83
|
+
Object.freeze({
|
|
84
|
+
id: "medium",
|
|
85
|
+
config: Object.freeze({ nearResolution: 16, midResolution: 8, splashCount: 18 }),
|
|
86
|
+
estimatedCostMs: 1.4,
|
|
87
|
+
}),
|
|
88
|
+
Object.freeze({
|
|
89
|
+
id: "high",
|
|
90
|
+
config: Object.freeze({ nearResolution: 24, midResolution: 12, splashCount: 28 }),
|
|
91
|
+
estimatedCostMs: 2.4,
|
|
92
|
+
}),
|
|
76
93
|
]),
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
cloth: Object.freeze([
|
|
95
|
+
Object.freeze({
|
|
96
|
+
id: "low",
|
|
97
|
+
config: Object.freeze({ cols: 10, rows: 7 }),
|
|
98
|
+
estimatedCostMs: 0.7,
|
|
99
|
+
}),
|
|
100
|
+
Object.freeze({
|
|
101
|
+
id: "medium",
|
|
102
|
+
config: Object.freeze({ cols: 16, rows: 11 }),
|
|
103
|
+
estimatedCostMs: 1.3,
|
|
104
|
+
}),
|
|
105
|
+
Object.freeze({
|
|
106
|
+
id: "high",
|
|
107
|
+
config: Object.freeze({ cols: 24, rows: 16 }),
|
|
108
|
+
estimatedCostMs: 2.1,
|
|
109
|
+
}),
|
|
110
|
+
]),
|
|
111
|
+
lighting: Object.freeze([
|
|
112
|
+
Object.freeze({
|
|
113
|
+
id: "low",
|
|
114
|
+
config: Object.freeze({ shadowStrength: 0.18, reflectionStrength: 0.08 }),
|
|
115
|
+
estimatedCostMs: 0.5,
|
|
116
|
+
}),
|
|
117
|
+
Object.freeze({
|
|
118
|
+
id: "medium",
|
|
119
|
+
config: Object.freeze({ shadowStrength: 0.34, reflectionStrength: 0.16 }),
|
|
120
|
+
estimatedCostMs: 1.0,
|
|
121
|
+
}),
|
|
122
|
+
Object.freeze({
|
|
123
|
+
id: "high",
|
|
124
|
+
config: Object.freeze({ shadowStrength: 0.5, reflectionStrength: 0.24 }),
|
|
125
|
+
estimatedCostMs: 1.8,
|
|
126
|
+
}),
|
|
84
127
|
]),
|
|
85
128
|
});
|
|
86
129
|
|
|
130
|
+
function createFallbackClothFeatureModule() {
|
|
131
|
+
const fallback = createFallbackClothFeatureAdapters();
|
|
132
|
+
return {
|
|
133
|
+
clothGarmentKinds: fallback.garmentKinds,
|
|
134
|
+
clothProfileNames: fallback.profileNames,
|
|
135
|
+
createClothRepresentationPlan: fallback.createPlan,
|
|
136
|
+
selectClothRepresentationBand: fallback.selectBand,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function createFallbackFluidFeatureModule() {
|
|
141
|
+
const fallback = createFallbackFluidFeatureAdapters();
|
|
142
|
+
return {
|
|
143
|
+
fluidBodyKinds: fallback.bodyKinds,
|
|
144
|
+
fluidProfileNames: fallback.profileNames,
|
|
145
|
+
createFluidContinuityEnvelope: fallback.createContinuityEnvelope,
|
|
146
|
+
createFluidRepresentationPlan: fallback.createPlan,
|
|
147
|
+
selectFluidRepresentationBand: fallback.selectBand,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createFallbackLightingFeatureModule() {
|
|
152
|
+
return {
|
|
153
|
+
createLightingBandPlan({ profile = FALLBACK_LIGHTING_PROFILE, importance = "high" } = {}) {
|
|
154
|
+
const defaultPlan = {
|
|
155
|
+
profile,
|
|
156
|
+
importance,
|
|
157
|
+
bands: FALLBACK_LIGHTING_DISTANCE_BANDS,
|
|
158
|
+
};
|
|
159
|
+
return defaultPlan;
|
|
160
|
+
},
|
|
161
|
+
defaultLightingProfile: FALLBACK_LIGHTING_PROFILE,
|
|
162
|
+
getLightingProfile(profile = FALLBACK_LIGHTING_PROFILE) {
|
|
163
|
+
return {
|
|
164
|
+
name: profile,
|
|
165
|
+
bands: FALLBACK_LIGHTING_DISTANCE_BANDS,
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
lightingDistanceBands: FALLBACK_LIGHTING_DISTANCE_BANDS,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createFallbackPerformanceQualityState(levels = [], initialLevel = "high", profile = "") {
|
|
173
|
+
const fallbackLevels = levels.length ? levels : FALLBACK_PERFORMANCE_LEVELS[profile] ?? [];
|
|
174
|
+
const resolvedLevels = fallbackLevels.map((entry) => ({
|
|
175
|
+
id: String(entry?.id ?? "high"),
|
|
176
|
+
config: entry?.config ?? {},
|
|
177
|
+
estimatedCostMs: Number.isFinite(Number(entry?.estimatedCostMs))
|
|
178
|
+
? Number(entry.estimatedCostMs)
|
|
179
|
+
: 1.0,
|
|
180
|
+
}));
|
|
181
|
+
if (resolvedLevels.length === 0) {
|
|
182
|
+
return {
|
|
183
|
+
module: {
|
|
184
|
+
id: "high",
|
|
185
|
+
config: Object.freeze({}),
|
|
186
|
+
estimatedCostMs: 1.0,
|
|
187
|
+
},
|
|
188
|
+
getSnapshot() {
|
|
189
|
+
return {
|
|
190
|
+
currentLevel: {
|
|
191
|
+
id: "high",
|
|
192
|
+
config: Object.freeze({}),
|
|
193
|
+
estimatedCostMs: 1.0,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
id: "high",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const initial = resolvedLevels.find((entry) => entry.id === initialLevel) ?? resolvedLevels[0];
|
|
201
|
+
return {
|
|
202
|
+
id: initial.id,
|
|
203
|
+
getSnapshot() {
|
|
204
|
+
return {
|
|
205
|
+
currentLevel: {
|
|
206
|
+
id: initial.id,
|
|
207
|
+
config: initial.config,
|
|
208
|
+
estimatedCostMs: initial.estimatedCostMs,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function createFallbackPerformanceFeatureModule() {
|
|
216
|
+
const moduleIds = new Set(["fluid-detail", "cloth-detail", "lighting-detail"]);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
createDeviceProfile(profile = {}) {
|
|
220
|
+
return {
|
|
221
|
+
deviceClass: "desktop",
|
|
222
|
+
mode: "flat",
|
|
223
|
+
refreshRateHz: Number.isFinite(profile?.refreshRateHz)
|
|
224
|
+
? Number(profile.refreshRateHz)
|
|
225
|
+
: 60,
|
|
226
|
+
supportedFrameRates: Array.isArray(profile?.supportedFrameRates)
|
|
227
|
+
? profile.supportedFrameRates
|
|
228
|
+
: [60, 90],
|
|
229
|
+
supportsWebGpu: true,
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
createQualityLadderAdapter({ id, domain, levels, initialLevel }) {
|
|
233
|
+
return {
|
|
234
|
+
id: String(id ?? ""),
|
|
235
|
+
domain,
|
|
236
|
+
...createFallbackPerformanceQualityState(
|
|
237
|
+
domain === "fluid" ? FALLBACK_PERFORMANCE_LEVELS.fluid : domain === "cloth" ? FALLBACK_PERFORMANCE_LEVELS.cloth : FALLBACK_PERFORMANCE_LEVELS.lighting,
|
|
238
|
+
initialLevel,
|
|
239
|
+
domain
|
|
240
|
+
),
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
createGpuPerformanceGovernor({ device, modules = [], adaptation = {} } = {}) {
|
|
244
|
+
let pressureLevel = "stable";
|
|
245
|
+
let frameSamples = 0;
|
|
246
|
+
let averageMs = 16.67;
|
|
247
|
+
|
|
248
|
+
const clamp = (next = 16.67) => (Number.isFinite(next) ? Math.max(1, next) : 16.67);
|
|
249
|
+
const target = Object.freeze({
|
|
250
|
+
targetFrameTimeMs: 16.67,
|
|
251
|
+
downgradeFrameTimeMs: clamp(adaptation?.degradeThresholdMs ?? 20),
|
|
252
|
+
upgradeFrameTimeMs: clamp(adaptation?.upgradeThresholdMs ?? 14),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
recordFrame({ frameTimeMs = averageMs } = {}) {
|
|
257
|
+
const sample = Number.isFinite(Number(frameTimeMs)) ? Number(frameTimeMs) : averageMs;
|
|
258
|
+
frameSamples += 1;
|
|
259
|
+
averageMs = clamp((averageMs * (frameSamples - 1) + sample) / frameSamples);
|
|
260
|
+
const fps = 1000 / averageMs;
|
|
261
|
+
|
|
262
|
+
pressureLevel =
|
|
263
|
+
sample > target.downgradeFrameTimeMs
|
|
264
|
+
? "degrade"
|
|
265
|
+
: pressureLevel === "degrade" && sample <= target.upgradeFrameTimeMs
|
|
266
|
+
? "stable"
|
|
267
|
+
: pressureLevel;
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
pressureLevel,
|
|
271
|
+
metrics: {
|
|
272
|
+
fps,
|
|
273
|
+
averageFrameTimeMs: averageMs,
|
|
274
|
+
},
|
|
275
|
+
adjustments: pressureLevel === "degrade" ? [{ type: "capability-step-down" }] : [],
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
getTarget() {
|
|
279
|
+
return target;
|
|
280
|
+
},
|
|
281
|
+
getState() {
|
|
282
|
+
return {
|
|
283
|
+
modules: modules
|
|
284
|
+
.filter((entry) => entry != null && typeof entry.id === "string" && moduleIds.has(entry.id))
|
|
285
|
+
.map((entry) => ({
|
|
286
|
+
isAtMaximum: pressureLevel === "stable",
|
|
287
|
+
})),
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function createFallbackDebugFeatureModule() {
|
|
296
|
+
let queueSamples = 0;
|
|
297
|
+
let queuePeakDepth = 0;
|
|
298
|
+
let readyLaneSamples = 0;
|
|
299
|
+
let readyLanePeakDepth = 0;
|
|
300
|
+
let dispatchSamples = 0;
|
|
301
|
+
let dispatchDurationTotal = 0;
|
|
302
|
+
let dependencyUnlockSamples = 0;
|
|
303
|
+
let dependencyUnlockCount = 0;
|
|
304
|
+
let pipelineSamples = 0;
|
|
305
|
+
let pipelineAgeTotal = 0;
|
|
306
|
+
let frameSamples = 0;
|
|
307
|
+
let frameTimeTotal = 0;
|
|
308
|
+
let gpuBusyTotal = 0;
|
|
309
|
+
let frameDroppedSamples = 0;
|
|
310
|
+
let memoryTotalBytes = 0;
|
|
311
|
+
|
|
312
|
+
function ensureNumber(value, fallback = 0) {
|
|
313
|
+
const asNumber = Number(value);
|
|
314
|
+
return Number.isFinite(asNumber) ? asNumber : fallback;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function createDebugSession({ adapter } = {}) {
|
|
318
|
+
const memoryHint = Number.isFinite(Number(adapter?.memoryCapacityHintBytes))
|
|
319
|
+
? Number(adapter.memoryCapacityHintBytes)
|
|
320
|
+
: 0;
|
|
321
|
+
|
|
322
|
+
memoryTotalBytes = Math.max(0, memoryHint);
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
trackAllocation({ sizeBytes = 0 } = {}) {
|
|
326
|
+
memoryTotalBytes += ensureNumber(sizeBytes);
|
|
327
|
+
},
|
|
328
|
+
recordQueue({ depth = 0 } = {}) {
|
|
329
|
+
queueSamples += 1;
|
|
330
|
+
queuePeakDepth = Math.max(queuePeakDepth, ensureNumber(depth, 0));
|
|
331
|
+
},
|
|
332
|
+
recordReadyLane({ depth = 0 } = {}) {
|
|
333
|
+
readyLaneSamples += 1;
|
|
334
|
+
readyLanePeakDepth = Math.max(readyLanePeakDepth, ensureNumber(depth, 0));
|
|
335
|
+
},
|
|
336
|
+
recordDispatch({ durationMs = 0 } = {}) {
|
|
337
|
+
dispatchSamples += 1;
|
|
338
|
+
dispatchDurationTotal += ensureNumber(durationMs);
|
|
339
|
+
},
|
|
340
|
+
recordDependencyUnlock({ unlockCount = 0 } = {}) {
|
|
341
|
+
dependencyUnlockSamples += 1;
|
|
342
|
+
dependencyUnlockCount += ensureNumber(unlockCount);
|
|
343
|
+
},
|
|
344
|
+
recordPipelinePhase({ snapshotAgeMs = 0 } = {}) {
|
|
345
|
+
pipelineSamples += 1;
|
|
346
|
+
pipelineAgeTotal += ensureNumber(snapshotAgeMs);
|
|
347
|
+
},
|
|
348
|
+
recordFrame({
|
|
349
|
+
frameTimeMs = 16.67,
|
|
350
|
+
targetFrameTimeMs = 16.67,
|
|
351
|
+
gpuBusyMs = 0,
|
|
352
|
+
dropped = false,
|
|
353
|
+
} = {}) {
|
|
354
|
+
frameSamples += 1;
|
|
355
|
+
frameTimeTotal += ensureNumber(frameTimeMs);
|
|
356
|
+
gpuBusyTotal += ensureNumber(gpuBusyMs);
|
|
357
|
+
frameDroppedSamples += dropped === true ? 1 : 0;
|
|
358
|
+
targetFrameTimeMs;
|
|
359
|
+
},
|
|
360
|
+
getSnapshot() {
|
|
361
|
+
const queueAverageDepth = queueSamples > 0 ? queuePeakDepth / queueSamples : 0;
|
|
362
|
+
return {
|
|
363
|
+
frames: {
|
|
364
|
+
sampleCount: frameSamples,
|
|
365
|
+
averageFrameTimeMs: frameSamples > 0 ? frameTimeTotal / frameSamples : 0,
|
|
366
|
+
averageGpuBusyMs: frameSamples > 0 ? gpuBusyTotal / frameSamples : 0,
|
|
367
|
+
droppedCount: frameDroppedSamples,
|
|
368
|
+
},
|
|
369
|
+
queues: {
|
|
370
|
+
sampleCount: queueSamples,
|
|
371
|
+
peakDepth: queuePeakDepth,
|
|
372
|
+
averageDepth: queueAverageDepth,
|
|
373
|
+
},
|
|
374
|
+
dispatch: {
|
|
375
|
+
sampleCount: dispatchSamples,
|
|
376
|
+
averageDurationMs: dispatchSamples > 0 ? dispatchDurationTotal / dispatchSamples : 0,
|
|
377
|
+
},
|
|
378
|
+
dag: {
|
|
379
|
+
peakReadyLaneDepth: readyLanePeakDepth,
|
|
380
|
+
totalUnlockCount: dependencyUnlockCount,
|
|
381
|
+
unlockSamples: dependencyUnlockSamples,
|
|
382
|
+
},
|
|
383
|
+
pipeline: {
|
|
384
|
+
sampleCount: pipelineSamples,
|
|
385
|
+
averageSnapshotAgeMs: pipelineSamples > 0 ? pipelineAgeTotal / pipelineSamples : 0,
|
|
386
|
+
},
|
|
387
|
+
memory: {
|
|
388
|
+
totalTrackedBytes: memoryTotalBytes,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
createGpuDebugSession({ adapter } = {}) {
|
|
397
|
+
return createDebugSession({ adapter });
|
|
398
|
+
},
|
|
399
|
+
createSession({ adapter } = {}) {
|
|
400
|
+
return createDebugSession({ adapter });
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function createFallbackPhysicsFeatureModule() {
|
|
406
|
+
const fallbackPlan = Object.freeze({
|
|
407
|
+
snapshotStageId: "baseline",
|
|
408
|
+
stageOrder: Object.freeze(["authoritative"]),
|
|
409
|
+
secondarySimulationStageIds: Object.freeze(["visual"]),
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
createPhysicsSimulationPlan() {
|
|
413
|
+
return {
|
|
414
|
+
snapshotStageId: "baseline",
|
|
415
|
+
stageOrder: fallbackPlan.stageOrder,
|
|
416
|
+
secondarySimulationStageIds: fallbackPlan.secondarySimulationStageIds,
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
createPhysicsWorldSnapshot(input = {}) {
|
|
420
|
+
return {
|
|
421
|
+
stage: "baseline",
|
|
422
|
+
stability: "stable",
|
|
423
|
+
stageId: fallbackPlan.snapshotStageId,
|
|
424
|
+
metadata: input.metadata ?? {},
|
|
425
|
+
bodyCount: input.bodyCount ?? 0,
|
|
426
|
+
dynamicBodyCount: input.dynamicBodyCount ?? 0,
|
|
427
|
+
contactCount: input.contactCount ?? 0,
|
|
428
|
+
snapshotStageId: fallbackPlan.snapshotStageId,
|
|
429
|
+
};
|
|
430
|
+
},
|
|
431
|
+
defaultPhysicsWorkerProfile: FALLBACK_PHYSICS_PROFILE,
|
|
432
|
+
getPhysicsWorkerManifest() {
|
|
433
|
+
return {
|
|
434
|
+
jobs: [
|
|
435
|
+
Object.freeze({
|
|
436
|
+
worker: Object.freeze({ authority: "authoritative", jobType: "simulate" }),
|
|
437
|
+
}),
|
|
438
|
+
],
|
|
439
|
+
suggestedAllocationIds: Object.freeze(["physics-workspace"]),
|
|
440
|
+
};
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const SHOWCASE_FEATURE_LOADERS = Object.freeze({
|
|
446
|
+
cloth: () => Promise.resolve(createFallbackClothFeatureModule()),
|
|
447
|
+
fluid: () => Promise.resolve(createFallbackFluidFeatureModule()),
|
|
448
|
+
lighting: () => Promise.resolve(createFallbackLightingFeatureModule()),
|
|
449
|
+
performance: () => Promise.resolve(createFallbackPerformanceFeatureModule()),
|
|
450
|
+
debug: () => Promise.resolve(createFallbackDebugFeatureModule()),
|
|
451
|
+
physics: () => Promise.resolve(createFallbackPhysicsFeatureModule()),
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const DEFAULT_FLUID_BAND_THRESHOLDS = Object.freeze({
|
|
455
|
+
near: Object.freeze({ minDistance: 0, maxDistance: 22 }),
|
|
456
|
+
mid: Object.freeze({ minDistance: 22, maxDistance: 90 }),
|
|
457
|
+
far: Object.freeze({ minDistance: 90, maxDistance: 260 }),
|
|
458
|
+
horizon: Object.freeze({ minDistance: 260, maxDistance: Number.POSITIVE_INFINITY }),
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const DEFAULT_CLOTH_BAND_THRESHOLDS = Object.freeze({
|
|
462
|
+
near: Object.freeze({ minDistance: 0, maxDistance: 24 }),
|
|
463
|
+
mid: Object.freeze({ minDistance: 24, maxDistance: 86 }),
|
|
464
|
+
far: Object.freeze({ minDistance: 86, maxDistance: 190 }),
|
|
465
|
+
horizon: Object.freeze({ minDistance: 190, maxDistance: Number.POSITIVE_INFINITY }),
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
function resolveFluidBandSelection(distance, thresholds = DEFAULT_FLUID_BAND_THRESHOLDS) {
|
|
469
|
+
if (!Number.isFinite(distance)) {
|
|
470
|
+
return "horizon";
|
|
471
|
+
}
|
|
472
|
+
if (distance <= (thresholds.near?.maxDistance ?? DEFAULT_FLUID_BAND_THRESHOLDS.near.maxDistance)) {
|
|
473
|
+
return "near";
|
|
474
|
+
}
|
|
475
|
+
if (distance <= (thresholds.mid?.maxDistance ?? DEFAULT_FLUID_BAND_THRESHOLDS.mid.maxDistance)) {
|
|
476
|
+
return "mid";
|
|
477
|
+
}
|
|
478
|
+
if (distance <= (thresholds.far?.maxDistance ?? DEFAULT_FLUID_BAND_THRESHOLDS.far.maxDistance)) {
|
|
479
|
+
return "far";
|
|
480
|
+
}
|
|
481
|
+
return "horizon";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function createFallbackFluidFeatureAdapters() {
|
|
485
|
+
const defaultContinuityBand = Object.freeze({
|
|
486
|
+
amplitudeFloor: 0.22,
|
|
487
|
+
frequencyFloor: 0.05,
|
|
488
|
+
blendWindowMeters: 14,
|
|
489
|
+
retainFoamHistory: true,
|
|
490
|
+
retainDirectionality: true,
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
bodyKinds: Object.freeze(["ocean"]),
|
|
494
|
+
profileNames: Object.freeze(["cinematic"]),
|
|
495
|
+
createContinuityEnvelope() {
|
|
496
|
+
return Object.freeze({
|
|
497
|
+
bands: Object.freeze({
|
|
498
|
+
near: defaultContinuityBand,
|
|
499
|
+
mid: Object.freeze({
|
|
500
|
+
...defaultContinuityBand,
|
|
501
|
+
blendWindowMeters: 22,
|
|
502
|
+
amplitudeFloor: 0.17,
|
|
503
|
+
}),
|
|
504
|
+
far: Object.freeze({
|
|
505
|
+
...defaultContinuityBand,
|
|
506
|
+
blendWindowMeters: 34,
|
|
507
|
+
amplitudeFloor: 0.12,
|
|
508
|
+
}),
|
|
509
|
+
horizon: Object.freeze({
|
|
510
|
+
...defaultContinuityBand,
|
|
511
|
+
blendWindowMeters: 42,
|
|
512
|
+
amplitudeFloor: 0.09,
|
|
513
|
+
}),
|
|
514
|
+
}),
|
|
515
|
+
});
|
|
516
|
+
},
|
|
517
|
+
createPlan({ fluidBodyId = "harbor", kind = "ocean", profile = "cinematic" }) {
|
|
518
|
+
return Object.freeze({
|
|
519
|
+
fluidBodyId,
|
|
520
|
+
kind,
|
|
521
|
+
profile,
|
|
522
|
+
thresholds: DEFAULT_FLUID_BAND_THRESHOLDS,
|
|
523
|
+
representations: Object.freeze([
|
|
524
|
+
Object.freeze({
|
|
525
|
+
band: "near",
|
|
526
|
+
output: "raster",
|
|
527
|
+
rtParticipation: "full",
|
|
528
|
+
shading: Object.freeze({ refraction: 0.14, reflectionMode: "full", caustics: true }),
|
|
529
|
+
}),
|
|
530
|
+
Object.freeze({
|
|
531
|
+
band: "mid",
|
|
532
|
+
output: "raster",
|
|
533
|
+
rtParticipation: "reduced",
|
|
534
|
+
shading: Object.freeze({ reflectionMode: "partial" }),
|
|
535
|
+
}),
|
|
536
|
+
Object.freeze({
|
|
537
|
+
band: "far",
|
|
538
|
+
output: "raster",
|
|
539
|
+
rtParticipation: "low",
|
|
540
|
+
shading: Object.freeze({ reflectionMode: "partial" }),
|
|
541
|
+
}),
|
|
542
|
+
Object.freeze({
|
|
543
|
+
band: "horizon",
|
|
544
|
+
output: "coarse",
|
|
545
|
+
rtParticipation: "off",
|
|
546
|
+
shading: Object.freeze({ reflectionMode: "reduced" }),
|
|
547
|
+
}),
|
|
548
|
+
]),
|
|
549
|
+
});
|
|
550
|
+
},
|
|
551
|
+
selectBand(distance, thresholds = DEFAULT_FLUID_BAND_THRESHOLDS) {
|
|
552
|
+
return resolveFluidBandSelection(distance, thresholds);
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function createFallbackClothFeatureAdapters() {
|
|
558
|
+
const defaultContinuity = Object.freeze({
|
|
559
|
+
amplitudeFloor: 0.22,
|
|
560
|
+
wrinkleFloor: 0.32,
|
|
561
|
+
damping: 0.58,
|
|
562
|
+
creaseBias: 0.14,
|
|
563
|
+
});
|
|
564
|
+
return {
|
|
565
|
+
garmentKinds: Object.freeze(["flag"]),
|
|
566
|
+
profileNames: Object.freeze(["cinematic"]),
|
|
567
|
+
createPlan() {
|
|
568
|
+
return Object.freeze({
|
|
569
|
+
thresholds: DEFAULT_CLOTH_BAND_THRESHOLDS,
|
|
570
|
+
representations: Object.freeze([
|
|
571
|
+
Object.freeze({
|
|
572
|
+
band: "near",
|
|
573
|
+
continuity: defaultContinuity,
|
|
574
|
+
}),
|
|
575
|
+
Object.freeze({
|
|
576
|
+
band: "mid",
|
|
577
|
+
continuity: defaultContinuity,
|
|
578
|
+
}),
|
|
579
|
+
Object.freeze({
|
|
580
|
+
band: "far",
|
|
581
|
+
continuity: defaultContinuity,
|
|
582
|
+
}),
|
|
583
|
+
Object.freeze({
|
|
584
|
+
band: "horizon",
|
|
585
|
+
continuity: defaultContinuity,
|
|
586
|
+
}),
|
|
587
|
+
]),
|
|
588
|
+
});
|
|
589
|
+
},
|
|
590
|
+
selectBand(distance, thresholds = DEFAULT_CLOTH_BAND_THRESHOLDS) {
|
|
591
|
+
if (!Number.isFinite(distance)) {
|
|
592
|
+
return "horizon";
|
|
593
|
+
}
|
|
594
|
+
if (distance <= (thresholds.near?.maxDistance ?? DEFAULT_CLOTH_BAND_THRESHOLDS.near.maxDistance)) {
|
|
595
|
+
return "near";
|
|
596
|
+
}
|
|
597
|
+
if (distance <= (thresholds.mid?.maxDistance ?? DEFAULT_CLOTH_BAND_THRESHOLDS.mid.maxDistance)) {
|
|
598
|
+
return "mid";
|
|
599
|
+
}
|
|
600
|
+
if (distance <= (thresholds.far?.maxDistance ?? DEFAULT_CLOTH_BAND_THRESHOLDS.far.maxDistance)) {
|
|
601
|
+
return "far";
|
|
602
|
+
}
|
|
603
|
+
return "horizon";
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function normalizeClothFeatureAdapters(clothFeatures) {
|
|
609
|
+
const fallback = createFallbackClothFeatureAdapters();
|
|
610
|
+
if (clothFeatures == null || typeof clothFeatures !== "object") {
|
|
611
|
+
return fallback;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
garmentKinds:
|
|
616
|
+
Array.isArray(clothFeatures.garmentKinds) && clothFeatures.garmentKinds.length > 0
|
|
617
|
+
? clothFeatures.garmentKinds
|
|
618
|
+
: fallback.garmentKinds,
|
|
619
|
+
profileNames:
|
|
620
|
+
Array.isArray(clothFeatures.profileNames) && clothFeatures.profileNames.length > 0
|
|
621
|
+
? clothFeatures.profileNames
|
|
622
|
+
: fallback.profileNames,
|
|
623
|
+
createPlan: assertRequiredFunction(clothFeatures, "cloth", "createPlan"),
|
|
624
|
+
selectBand: assertRequiredFunction(clothFeatures, "cloth", "selectBand"),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function normalizeFluidFeatureAdapters(fluidFeatures) {
|
|
629
|
+
const fallback = createFallbackFluidFeatureAdapters();
|
|
630
|
+
if (fluidFeatures == null || typeof fluidFeatures !== "object") {
|
|
631
|
+
return fallback;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
bodyKinds:
|
|
636
|
+
Array.isArray(fluidFeatures.bodyKinds) && fluidFeatures.bodyKinds.length > 0
|
|
637
|
+
? fluidFeatures.bodyKinds
|
|
638
|
+
: fallback.bodyKinds,
|
|
639
|
+
profileNames:
|
|
640
|
+
Array.isArray(fluidFeatures.profileNames) && fluidFeatures.profileNames.length > 0
|
|
641
|
+
? fluidFeatures.profileNames
|
|
642
|
+
: fallback.profileNames,
|
|
643
|
+
createContinuityEnvelope: assertRequiredFunction(
|
|
644
|
+
fluidFeatures,
|
|
645
|
+
"fluid",
|
|
646
|
+
"createContinuityEnvelope"
|
|
647
|
+
),
|
|
648
|
+
createPlan: assertRequiredFunction(fluidFeatures, "fluid", "createPlan"),
|
|
649
|
+
selectBand: assertRequiredFunction(fluidFeatures, "fluid", "selectBand"),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function assertRequiredFunction(module, featureLabel, exportName) {
|
|
654
|
+
const value = module?.[exportName];
|
|
655
|
+
if (typeof value !== "function") {
|
|
656
|
+
throw new Error(
|
|
657
|
+
`Showcase ${featureLabel} feature package must export "${exportName}" as a function.`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
return value;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function assertRequiredArray(module, featureLabel, exportName) {
|
|
664
|
+
const value = module?.[exportName];
|
|
665
|
+
if (!Array.isArray(value)) {
|
|
666
|
+
throw new Error(
|
|
667
|
+
`Showcase ${featureLabel} feature package must export "${exportName}" as an array.`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
return value;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function loadShowcaseFeatureModule(featureLabel, loader) {
|
|
674
|
+
try {
|
|
675
|
+
const module = await loader();
|
|
676
|
+
if (module == null || typeof module !== "object") {
|
|
677
|
+
throw new Error("module is missing or not an object.");
|
|
678
|
+
}
|
|
679
|
+
return module;
|
|
680
|
+
} catch (error) {
|
|
681
|
+
const message = error?.message ?? String(error);
|
|
682
|
+
throw new Error(`Unable to load showcase ${featureLabel} feature package: ${message}`, {
|
|
683
|
+
cause: error,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function resolveShowcaseFeatureLoaders(options = {}) {
|
|
689
|
+
const overrides = options.__showcaseFeatureLoaders;
|
|
690
|
+
return {
|
|
691
|
+
cloth:
|
|
692
|
+
typeof overrides?.cloth === "function"
|
|
693
|
+
? overrides.cloth
|
|
694
|
+
: SHOWCASE_FEATURE_LOADERS.cloth,
|
|
695
|
+
fluid:
|
|
696
|
+
typeof overrides?.fluid === "function"
|
|
697
|
+
? overrides.fluid
|
|
698
|
+
: SHOWCASE_FEATURE_LOADERS.fluid,
|
|
699
|
+
lighting:
|
|
700
|
+
typeof overrides?.lighting === "function"
|
|
701
|
+
? overrides.lighting
|
|
702
|
+
: SHOWCASE_FEATURE_LOADERS.lighting,
|
|
703
|
+
performance:
|
|
704
|
+
typeof overrides?.performance === "function"
|
|
705
|
+
? overrides.performance
|
|
706
|
+
: SHOWCASE_FEATURE_LOADERS.performance,
|
|
707
|
+
debug:
|
|
708
|
+
typeof overrides?.debug === "function"
|
|
709
|
+
? overrides.debug
|
|
710
|
+
: SHOWCASE_FEATURE_LOADERS.debug,
|
|
711
|
+
physics:
|
|
712
|
+
typeof overrides?.physics === "function"
|
|
713
|
+
? overrides.physics
|
|
714
|
+
: SHOWCASE_FEATURE_LOADERS.physics,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function resolveShowcaseFeatureAdapters(options = {}) {
|
|
719
|
+
const loaders = resolveShowcaseFeatureLoaders(options);
|
|
720
|
+
const [
|
|
721
|
+
clothModule,
|
|
722
|
+
fluidModule,
|
|
723
|
+
lightingModule,
|
|
724
|
+
performanceModule,
|
|
725
|
+
debugModule,
|
|
726
|
+
physicsModule,
|
|
727
|
+
] = await Promise.all([
|
|
728
|
+
loadShowcaseFeatureModule("cloth", loaders.cloth),
|
|
729
|
+
loadShowcaseFeatureModule("fluid", loaders.fluid),
|
|
730
|
+
loadShowcaseFeatureModule("lighting", loaders.lighting),
|
|
731
|
+
loadShowcaseFeatureModule("performance", loaders.performance),
|
|
732
|
+
loadShowcaseFeatureModule("debug", loaders.debug),
|
|
733
|
+
loadShowcaseFeatureModule("physics", loaders.physics),
|
|
734
|
+
]);
|
|
735
|
+
|
|
736
|
+
return {
|
|
737
|
+
cloth: {
|
|
738
|
+
garmentKinds: assertRequiredArray(clothModule, "cloth", "clothGarmentKinds"),
|
|
739
|
+
profileNames: assertRequiredArray(clothModule, "cloth", "clothProfileNames"),
|
|
740
|
+
createPlan: assertRequiredFunction(clothModule, "cloth", "createClothRepresentationPlan"),
|
|
741
|
+
selectBand: assertRequiredFunction(clothModule, "cloth", "selectClothRepresentationBand"),
|
|
742
|
+
},
|
|
743
|
+
fluid: {
|
|
744
|
+
bodyKinds: assertRequiredArray(fluidModule, "fluid", "fluidBodyKinds"),
|
|
745
|
+
profileNames: assertRequiredArray(fluidModule, "fluid", "fluidProfileNames"),
|
|
746
|
+
createContinuityEnvelope: assertRequiredFunction(
|
|
747
|
+
fluidModule,
|
|
748
|
+
"fluid",
|
|
749
|
+
"createFluidContinuityEnvelope"
|
|
750
|
+
),
|
|
751
|
+
createPlan: assertRequiredFunction(fluidModule, "fluid", "createFluidRepresentationPlan"),
|
|
752
|
+
selectBand: assertRequiredFunction(fluidModule, "fluid", "selectFluidRepresentationBand"),
|
|
753
|
+
},
|
|
754
|
+
lighting: {
|
|
755
|
+
createBandPlan: assertRequiredFunction(lightingModule, "lighting", "createLightingBandPlan"),
|
|
756
|
+
defaultProfile: lightingModule.defaultLightingProfile,
|
|
757
|
+
getProfile: assertRequiredFunction(lightingModule, "lighting", "getLightingProfile"),
|
|
758
|
+
distanceBands: assertRequiredArray(lightingModule, "lighting", "lightingDistanceBands"),
|
|
759
|
+
},
|
|
760
|
+
performance: {
|
|
761
|
+
createDeviceProfile: assertRequiredFunction(
|
|
762
|
+
performanceModule,
|
|
763
|
+
"performance",
|
|
764
|
+
"createDeviceProfile"
|
|
765
|
+
),
|
|
766
|
+
createGovernor: assertRequiredFunction(
|
|
767
|
+
performanceModule,
|
|
768
|
+
"performance",
|
|
769
|
+
"createGpuPerformanceGovernor"
|
|
770
|
+
),
|
|
771
|
+
createQualityAdapter: assertRequiredFunction(
|
|
772
|
+
performanceModule,
|
|
773
|
+
"performance",
|
|
774
|
+
"createQualityLadderAdapter"
|
|
775
|
+
),
|
|
776
|
+
},
|
|
777
|
+
debug: {
|
|
778
|
+
createSession: assertRequiredFunction(debugModule, "debug", "createGpuDebugSession"),
|
|
779
|
+
},
|
|
780
|
+
physics: {
|
|
781
|
+
createSimulationPlan: assertRequiredFunction(
|
|
782
|
+
physicsModule,
|
|
783
|
+
"physics",
|
|
784
|
+
"createPhysicsSimulationPlan"
|
|
785
|
+
),
|
|
786
|
+
createWorldSnapshot: assertRequiredFunction(
|
|
787
|
+
physicsModule,
|
|
788
|
+
"physics",
|
|
789
|
+
"createPhysicsWorldSnapshot"
|
|
790
|
+
),
|
|
791
|
+
defaultProfile: physicsModule.defaultPhysicsWorkerProfile,
|
|
792
|
+
getManifest: assertRequiredFunction(physicsModule, "physics", "getPhysicsWorkerManifest"),
|
|
793
|
+
},
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export const showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
|
|
798
|
+
|
|
799
|
+
const FOCUS_MODE_TRANSLATION_KEYS = Object.freeze({
|
|
800
|
+
integrated: gpuSharedTranslationKeys.focusIntegrated,
|
|
801
|
+
lighting: gpuSharedTranslationKeys.focusLighting,
|
|
802
|
+
cloth: gpuSharedTranslationKeys.focusCloth,
|
|
803
|
+
fluid: gpuSharedTranslationKeys.focusFluid,
|
|
804
|
+
physics: gpuSharedTranslationKeys.focusPhysics,
|
|
805
|
+
performance: gpuSharedTranslationKeys.focusPerformance,
|
|
806
|
+
debug: gpuSharedTranslationKeys.focusDebug,
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
const SCENE_NOTE_KEYS = Object.freeze([
|
|
810
|
+
gpuSharedTranslationKeys.noteAssetLoading,
|
|
811
|
+
gpuSharedTranslationKeys.noteMoonlight,
|
|
812
|
+
gpuSharedTranslationKeys.noteContinuity,
|
|
813
|
+
gpuSharedTranslationKeys.notePerformance,
|
|
814
|
+
]);
|
|
815
|
+
|
|
816
|
+
const PHYSICS_SCENE_NOTE_KEYS = Object.freeze([
|
|
817
|
+
gpuSharedTranslationKeys.notePhysicsSnapshots,
|
|
818
|
+
gpuSharedTranslationKeys.notePhysicsCollisions,
|
|
819
|
+
gpuSharedTranslationKeys.notePhysicsLighting,
|
|
820
|
+
]);
|
|
821
|
+
|
|
822
|
+
const LEGACY_HARBOR_LAYOUT = Object.freeze([
|
|
823
|
+
Object.freeze({
|
|
824
|
+
position: Object.freeze({ x: -8.2, y: 1.1, z: -0.9 }),
|
|
825
|
+
rotationY: -0.16,
|
|
826
|
+
scale: 5.4,
|
|
827
|
+
color: { r: 0.32, g: 0.27, b: 0.23, a: 1 },
|
|
828
|
+
accent: 0.06,
|
|
829
|
+
}),
|
|
830
|
+
Object.freeze({
|
|
831
|
+
position: Object.freeze({ x: -5.7, y: 0.45, z: 1.4 }),
|
|
832
|
+
rotationY: -0.08,
|
|
833
|
+
scale: { x: 6.8, y: 0.3, z: 2.1 },
|
|
834
|
+
color: { r: 0.31, g: 0.31, b: 0.34, a: 1 },
|
|
835
|
+
accent: 0.04,
|
|
836
|
+
}),
|
|
837
|
+
Object.freeze({
|
|
838
|
+
position: Object.freeze({ x: -10.4, y: 0.28, z: 0.8 }),
|
|
839
|
+
rotationY: 0.22,
|
|
840
|
+
scale: { x: 1.2, y: 0.9, z: 1.2 },
|
|
841
|
+
color: { r: 0.31, g: 0.35, b: 0.39, a: 1 },
|
|
842
|
+
accent: 0.02,
|
|
843
|
+
}),
|
|
844
|
+
]);
|
|
845
|
+
|
|
846
|
+
const SHOWCASE_ENVIRONMENT_LAYOUT = Object.freeze([
|
|
847
|
+
Object.freeze({
|
|
848
|
+
assetKey: "harbor-dock",
|
|
849
|
+
position: Object.freeze({ x: -4.6, y: 0.16, z: 0.7 }),
|
|
850
|
+
rotationY: -0.08,
|
|
851
|
+
scale: 0.84,
|
|
852
|
+
accent: 0.04,
|
|
853
|
+
}),
|
|
854
|
+
Object.freeze({
|
|
855
|
+
assetKey: "lighthouse",
|
|
856
|
+
position: Object.freeze({ x: -9.8, y: 0, z: -0.58 }),
|
|
857
|
+
rotationY: 0.12,
|
|
858
|
+
scale: 0.56,
|
|
859
|
+
accent: 0.08,
|
|
860
|
+
}),
|
|
861
|
+
]);
|
|
862
|
+
|
|
87
863
|
const SHIP_LANTERNS = Object.freeze([
|
|
88
864
|
Object.freeze({ x: 0.94, y: 1.54, z: 2.52, glow: 1 }),
|
|
89
865
|
Object.freeze({ x: -0.9, y: 1.58, z: 2.44, glow: 0.92 }),
|
|
90
866
|
Object.freeze({ x: 0.62, y: 1.42, z: -2.18, glow: 0.88 }),
|
|
91
867
|
Object.freeze({ x: -0.58, y: 1.46, z: -2.04, glow: 0.84 }),
|
|
92
868
|
]);
|
|
869
|
+
const CUTTER_LANTERNS = Object.freeze([
|
|
870
|
+
Object.freeze({ x: 0.42, y: 1.04, z: 1.18, glow: 0.94 }),
|
|
871
|
+
Object.freeze({ x: -0.42, y: 1.04, z: 1.12, glow: 0.88 }),
|
|
872
|
+
]);
|
|
93
873
|
|
|
94
874
|
const HARBOR_TORCHES = Object.freeze([
|
|
95
875
|
Object.freeze({ x: -5.2, y: 1.25, z: 1.36, glow: 1.1 }),
|
|
@@ -128,6 +908,11 @@ function injectStyles() {
|
|
|
128
908
|
radial-gradient(circle at 82% 18%, rgba(240, 188, 103, 0.08), transparent 18%),
|
|
129
909
|
linear-gradient(180deg, #04101d 0%, #0b1930 42%, #081321 100%);
|
|
130
910
|
}
|
|
911
|
+
.${ROOT_CLASS}.${CAPTURE_CLASS} {
|
|
912
|
+
min-height: 100vh;
|
|
913
|
+
overflow: hidden;
|
|
914
|
+
background: #030710;
|
|
915
|
+
}
|
|
131
916
|
.${ROOT_CLASS},
|
|
132
917
|
.${ROOT_CLASS} * {
|
|
133
918
|
box-sizing: border-box;
|
|
@@ -207,6 +992,40 @@ function injectStyles() {
|
|
|
207
992
|
border: 1px solid rgba(159, 185, 223, 0.12);
|
|
208
993
|
background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
|
|
209
994
|
}
|
|
995
|
+
.${CAPTURE_CLASS} .plasius-demo {
|
|
996
|
+
width: 100vw;
|
|
997
|
+
height: 100vh;
|
|
998
|
+
padding: 0;
|
|
999
|
+
display: block;
|
|
1000
|
+
}
|
|
1001
|
+
.${CAPTURE_CLASS} .plasius-demo__hero,
|
|
1002
|
+
.${CAPTURE_CLASS} .plasius-demo__toolbar,
|
|
1003
|
+
.${CAPTURE_CLASS} .plasius-demo__legend,
|
|
1004
|
+
.${CAPTURE_CLASS} .plasius-demo__sidebar,
|
|
1005
|
+
.${CAPTURE_CLASS} .plasius-demo__footer {
|
|
1006
|
+
display: none;
|
|
1007
|
+
}
|
|
1008
|
+
.${CAPTURE_CLASS} .plasius-demo__layout {
|
|
1009
|
+
display: block;
|
|
1010
|
+
height: 100%;
|
|
1011
|
+
}
|
|
1012
|
+
.${CAPTURE_CLASS} .plasius-demo__canvas-panel {
|
|
1013
|
+
height: 100%;
|
|
1014
|
+
padding: 0;
|
|
1015
|
+
border: 0;
|
|
1016
|
+
border-radius: 0;
|
|
1017
|
+
background: transparent;
|
|
1018
|
+
box-shadow: none;
|
|
1019
|
+
backdrop-filter: none;
|
|
1020
|
+
}
|
|
1021
|
+
.${CAPTURE_CLASS} .plasius-demo__canvas {
|
|
1022
|
+
width: 100%;
|
|
1023
|
+
height: 100%;
|
|
1024
|
+
aspect-ratio: auto;
|
|
1025
|
+
border: 0;
|
|
1026
|
+
border-radius: 0;
|
|
1027
|
+
background: #030710;
|
|
1028
|
+
}
|
|
210
1029
|
.plasius-demo__toolbar {
|
|
211
1030
|
position: absolute;
|
|
212
1031
|
top: 26px;
|
|
@@ -381,6 +1200,15 @@ function transformPoint(point, transform) {
|
|
|
381
1200
|
return addVec3(rotated, transform.position);
|
|
382
1201
|
}
|
|
383
1202
|
|
|
1203
|
+
function transformDirection(direction, transform) {
|
|
1204
|
+
const scale =
|
|
1205
|
+
typeof transform.scale === "number"
|
|
1206
|
+
? { x: transform.scale, y: transform.scale, z: transform.scale }
|
|
1207
|
+
: transform.scale;
|
|
1208
|
+
const scaled = vec3(direction.x * scale.x, direction.y * scale.y, direction.z * scale.z);
|
|
1209
|
+
return normalizeVec3(rotateY(scaled, transform.rotationY));
|
|
1210
|
+
}
|
|
1211
|
+
|
|
384
1212
|
function projectPoint(point, camera, viewport) {
|
|
385
1213
|
const relative = subVec3(point, camera.eye);
|
|
386
1214
|
const viewX = dotVec3(relative, camera.right);
|
|
@@ -406,6 +1234,92 @@ function colorToRgba(color, alpha = 1) {
|
|
|
406
1234
|
return `rgba(${r}, ${g}, ${b}, ${clamp(alpha, 0, 1)})`;
|
|
407
1235
|
}
|
|
408
1236
|
|
|
1237
|
+
function mixColor(a, b, t) {
|
|
1238
|
+
return {
|
|
1239
|
+
r: mix(a.r, b.r, t),
|
|
1240
|
+
g: mix(a.g, b.g, t),
|
|
1241
|
+
b: mix(a.b, b.b, t),
|
|
1242
|
+
a: mix(a.a ?? 1, b.a ?? 1, t),
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function multiplyColor(a, b) {
|
|
1247
|
+
return {
|
|
1248
|
+
r: a.r * b.r,
|
|
1249
|
+
g: a.g * b.g,
|
|
1250
|
+
b: a.b * b.b,
|
|
1251
|
+
a: (a.a ?? 1) * (b.a ?? 1),
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function createLegacyMeshPrimitive(mesh) {
|
|
1256
|
+
return Object.freeze({
|
|
1257
|
+
name: mesh.name ?? "legacy-mesh",
|
|
1258
|
+
positions: mesh.positions,
|
|
1259
|
+
indices: mesh.indices,
|
|
1260
|
+
normals: null,
|
|
1261
|
+
colors: null,
|
|
1262
|
+
material: Object.freeze({
|
|
1263
|
+
name: "legacy-material",
|
|
1264
|
+
color: mesh.color ?? { r: 0.56, g: 0.33, b: 0.22, a: 1 },
|
|
1265
|
+
roughness: 0.88,
|
|
1266
|
+
metallic: 0.08,
|
|
1267
|
+
emissive: Object.freeze({ r: 0, g: 0, b: 0 }),
|
|
1268
|
+
}),
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function isFeatureEnabled(featureFlags, featureName, fallback = true) {
|
|
1273
|
+
const directValue =
|
|
1274
|
+
typeof featureFlags?.[featureName] === "boolean"
|
|
1275
|
+
? featureFlags[featureName]
|
|
1276
|
+
: featureFlags?.flags?.[featureName];
|
|
1277
|
+
if (typeof directValue === "boolean") {
|
|
1278
|
+
return directValue;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const enabledValue =
|
|
1282
|
+
typeof featureFlags?.enabled?.[featureName] === "boolean"
|
|
1283
|
+
? featureFlags.enabled[featureName]
|
|
1284
|
+
: undefined;
|
|
1285
|
+
if (typeof enabledValue === "boolean") {
|
|
1286
|
+
return enabledValue;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
return fallback;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function getMeshPrimitives(mesh) {
|
|
1293
|
+
return Array.isArray(mesh?.primitives) && mesh.primitives.length > 0
|
|
1294
|
+
? mesh.primitives
|
|
1295
|
+
: [createLegacyMeshPrimitive(mesh)];
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function tintPrimitiveColor(material, colorOverride) {
|
|
1299
|
+
if (!colorOverride) {
|
|
1300
|
+
return material.color;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const name = String(material.name ?? "").toLowerCase();
|
|
1304
|
+
if (name.includes("sail") || name.includes("glass") || name.includes("roof")) {
|
|
1305
|
+
return material.color;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const tintAmount = name.includes("hull")
|
|
1309
|
+
? 0.54
|
|
1310
|
+
: name.includes("trim")
|
|
1311
|
+
? 0.22
|
|
1312
|
+
: name.includes("deck")
|
|
1313
|
+
? 0.12
|
|
1314
|
+
: 0;
|
|
1315
|
+
|
|
1316
|
+
if (tintAmount <= 0) {
|
|
1317
|
+
return material.color;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
return mixColor(material.color, multiplyColor(material.color, colorOverride), tintAmount);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
409
1323
|
function projectShadowPoint(point, lightDir, planeY) {
|
|
410
1324
|
const shadowDir = scaleVec3(lightDir, -1);
|
|
411
1325
|
if (Math.abs(shadowDir.y) < 0.0001) {
|
|
@@ -430,6 +1344,64 @@ function shadeColor(base, normal, lightDir, heightBias = 0, accent = 0) {
|
|
|
430
1344
|
};
|
|
431
1345
|
}
|
|
432
1346
|
|
|
1347
|
+
function getMaterialSeed(materialName) {
|
|
1348
|
+
let seed = 0;
|
|
1349
|
+
for (let index = 0; index < materialName.length; index += 1) {
|
|
1350
|
+
seed += materialName.charCodeAt(index) * (index + 1);
|
|
1351
|
+
}
|
|
1352
|
+
return seed;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function getMaterialDetailStrength(material, surfaceType) {
|
|
1356
|
+
const name = String(material?.name ?? "").toLowerCase();
|
|
1357
|
+
if (surfaceType === "water" || name.includes("glass")) {
|
|
1358
|
+
return 0.018;
|
|
1359
|
+
}
|
|
1360
|
+
if (name.includes("wood") || name.includes("timber") || name.includes("plank")) {
|
|
1361
|
+
return 0.13;
|
|
1362
|
+
}
|
|
1363
|
+
if (name.includes("stone") || name.includes("concrete") || name.includes("plaster")) {
|
|
1364
|
+
return 0.1;
|
|
1365
|
+
}
|
|
1366
|
+
if (name.includes("roof") || name.includes("crate")) {
|
|
1367
|
+
return 0.09;
|
|
1368
|
+
}
|
|
1369
|
+
if (name.includes("paint")) {
|
|
1370
|
+
return 0.045;
|
|
1371
|
+
}
|
|
1372
|
+
if (name.includes("metal")) {
|
|
1373
|
+
return 0.035;
|
|
1374
|
+
}
|
|
1375
|
+
return 0.04;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function applyMaterialDetail(color, material, worldCenter, normal, surfaceType) {
|
|
1379
|
+
const materialName = String(material?.name ?? surfaceType ?? "material");
|
|
1380
|
+
const detailStrength = getMaterialDetailStrength(material, surfaceType);
|
|
1381
|
+
const sample =
|
|
1382
|
+
worldCenter.x * 3.17 +
|
|
1383
|
+
worldCenter.y * 5.29 +
|
|
1384
|
+
worldCenter.z * 7.83 +
|
|
1385
|
+
getMaterialSeed(materialName) * 0.013;
|
|
1386
|
+
const grain = (pseudoRandom(sample) - 0.5) * detailStrength;
|
|
1387
|
+
const lowerSurface = smoothstep(7.5, -0.8, worldCenter.y);
|
|
1388
|
+
const verticalSurface = 1 - clamp(Math.abs(normal.y), 0, 1);
|
|
1389
|
+
const materialLowerWear =
|
|
1390
|
+
/stone|concrete|plaster|paint|wood|timber|plank|crate/.test(materialName.toLowerCase())
|
|
1391
|
+
? lowerSurface * verticalSurface * 0.055
|
|
1392
|
+
: 0;
|
|
1393
|
+
const wetlineWear =
|
|
1394
|
+
surfaceType === "ship" && worldCenter.y < 0.72
|
|
1395
|
+
? smoothstep(0.72, -0.1, worldCenter.y) * 0.05
|
|
1396
|
+
: 0;
|
|
1397
|
+
|
|
1398
|
+
return {
|
|
1399
|
+
r: clamp(color.r * (1 + grain) - materialLowerWear - wetlineWear, 0, 1),
|
|
1400
|
+
g: clamp(color.g * (1 + grain * 0.82) - materialLowerWear * 0.9 - wetlineWear, 0, 1),
|
|
1401
|
+
b: clamp(color.b * (1 + grain * 0.62) - materialLowerWear * 0.68 - wetlineWear * 0.75, 0, 1),
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
|
|
433
1405
|
function buildCamera(state, canvas) {
|
|
434
1406
|
const preset = CAMERA_PRESETS[state.focus] ?? CAMERA_PRESETS.integrated;
|
|
435
1407
|
const yaw = state.camera.yaw ?? preset.yaw;
|
|
@@ -455,50 +1427,170 @@ function buildCamera(state, canvas) {
|
|
|
455
1427
|
};
|
|
456
1428
|
}
|
|
457
1429
|
|
|
458
|
-
function buildTrianglesFromMesh(
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1430
|
+
function buildTrianglesFromMesh(
|
|
1431
|
+
mesh,
|
|
1432
|
+
transform,
|
|
1433
|
+
colorOverride,
|
|
1434
|
+
camera,
|
|
1435
|
+
viewport,
|
|
1436
|
+
triangles,
|
|
1437
|
+
options = {}
|
|
1438
|
+
) {
|
|
1439
|
+
const primitives = getMeshPrimitives(mesh);
|
|
1440
|
+
for (const primitive of primitives) {
|
|
1441
|
+
const resolvedColor = tintPrimitiveColor(primitive.material, colorOverride);
|
|
1442
|
+
for (let index = 0; index < primitive.indices.length; index += 3) {
|
|
1443
|
+
const aIndex = primitive.indices[index] * 3;
|
|
1444
|
+
const bIndex = primitive.indices[index + 1] * 3;
|
|
1445
|
+
const cIndex = primitive.indices[index + 2] * 3;
|
|
1446
|
+
|
|
1447
|
+
const a = transformPoint(
|
|
1448
|
+
vec3(
|
|
1449
|
+
primitive.positions[aIndex],
|
|
1450
|
+
primitive.positions[aIndex + 1],
|
|
1451
|
+
primitive.positions[aIndex + 2]
|
|
1452
|
+
),
|
|
1453
|
+
transform
|
|
1454
|
+
);
|
|
1455
|
+
const b = transformPoint(
|
|
1456
|
+
vec3(
|
|
1457
|
+
primitive.positions[bIndex],
|
|
1458
|
+
primitive.positions[bIndex + 1],
|
|
1459
|
+
primitive.positions[bIndex + 2]
|
|
1460
|
+
),
|
|
1461
|
+
transform
|
|
1462
|
+
);
|
|
1463
|
+
const c = transformPoint(
|
|
1464
|
+
vec3(
|
|
1465
|
+
primitive.positions[cIndex],
|
|
1466
|
+
primitive.positions[cIndex + 1],
|
|
1467
|
+
primitive.positions[cIndex + 2]
|
|
1468
|
+
),
|
|
1469
|
+
transform
|
|
1470
|
+
);
|
|
463
1471
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
1472
|
+
const ab = subVec3(b, a);
|
|
1473
|
+
const ac = subVec3(c, a);
|
|
1474
|
+
const faceNormal = normalizeVec3(crossVec3(ab, ac));
|
|
1475
|
+
let normal = faceNormal;
|
|
1476
|
+
if (Array.isArray(primitive.normals)) {
|
|
1477
|
+
const aNormal = transformDirection(
|
|
1478
|
+
vec3(
|
|
1479
|
+
primitive.normals[aIndex],
|
|
1480
|
+
primitive.normals[aIndex + 1],
|
|
1481
|
+
primitive.normals[aIndex + 2]
|
|
1482
|
+
),
|
|
1483
|
+
transform
|
|
1484
|
+
);
|
|
1485
|
+
const bNormal = transformDirection(
|
|
1486
|
+
vec3(
|
|
1487
|
+
primitive.normals[bIndex],
|
|
1488
|
+
primitive.normals[bIndex + 1],
|
|
1489
|
+
primitive.normals[bIndex + 2]
|
|
1490
|
+
),
|
|
1491
|
+
transform
|
|
1492
|
+
);
|
|
1493
|
+
const cNormal = transformDirection(
|
|
1494
|
+
vec3(
|
|
1495
|
+
primitive.normals[cIndex],
|
|
1496
|
+
primitive.normals[cIndex + 1],
|
|
1497
|
+
primitive.normals[cIndex + 2]
|
|
1498
|
+
),
|
|
1499
|
+
transform
|
|
1500
|
+
);
|
|
1501
|
+
normal = normalizeVec3(
|
|
1502
|
+
scaleVec3(addVec3(addVec3(aNormal, bNormal), cNormal), 1 / 3)
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
476
1505
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (dotVec3(normal, viewDir) <= 0) {
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
1506
|
+
const viewDir = normalizeVec3(subVec3(camera.eye, a));
|
|
1507
|
+
if (dotVec3(faceNormal, viewDir) <= 0) {
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
484
1510
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1511
|
+
const projected = [
|
|
1512
|
+
projectPoint(a, camera, viewport),
|
|
1513
|
+
projectPoint(b, camera, viewport),
|
|
1514
|
+
projectPoint(c, camera, viewport),
|
|
1515
|
+
];
|
|
1516
|
+
if (projected.some((value) => value === null)) {
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
489
1519
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1520
|
+
triangles.push({
|
|
1521
|
+
points: projected,
|
|
1522
|
+
depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
|
|
1523
|
+
worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
|
|
1524
|
+
normal,
|
|
1525
|
+
baseColor: resolvedColor,
|
|
1526
|
+
accent: options.accent ?? 0,
|
|
1527
|
+
material: primitive.material,
|
|
1528
|
+
reflection: options.reflection ?? 0,
|
|
1529
|
+
surfaceType: options.surfaceType ?? "solid",
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
498
1532
|
}
|
|
499
1533
|
}
|
|
500
1534
|
|
|
501
|
-
function
|
|
1535
|
+
async function loadShowcaseAssetCatalog() {
|
|
1536
|
+
const [brigantine, cutter, lighthouse, harborDock] = await Promise.all([
|
|
1537
|
+
loadGltfModel(resolveShowcaseAssetUrl("brigantine")),
|
|
1538
|
+
loadGltfModel(resolveShowcaseAssetUrl("cutter")),
|
|
1539
|
+
loadGltfModel(resolveShowcaseAssetUrl("lighthouse")),
|
|
1540
|
+
loadGltfModel(resolveShowcaseAssetUrl("harbor-dock")),
|
|
1541
|
+
]);
|
|
1542
|
+
|
|
1543
|
+
return Object.freeze({
|
|
1544
|
+
primaryShipKey: "brigantine",
|
|
1545
|
+
ships: Object.freeze({
|
|
1546
|
+
brigantine,
|
|
1547
|
+
cutter,
|
|
1548
|
+
}),
|
|
1549
|
+
environment: Object.freeze({
|
|
1550
|
+
lighthouse,
|
|
1551
|
+
"harbor-dock": harborDock,
|
|
1552
|
+
}),
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function createLegacyShowcaseAssetCatalog() {
|
|
1557
|
+
const brigantine = loadGltfModel(resolveShowcaseAssetUrl("brigantine"));
|
|
1558
|
+
return Promise.resolve(brigantine).then((primary) =>
|
|
1559
|
+
Object.freeze({
|
|
1560
|
+
primaryShipKey: "brigantine",
|
|
1561
|
+
ships: Object.freeze({
|
|
1562
|
+
brigantine: primary,
|
|
1563
|
+
}),
|
|
1564
|
+
environment: Object.freeze({}),
|
|
1565
|
+
})
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function resolveShipModel(state, ship, fallbackModel = null) {
|
|
1570
|
+
return (
|
|
1571
|
+
state.assetCatalog?.ships?.[ship.modelKey ?? state.assetCatalog?.primaryShipKey ?? "brigantine"] ??
|
|
1572
|
+
fallbackModel ??
|
|
1573
|
+
state.shipModel
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function createPerformanceGovernor(performanceFeatures) {
|
|
1578
|
+
const createQualityLadderAdapter = assertRequiredFunction(
|
|
1579
|
+
performanceFeatures,
|
|
1580
|
+
"performance",
|
|
1581
|
+
"createQualityAdapter"
|
|
1582
|
+
);
|
|
1583
|
+
const createDeviceProfile = assertRequiredFunction(
|
|
1584
|
+
performanceFeatures,
|
|
1585
|
+
"performance",
|
|
1586
|
+
"createDeviceProfile"
|
|
1587
|
+
);
|
|
1588
|
+
const createGpuPerformanceGovernor = assertRequiredFunction(
|
|
1589
|
+
performanceFeatures,
|
|
1590
|
+
"performance",
|
|
1591
|
+
"createGovernor"
|
|
1592
|
+
);
|
|
1593
|
+
|
|
502
1594
|
const fluidDetail = createQualityLadderAdapter({
|
|
503
1595
|
id: "fluid-detail",
|
|
504
1596
|
domain: "geometry",
|
|
@@ -554,6 +1646,7 @@ function createPerformanceGovernor() {
|
|
|
554
1646
|
}
|
|
555
1647
|
|
|
556
1648
|
function buildDemoDom(root, options) {
|
|
1649
|
+
const t = options.translate;
|
|
557
1650
|
root.innerHTML = `
|
|
558
1651
|
<main class="plasius-demo">
|
|
559
1652
|
<section class="plasius-demo__hero">
|
|
@@ -563,56 +1656,55 @@ function buildDemoDom(root, options) {
|
|
|
563
1656
|
<p class="plasius-demo__lead">${options.subtitle}</p>
|
|
564
1657
|
</section>
|
|
565
1658
|
<section class="plasius-panel plasius-demo__status">
|
|
566
|
-
<p id="demoStatus" class="plasius-demo__status-badge"
|
|
1659
|
+
<p id="demoStatus" class="plasius-demo__status-badge">${t(gpuSharedTranslationKeys.statusBooting)}</p>
|
|
567
1660
|
<p id="demoDetails" class="plasius-demo__status-text">
|
|
568
|
-
|
|
1661
|
+
${t(gpuSharedTranslationKeys.detailsBooting)}
|
|
569
1662
|
</p>
|
|
570
1663
|
</section>
|
|
571
1664
|
</section>
|
|
572
1665
|
<section class="plasius-demo__layout">
|
|
573
1666
|
<section class="plasius-panel plasius-demo__canvas-panel">
|
|
574
|
-
<canvas id="demoCanvas" class="plasius-demo__canvas" width="
|
|
1667
|
+
<canvas id="demoCanvas" class="plasius-demo__canvas" width="${DEFAULT_CANVAS_WIDTH}" height="${DEFAULT_CANVAS_HEIGHT}"></canvas>
|
|
575
1668
|
<div class="plasius-demo__toolbar">
|
|
576
|
-
<button id="pauseButton" type="button"
|
|
1669
|
+
<button id="pauseButton" type="button">${t(gpuSharedTranslationKeys.pause)}</button>
|
|
577
1670
|
<label class="plasius-toggle">
|
|
578
1671
|
<input id="stressToggle" type="checkbox" />
|
|
579
|
-
|
|
1672
|
+
${t(gpuSharedTranslationKeys.stressMode)}
|
|
580
1673
|
</label>
|
|
581
1674
|
<label class="plasius-toggle">
|
|
582
|
-
|
|
1675
|
+
${t(gpuSharedTranslationKeys.focus)}
|
|
583
1676
|
<select id="focusMode">
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
<option value="debug">debug</option>
|
|
1677
|
+
${showcaseFocusModes
|
|
1678
|
+
.map(
|
|
1679
|
+
(mode) =>
|
|
1680
|
+
`<option value="${mode}">${t(FOCUS_MODE_TRANSLATION_KEYS[mode])}</option>`
|
|
1681
|
+
)
|
|
1682
|
+
.join("")}
|
|
591
1683
|
</select>
|
|
592
1684
|
</label>
|
|
593
1685
|
</div>
|
|
594
1686
|
<div class="plasius-demo__legend">
|
|
595
|
-
<strong
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1687
|
+
<strong>${t(gpuSharedTranslationKeys.legendTitle)}</strong>
|
|
1688
|
+
${t(gpuSharedTranslationKeys.legendShipMetadata)}<br />
|
|
1689
|
+
${t(gpuSharedTranslationKeys.legendLighting)}<br />
|
|
1690
|
+
${t(gpuSharedTranslationKeys.legendCollisions)}
|
|
599
1691
|
</div>
|
|
600
1692
|
</section>
|
|
601
1693
|
<aside class="plasius-demo__sidebar">
|
|
602
1694
|
<section class="plasius-panel plasius-demo__card">
|
|
603
|
-
<h2
|
|
1695
|
+
<h2>${t(gpuSharedTranslationKeys.sceneState)}</h2>
|
|
604
1696
|
<ul id="sceneMetrics" class="plasius-demo__metrics"></ul>
|
|
605
1697
|
</section>
|
|
606
1698
|
<section class="plasius-panel plasius-demo__card">
|
|
607
|
-
<h2
|
|
1699
|
+
<h2>${t(gpuSharedTranslationKeys.qualityBudgets)}</h2>
|
|
608
1700
|
<ul id="qualityMetrics" class="plasius-demo__metrics"></ul>
|
|
609
1701
|
</section>
|
|
610
1702
|
<section class="plasius-panel plasius-demo__card">
|
|
611
|
-
<h2
|
|
1703
|
+
<h2>${t(gpuSharedTranslationKeys.debugTelemetry)}</h2>
|
|
612
1704
|
<ul id="debugMetrics" class="plasius-demo__metrics"></ul>
|
|
613
1705
|
</section>
|
|
614
1706
|
<section class="plasius-panel plasius-demo__card">
|
|
615
|
-
<h2
|
|
1707
|
+
<h2>${t(gpuSharedTranslationKeys.notes)}</h2>
|
|
616
1708
|
<ul id="sceneNotes" class="plasius-demo__metrics"></ul>
|
|
617
1709
|
</section>
|
|
618
1710
|
</aside>
|
|
@@ -638,6 +1730,12 @@ function buildDemoDom(root, options) {
|
|
|
638
1730
|
}
|
|
639
1731
|
|
|
640
1732
|
function buildSceneSnapshot(state, shipModel) {
|
|
1733
|
+
const shipPhysics = Object.freeze(
|
|
1734
|
+
Object.fromEntries(
|
|
1735
|
+
state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
|
|
1736
|
+
)
|
|
1737
|
+
);
|
|
1738
|
+
|
|
641
1739
|
return Object.freeze({
|
|
642
1740
|
focus: state.focus,
|
|
643
1741
|
frame: state.frame,
|
|
@@ -659,6 +1757,7 @@ function buildSceneSnapshot(state, shipModel) {
|
|
|
659
1757
|
state.ships.map((ship) =>
|
|
660
1758
|
Object.freeze({
|
|
661
1759
|
id: ship.id,
|
|
1760
|
+
modelKey: ship.modelKey ?? "brigantine",
|
|
662
1761
|
position: Object.freeze({ ...ship.position }),
|
|
663
1762
|
velocity: Object.freeze({ ...ship.velocity }),
|
|
664
1763
|
rotationY: ship.rotationY,
|
|
@@ -679,12 +1778,13 @@ function buildSceneSnapshot(state, shipModel) {
|
|
|
679
1778
|
)
|
|
680
1779
|
),
|
|
681
1780
|
shipPhysics: shipModel?.physics ?? null,
|
|
1781
|
+
shipModels: shipPhysics,
|
|
682
1782
|
physics: Object.freeze({
|
|
683
1783
|
profile: state.physics.profile,
|
|
684
1784
|
plan: state.physics.plan,
|
|
685
1785
|
manifest: state.physics.manifest,
|
|
686
1786
|
snapshot: state.physics.snapshot,
|
|
687
|
-
shipPhysics
|
|
1787
|
+
shipPhysics,
|
|
688
1788
|
}),
|
|
689
1789
|
});
|
|
690
1790
|
}
|
|
@@ -727,11 +1827,88 @@ function readVisualNumber(value, fallback) {
|
|
|
727
1827
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
728
1828
|
}
|
|
729
1829
|
|
|
730
|
-
function
|
|
731
|
-
|
|
1830
|
+
function readPositiveNumber(value, fallback) {
|
|
1831
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
1832
|
+
? value
|
|
1833
|
+
: fallback;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function isTruthyCaptureValue(value) {
|
|
1837
|
+
return value === "1" || value === "true" || value === "scene" || value === "video";
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function resolveCaptureSettings(options) {
|
|
1841
|
+
const explicitCaptureMode =
|
|
1842
|
+
typeof options.captureMode === "boolean" ? options.captureMode : undefined;
|
|
1843
|
+
let captureMode = explicitCaptureMode ?? false;
|
|
1844
|
+
let renderScale = readPositiveNumber(options.renderScale, undefined);
|
|
1845
|
+
|
|
1846
|
+
try {
|
|
1847
|
+
const params = new URLSearchParams(window.location.search);
|
|
1848
|
+
if (explicitCaptureMode === undefined) {
|
|
1849
|
+
captureMode =
|
|
1850
|
+
isTruthyCaptureValue(params.get("capture")) ||
|
|
1851
|
+
params.get("presentation") === "capture";
|
|
1852
|
+
}
|
|
1853
|
+
renderScale = readPositiveNumber(Number(params.get("renderScale")), renderScale);
|
|
1854
|
+
} catch {
|
|
1855
|
+
// Query-string capture controls are optional and only available in browsers.
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
return {
|
|
1859
|
+
captureMode,
|
|
1860
|
+
renderScale,
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function getCanvasDisplaySize(canvas) {
|
|
1865
|
+
const rect =
|
|
1866
|
+
typeof canvas.getBoundingClientRect === "function"
|
|
1867
|
+
? canvas.getBoundingClientRect()
|
|
1868
|
+
: null;
|
|
1869
|
+
const width = Math.round(
|
|
1870
|
+
readPositiveNumber(rect?.width, readPositiveNumber(canvas.clientWidth, canvas.width))
|
|
1871
|
+
);
|
|
1872
|
+
const height = Math.round(
|
|
1873
|
+
readPositiveNumber(rect?.height, readPositiveNumber(canvas.clientHeight, canvas.height))
|
|
1874
|
+
);
|
|
1875
|
+
|
|
1876
|
+
return {
|
|
1877
|
+
width: Math.max(1, width || DEFAULT_CANVAS_WIDTH),
|
|
1878
|
+
height: Math.max(1, height || DEFAULT_CANVAS_HEIGHT),
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function resizeCanvasToDisplaySize(canvas, state) {
|
|
1883
|
+
const { width, height } = getCanvasDisplaySize(canvas);
|
|
1884
|
+
const deviceScale = readPositiveNumber(globalThis.devicePixelRatio, 1);
|
|
1885
|
+
const requestedScale = readPositiveNumber(state.renderScale, deviceScale);
|
|
1886
|
+
const maxScale = state.captureMode ? 2 : 1.5;
|
|
1887
|
+
let scale = clamp(requestedScale, 1, maxScale);
|
|
1888
|
+
const pixelBudget = state.captureMode
|
|
1889
|
+
? CAPTURE_CANVAS_PIXEL_BUDGET
|
|
1890
|
+
: DEFAULT_CANVAS_WIDTH * DEFAULT_CANVAS_HEIGHT * 1.5;
|
|
1891
|
+
const projectedPixels = width * height * scale * scale;
|
|
1892
|
+
|
|
1893
|
+
if (projectedPixels > pixelBudget) {
|
|
1894
|
+
scale = Math.sqrt(pixelBudget / Math.max(1, width * height));
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
const targetWidth = Math.max(1, Math.round(width * scale));
|
|
1898
|
+
const targetHeight = Math.max(1, Math.round(height * scale));
|
|
1899
|
+
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
|
1900
|
+
canvas.width = targetWidth;
|
|
1901
|
+
canvas.height = targetHeight;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
state.renderScale = scale;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
function resolveClothPresentation(state, meshDetail, clothFeatures) {
|
|
1908
|
+
const clothPlan = clothFeatures.createPlan({
|
|
732
1909
|
garmentId: "shore-flag",
|
|
733
|
-
kind: state.focus === "cloth" ? "flag" :
|
|
734
|
-
profile: state.focus === "cloth" ? "cinematic" :
|
|
1910
|
+
kind: state.focus === "cloth" ? "flag" : clothFeatures.garmentKinds[0],
|
|
1911
|
+
profile: state.focus === "cloth" ? "cinematic" : clothFeatures.profileNames[0],
|
|
735
1912
|
supportsRayTracing: true,
|
|
736
1913
|
nearFieldMaxMeters: 18,
|
|
737
1914
|
midFieldMaxMeters: 55,
|
|
@@ -749,7 +1926,7 @@ function resolveClothPresentation(state, meshDetail) {
|
|
|
749
1926
|
)
|
|
750
1927
|
);
|
|
751
1928
|
const cameraDistance = lengthVec3(subVec3(state.camera.target, fallbackEye));
|
|
752
|
-
const band =
|
|
1929
|
+
const band = clothFeatures.selectBand(cameraDistance, clothPlan.thresholds);
|
|
753
1930
|
const representation =
|
|
754
1931
|
clothPlan.representations.find((entry) => entry.band === band) ?? clothPlan.representations[0];
|
|
755
1932
|
return {
|
|
@@ -1111,8 +2288,9 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
|
|
|
1111
2288
|
};
|
|
1112
2289
|
}
|
|
1113
2290
|
|
|
1114
|
-
function buildClothSurface(model, state, meshDetail, visuals) {
|
|
1115
|
-
const
|
|
2291
|
+
function buildClothSurface(model, state, meshDetail, visuals, clothFeatures) {
|
|
2292
|
+
const resolvedClothFeatures = normalizeClothFeatureAdapters(clothFeatures);
|
|
2293
|
+
const clothPresentation = resolveClothPresentation(state, meshDetail, resolvedClothFeatures);
|
|
1116
2294
|
const clothState = ensureShowcaseClothState(state, meshDetail, clothPresentation);
|
|
1117
2295
|
|
|
1118
2296
|
return {
|
|
@@ -1281,11 +2459,12 @@ function buildWaterMotionEffects(state) {
|
|
|
1281
2459
|
});
|
|
1282
2460
|
}
|
|
1283
2461
|
|
|
1284
|
-
function buildWaterBands(state, fluidDetail, visuals) {
|
|
1285
|
-
const
|
|
2462
|
+
function buildWaterBands(state, fluidDetail, visuals, fluidFeatures) {
|
|
2463
|
+
const resolvedFluidFeatures = normalizeFluidFeatureAdapters(fluidFeatures);
|
|
2464
|
+
const fluidPlan = resolvedFluidFeatures.createPlan({
|
|
1286
2465
|
fluidBodyId: "harbor",
|
|
1287
|
-
kind: state.focus === "fluid" ? "ocean" :
|
|
1288
|
-
profile: state.focus === "fluid" ? "cinematic" :
|
|
2466
|
+
kind: state.focus === "fluid" ? "ocean" : resolvedFluidFeatures.bodyKinds[0],
|
|
2467
|
+
profile: state.focus === "fluid" ? "cinematic" : resolvedFluidFeatures.profileNames[0],
|
|
1289
2468
|
supportsRayTracing: true,
|
|
1290
2469
|
nearFieldMaxMeters: 40,
|
|
1291
2470
|
midFieldMaxMeters: 150,
|
|
@@ -1304,7 +2483,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
|
|
|
1304
2483
|
const representation =
|
|
1305
2484
|
fluidPlan.representations.find((entry) => entry.band === bandSpec.band) ??
|
|
1306
2485
|
fluidPlan.representations[0];
|
|
1307
|
-
const continuity =
|
|
2486
|
+
const continuity = resolvedFluidFeatures.createContinuityEnvelope({ fluidBodyId: "harbor" });
|
|
1308
2487
|
const bandContinuity = resolveFluidBandContinuity(continuity, bandSpec.band);
|
|
1309
2488
|
const bandResolution =
|
|
1310
2489
|
bandSpec.band === "near"
|
|
@@ -1374,15 +2553,34 @@ function buildWaterBands(state, fluidDetail, visuals) {
|
|
|
1374
2553
|
return { fluidPlan, bandMeshes };
|
|
1375
2554
|
}
|
|
1376
2555
|
|
|
1377
|
-
function createSceneState(options) {
|
|
1378
|
-
const
|
|
1379
|
-
const
|
|
2556
|
+
function createSceneState(options, featureAdapters) {
|
|
2557
|
+
const translate = options.translate;
|
|
2558
|
+
const { governor, fluidDetail, clothDetail, lightingDetail } = createPerformanceGovernor(
|
|
2559
|
+
featureAdapters.performance
|
|
2560
|
+
);
|
|
2561
|
+
const physicsProfile = featureAdapters.physics.defaultProfile;
|
|
2562
|
+
const createPhysicsSimulationPlan = assertRequiredFunction(
|
|
2563
|
+
featureAdapters.physics,
|
|
2564
|
+
"physics",
|
|
2565
|
+
"createSimulationPlan"
|
|
2566
|
+
);
|
|
2567
|
+
const getPhysicsWorkerManifest = assertRequiredFunction(
|
|
2568
|
+
featureAdapters.physics,
|
|
2569
|
+
"physics",
|
|
2570
|
+
"getManifest"
|
|
2571
|
+
);
|
|
2572
|
+
const createGpuDebugSession = assertRequiredFunction(
|
|
2573
|
+
featureAdapters.debug,
|
|
2574
|
+
"debug",
|
|
2575
|
+
"createSession"
|
|
2576
|
+
);
|
|
2577
|
+
|
|
1380
2578
|
const physicsPlan = createPhysicsSimulationPlan(physicsProfile);
|
|
1381
2579
|
const physicsManifest = getPhysicsWorkerManifest(physicsProfile);
|
|
1382
2580
|
const debugSession = createGpuDebugSession({
|
|
1383
2581
|
enabled: true,
|
|
1384
2582
|
adapter: {
|
|
1385
|
-
label:
|
|
2583
|
+
label: translate(gpuSharedTranslationKeys.debugAdapterShowcase),
|
|
1386
2584
|
memoryCapacityHintBytes: 6 * 1024 * 1024 * 1024,
|
|
1387
2585
|
coreCountHint: 12,
|
|
1388
2586
|
},
|
|
@@ -1392,23 +2590,27 @@ function createSceneState(options) {
|
|
|
1392
2590
|
owner: "renderer",
|
|
1393
2591
|
category: "texture",
|
|
1394
2592
|
sizeBytes: 1280 * 720 * 4,
|
|
1395
|
-
label:
|
|
2593
|
+
label: translate(gpuSharedTranslationKeys.debugMainColorBuffer),
|
|
1396
2594
|
});
|
|
1397
2595
|
debugSession.trackAllocation({
|
|
1398
2596
|
id: "showcase.shadow-impression",
|
|
1399
2597
|
owner: "lighting",
|
|
1400
2598
|
category: "texture",
|
|
1401
2599
|
sizeBytes: 12 * 1024 * 1024,
|
|
1402
|
-
label:
|
|
2600
|
+
label: translate(gpuSharedTranslationKeys.debugShadowImpressionAtlas),
|
|
1403
2601
|
});
|
|
1404
2602
|
|
|
1405
2603
|
return {
|
|
2604
|
+
translate,
|
|
1406
2605
|
focus: options.focus,
|
|
1407
2606
|
governor,
|
|
1408
2607
|
fluidDetail,
|
|
1409
2608
|
clothDetail,
|
|
1410
2609
|
lightingDetail,
|
|
1411
2610
|
debugSession,
|
|
2611
|
+
showcaseRealisticModelsEnabled: options.realisticModelsEnabled !== false,
|
|
2612
|
+
captureMode: options.captureMode === true,
|
|
2613
|
+
renderScale: readPositiveNumber(options.renderScale, undefined),
|
|
1412
2614
|
packageState: undefined,
|
|
1413
2615
|
demoDescription: null,
|
|
1414
2616
|
demoVisuals: null,
|
|
@@ -1423,6 +2625,7 @@ function createSceneState(options) {
|
|
|
1423
2625
|
ships: [
|
|
1424
2626
|
{
|
|
1425
2627
|
id: "northwind",
|
|
2628
|
+
modelKey: "brigantine",
|
|
1426
2629
|
position: vec3(-5.2, 0, 7.2),
|
|
1427
2630
|
velocity: vec3(2.35, 0, -1.08),
|
|
1428
2631
|
rotationY: 0.58,
|
|
@@ -1433,17 +2636,18 @@ function createSceneState(options) {
|
|
|
1433
2636
|
throttleResponse: 0.46,
|
|
1434
2637
|
rudderResponse: 0.54,
|
|
1435
2638
|
wanderPhase: 0.35,
|
|
1436
|
-
lanterns:
|
|
2639
|
+
lanterns: CUTTER_LANTERNS,
|
|
1437
2640
|
lanternStrength: 1.06,
|
|
1438
2641
|
collisionRadiusScale: 1.04,
|
|
1439
2642
|
},
|
|
1440
2643
|
{
|
|
1441
2644
|
id: "tidecaller",
|
|
2645
|
+
modelKey: "cutter",
|
|
1442
2646
|
position: vec3(4.8, 0, 4.4),
|
|
1443
2647
|
velocity: vec3(-2.15, 0, 1.74),
|
|
1444
2648
|
rotationY: -2.48,
|
|
1445
2649
|
angularVelocity: -0.2,
|
|
1446
|
-
tint: { r: 0.
|
|
2650
|
+
tint: { r: 0.58, g: 0.24, b: 0.16 },
|
|
1447
2651
|
massScale: 0.84,
|
|
1448
2652
|
cruiseSpeed: 2.68,
|
|
1449
2653
|
throttleResponse: 0.7,
|
|
@@ -1467,6 +2671,7 @@ function createSceneState(options) {
|
|
|
1467
2671
|
manifest: physicsManifest,
|
|
1468
2672
|
snapshot: null,
|
|
1469
2673
|
},
|
|
2674
|
+
assetCatalog: null,
|
|
1470
2675
|
shipModel: null,
|
|
1471
2676
|
};
|
|
1472
2677
|
}
|
|
@@ -1553,10 +2758,51 @@ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, s
|
|
|
1553
2758
|
}
|
|
1554
2759
|
}
|
|
1555
2760
|
|
|
1556
|
-
function
|
|
2761
|
+
function resolveLocalLightContribution(triangle, lightSources) {
|
|
2762
|
+
const contribution = { r: 0, g: 0, b: 0 };
|
|
2763
|
+
if (!Array.isArray(lightSources) || triangle.surfaceType === "water") {
|
|
2764
|
+
return contribution;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
const normal = normalizeVec3(triangle.normal);
|
|
2768
|
+
for (const source of lightSources.slice(0, 8)) {
|
|
2769
|
+
const delta = subVec3(source.point, triangle.worldCenter);
|
|
2770
|
+
const distance = lengthVec3(delta);
|
|
2771
|
+
const attenuation =
|
|
2772
|
+
(source.glowScale ?? 1) / Math.max(1, 0.68 + distance * distance * 0.2);
|
|
2773
|
+
if (attenuation < 0.012) {
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
const lightDir = normalizeVec3(delta);
|
|
2778
|
+
const facing = clamp(dotVec3(normal, lightDir), 0, 1);
|
|
2779
|
+
const response = attenuation * (0.18 + facing * 0.82);
|
|
2780
|
+
const glowColor = source.glowColor ?? source.coreColor ?? { r: 1, g: 0.72, b: 0.4 };
|
|
2781
|
+
contribution.r += glowColor.r * response * 0.32;
|
|
2782
|
+
contribution.g += glowColor.g * response * 0.26;
|
|
2783
|
+
contribution.b += glowColor.b * response * 0.18;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
return contribution;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
function drawTriangles(
|
|
2790
|
+
ctx,
|
|
2791
|
+
triangles,
|
|
2792
|
+
lightDir,
|
|
2793
|
+
reflectionStrength,
|
|
2794
|
+
camera,
|
|
2795
|
+
shadowStrength,
|
|
2796
|
+
localLights = []
|
|
2797
|
+
) {
|
|
1557
2798
|
triangles.sort((left, right) => right.depth - left.depth);
|
|
1558
2799
|
for (const triangle of triangles) {
|
|
1559
2800
|
const surfaceNormal = normalizeVec3(triangle.normal);
|
|
2801
|
+
const material = triangle.material ?? {
|
|
2802
|
+
roughness: 0.88,
|
|
2803
|
+
metallic: 0.08,
|
|
2804
|
+
emissive: { r: 0, g: 0, b: 0 },
|
|
2805
|
+
};
|
|
1560
2806
|
const shaded = shadeColor(
|
|
1561
2807
|
triangle.baseColor,
|
|
1562
2808
|
surfaceNormal,
|
|
@@ -1564,19 +2810,42 @@ function drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, sha
|
|
|
1564
2810
|
clamp((triangle.worldCenter.y + 3) / 10, 0, 1),
|
|
1565
2811
|
triangle.accent
|
|
1566
2812
|
);
|
|
1567
|
-
const reflection =
|
|
2813
|
+
const reflection = reflectionStrength * (triangle.reflection ?? 0);
|
|
1568
2814
|
const viewDir = normalizeVec3(subVec3(camera.eye, triangle.worldCenter));
|
|
1569
2815
|
const reflectedLight = reflectVec3(scaleVec3(lightDir, -1), surfaceNormal);
|
|
1570
|
-
const gloss =
|
|
1571
|
-
const
|
|
1572
|
-
const
|
|
1573
|
-
|
|
2816
|
+
const gloss = mix(0.78, 0.14, clamp(material.roughness ?? 0.88, 0, 1)) + (material.metallic ?? 0) * 0.18;
|
|
2817
|
+
const specularPower = mix(26, 7, clamp(material.roughness ?? 0.88, 0, 1));
|
|
2818
|
+
const specular =
|
|
2819
|
+
Math.pow(clamp(dotVec3(reflectedLight, viewDir), 0, 1), specularPower) * gloss;
|
|
2820
|
+
const emissive = material.emissive ?? { r: 0, g: 0, b: 0 };
|
|
2821
|
+
const localLight = resolveLocalLightContribution(triangle, localLights);
|
|
2822
|
+
const occlusion = triangle.surfaceType === "water" ? shadowStrength * 0.018 : shadowStrength * 0.04;
|
|
2823
|
+
const detailed = applyMaterialDetail(
|
|
1574
2824
|
{
|
|
1575
|
-
r: clamp(
|
|
1576
|
-
|
|
1577
|
-
|
|
2825
|
+
r: clamp(
|
|
2826
|
+
shaded.r + reflection * 0.08 + specular * 0.16 + emissive.r * 0.42 + localLight.r - occlusion,
|
|
2827
|
+
0,
|
|
2828
|
+
1
|
|
2829
|
+
),
|
|
2830
|
+
g: clamp(
|
|
2831
|
+
shaded.g + reflection * 0.08 + specular * 0.16 + emissive.g * 0.42 + localLight.g - occlusion,
|
|
2832
|
+
0,
|
|
2833
|
+
1
|
|
2834
|
+
),
|
|
2835
|
+
b: clamp(
|
|
2836
|
+
shaded.b + reflection * 0.16 + specular * 0.22 + emissive.b * 0.46 + localLight.b - occlusion * 0.5,
|
|
2837
|
+
0,
|
|
2838
|
+
1
|
|
2839
|
+
),
|
|
1578
2840
|
},
|
|
1579
|
-
|
|
2841
|
+
material,
|
|
2842
|
+
triangle.worldCenter,
|
|
2843
|
+
surfaceNormal,
|
|
2844
|
+
triangle.surfaceType
|
|
2845
|
+
);
|
|
2846
|
+
const fill = colorToRgba(
|
|
2847
|
+
detailed,
|
|
2848
|
+
triangle.baseColor.a ?? 0.98
|
|
1580
2849
|
);
|
|
1581
2850
|
ctx.fillStyle = fill;
|
|
1582
2851
|
ctx.beginPath();
|
|
@@ -1615,78 +2884,111 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
|
|
|
1615
2884
|
ctx.restore();
|
|
1616
2885
|
}
|
|
1617
2886
|
|
|
1618
|
-
function pushHarborGeometry(camera, viewport, triangles,
|
|
1619
|
-
|
|
1620
|
-
{
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2887
|
+
function pushHarborGeometry(camera, viewport, triangles, state) {
|
|
2888
|
+
if (!state.showcaseRealisticModelsEnabled) {
|
|
2889
|
+
for (const object of LEGACY_HARBOR_LAYOUT) {
|
|
2890
|
+
buildTrianglesFromMesh(
|
|
2891
|
+
{ positions: [object], indices: [0], normals: null, colors: null, material: createLegacyMeshPrimitive({})?.material, bounds: null, name: "legacy-structure" },
|
|
2892
|
+
{
|
|
2893
|
+
position: object.position,
|
|
2894
|
+
rotationY: object.rotationY,
|
|
2895
|
+
scale: object.scale,
|
|
2896
|
+
},
|
|
2897
|
+
object.color,
|
|
2898
|
+
camera,
|
|
2899
|
+
viewport,
|
|
2900
|
+
triangles,
|
|
2901
|
+
{
|
|
2902
|
+
accent: object.accent,
|
|
2903
|
+
reflection: 0,
|
|
2904
|
+
surfaceType: "structure",
|
|
2905
|
+
}
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
for (const placement of SHOWCASE_ENVIRONMENT_LAYOUT) {
|
|
2913
|
+
const mesh = state.assetCatalog?.environment?.[placement.assetKey] ?? null;
|
|
2914
|
+
if (!mesh) {
|
|
2915
|
+
continue;
|
|
2916
|
+
}
|
|
1642
2917
|
|
|
1643
|
-
for (const object of harborObjects) {
|
|
1644
2918
|
buildTrianglesFromMesh(
|
|
1645
|
-
|
|
2919
|
+
mesh,
|
|
1646
2920
|
{
|
|
1647
|
-
position:
|
|
1648
|
-
rotationY:
|
|
1649
|
-
scale:
|
|
2921
|
+
position: vec3(placement.position.x, placement.position.y, placement.position.z),
|
|
2922
|
+
rotationY: placement.rotationY,
|
|
2923
|
+
scale: placement.scale,
|
|
1650
2924
|
},
|
|
1651
|
-
|
|
2925
|
+
null,
|
|
1652
2926
|
camera,
|
|
1653
2927
|
viewport,
|
|
1654
2928
|
triangles,
|
|
1655
|
-
|
|
2929
|
+
{
|
|
2930
|
+
accent: placement.accent,
|
|
2931
|
+
reflection: 0,
|
|
2932
|
+
surfaceType: "structure",
|
|
2933
|
+
}
|
|
1656
2934
|
);
|
|
1657
2935
|
}
|
|
1658
2936
|
}
|
|
1659
2937
|
|
|
1660
2938
|
function renderShipRigging(ctx, ship, camera, viewport) {
|
|
1661
2939
|
const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
2940
|
+
const layout =
|
|
2941
|
+
ship.modelKey === "cutter"
|
|
2942
|
+
? {
|
|
2943
|
+
lineColor: "rgba(85, 89, 97, 0.92)",
|
|
2944
|
+
sailColor: "rgba(218, 232, 244, 0.28)",
|
|
2945
|
+
points: [
|
|
2946
|
+
vec3(0, 0.88, -0.32),
|
|
2947
|
+
vec3(0, 2.4, -0.28),
|
|
2948
|
+
vec3(0.1, 1.92, -0.3),
|
|
2949
|
+
vec3(1.18, 1.72, -0.18),
|
|
2950
|
+
vec3(1.04, 1.08, -0.12),
|
|
2951
|
+
],
|
|
2952
|
+
mastPairs: [[0, 1], [2, 3]],
|
|
2953
|
+
sailTriangle: [2, 3, 4],
|
|
2954
|
+
}
|
|
2955
|
+
: {
|
|
2956
|
+
lineColor: "rgba(73, 54, 45, 0.94)",
|
|
2957
|
+
sailColor: "rgba(238, 232, 214, 0.88)",
|
|
2958
|
+
points: [
|
|
2959
|
+
vec3(0, 0.38, -0.4),
|
|
2960
|
+
vec3(0, 3.8, -0.2),
|
|
2961
|
+
vec3(-0.25, 0.32, -1.9),
|
|
2962
|
+
vec3(-0.15, 2.7, -1.75),
|
|
2963
|
+
vec3(0.08, 3.2, -0.2),
|
|
2964
|
+
vec3(0.12, 1.2, -0.5),
|
|
2965
|
+
vec3(2.25, 2.25, 0.15),
|
|
2966
|
+
],
|
|
2967
|
+
mastPairs: [[0, 1], [2, 3]],
|
|
2968
|
+
sailTriangle: [4, 5, 6],
|
|
2969
|
+
};
|
|
2970
|
+
const projected = layout.points
|
|
2971
|
+
.map((point) => transformPoint(point, transform))
|
|
2972
|
+
.map((point) => projectPoint(point, camera, viewport));
|
|
1672
2973
|
if (projected.some((value) => value === null)) {
|
|
1673
2974
|
return;
|
|
1674
2975
|
}
|
|
1675
2976
|
|
|
1676
|
-
ctx.strokeStyle =
|
|
1677
|
-
ctx.lineWidth = 3.5;
|
|
2977
|
+
ctx.strokeStyle = layout.lineColor;
|
|
2978
|
+
ctx.lineWidth = ship.modelKey === "cutter" ? 2.2 : 3.5;
|
|
1678
2979
|
ctx.beginPath();
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2980
|
+
for (const [from, to] of layout.mastPairs) {
|
|
2981
|
+
ctx.moveTo(projected[from].x, projected[from].y);
|
|
2982
|
+
ctx.lineTo(projected[to].x, projected[to].y);
|
|
2983
|
+
}
|
|
1683
2984
|
ctx.stroke();
|
|
1684
2985
|
|
|
1685
|
-
|
|
2986
|
+
const [a, b, c] = layout.sailTriangle;
|
|
2987
|
+
ctx.fillStyle = layout.sailColor;
|
|
1686
2988
|
ctx.beginPath();
|
|
1687
|
-
ctx.moveTo(projected[
|
|
1688
|
-
ctx.lineTo(projected[
|
|
1689
|
-
ctx.lineTo(projected[
|
|
2989
|
+
ctx.moveTo(projected[a].x, projected[a].y);
|
|
2990
|
+
ctx.lineTo(projected[b].x, projected[b].y);
|
|
2991
|
+
ctx.lineTo(projected[c].x, projected[c].y);
|
|
1690
2992
|
ctx.closePath();
|
|
1691
2993
|
ctx.fill();
|
|
1692
2994
|
}
|
|
@@ -1941,10 +3243,10 @@ function resolveBoundaryCollision(ship, state, shipModel) {
|
|
|
1941
3243
|
}
|
|
1942
3244
|
}
|
|
1943
3245
|
|
|
1944
|
-
function resolveShipCollision(state, a, b,
|
|
3246
|
+
function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
|
|
1945
3247
|
const delta = subVec3(b.position, a.position);
|
|
1946
|
-
const radiusA = getShipCollisionRadius(a,
|
|
1947
|
-
const radiusB = getShipCollisionRadius(b,
|
|
3248
|
+
const radiusA = getShipCollisionRadius(a, shipModelA);
|
|
3249
|
+
const radiusB = getShipCollisionRadius(b, shipModelB);
|
|
1948
3250
|
const distance = Math.hypot(delta.x, delta.z);
|
|
1949
3251
|
const minDistance = radiusA + radiusB;
|
|
1950
3252
|
if (distance >= minDistance) {
|
|
@@ -1957,8 +3259,8 @@ function resolveShipCollision(state, a, b, shipModel) {
|
|
|
1957
3259
|
: normalizeVec3(vec3(Math.cos(state.time * 5.2), 0, Math.sin(state.time * 4.8)));
|
|
1958
3260
|
const tangent = vec3(-normal.z, 0, normal.x);
|
|
1959
3261
|
const penetration = minDistance - distance;
|
|
1960
|
-
const invMassA = getShipInverseMass(a,
|
|
1961
|
-
const invMassB = getShipInverseMass(b,
|
|
3262
|
+
const invMassA = getShipInverseMass(a, shipModelA);
|
|
3263
|
+
const invMassB = getShipInverseMass(b, shipModelB);
|
|
1962
3264
|
const invMassSum = invMassA + invMassB;
|
|
1963
3265
|
const correction = scaleVec3(normal, (penetration / Math.max(0.0001, invMassSum)) * 0.72);
|
|
1964
3266
|
a.position = subVec3(a.position, scaleVec3(correction, invMassA));
|
|
@@ -1966,7 +3268,11 @@ function resolveShipCollision(state, a, b, shipModel) {
|
|
|
1966
3268
|
|
|
1967
3269
|
const relativeVelocity = subVec3(b.velocity, a.velocity);
|
|
1968
3270
|
const velocityAlongNormal = dotVec3(relativeVelocity, normal);
|
|
1969
|
-
const restitution =
|
|
3271
|
+
const restitution =
|
|
3272
|
+
((readPhysicsNumber(shipModelA.physics, "restitution", 0.22) +
|
|
3273
|
+
readPhysicsNumber(shipModelB.physics, "restitution", 0.22)) /
|
|
3274
|
+
2) *
|
|
3275
|
+
0.88;
|
|
1970
3276
|
if (velocityAlongNormal < 0) {
|
|
1971
3277
|
const impulseMagnitude =
|
|
1972
3278
|
(-(1 + restitution) * velocityAlongNormal) / Math.max(0.0001, invMassSum);
|
|
@@ -1985,10 +3291,10 @@ function resolveShipCollision(state, a, b, shipModel) {
|
|
|
1985
3291
|
b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
|
|
1986
3292
|
|
|
1987
3293
|
a.angularVelocity -=
|
|
1988
|
-
tangentSpeed * radiusA * getShipInverseInertia(a,
|
|
3294
|
+
tangentSpeed * radiusA * getShipInverseInertia(a, shipModelA) * 0.2 +
|
|
1989
3295
|
impulseMagnitude * 0.00024;
|
|
1990
3296
|
b.angularVelocity +=
|
|
1991
|
-
tangentSpeed * radiusB * getShipInverseInertia(b,
|
|
3297
|
+
tangentSpeed * radiusB * getShipInverseInertia(b, shipModelB) * 0.2 +
|
|
1992
3298
|
impulseMagnitude * 0.00024;
|
|
1993
3299
|
|
|
1994
3300
|
const impactSpeed = Math.abs(velocityAlongNormal);
|
|
@@ -2028,14 +3334,19 @@ function updateShips(state, dt, shipModel) {
|
|
|
2028
3334
|
state.contactCount = 0;
|
|
2029
3335
|
|
|
2030
3336
|
for (const ship of state.ships) {
|
|
2031
|
-
|
|
2032
|
-
|
|
3337
|
+
const activeShipModel = resolveShipModel(state, ship, shipModel);
|
|
3338
|
+
updateShipMotion(state, ship, dt, activeShipModel);
|
|
3339
|
+
resolveBoundaryCollision(ship, state, activeShipModel);
|
|
2033
3340
|
}
|
|
2034
3341
|
|
|
2035
3342
|
for (let index = 0; index < state.ships.length; index += 1) {
|
|
2036
3343
|
for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
|
|
3344
|
+
const shipA = state.ships[index];
|
|
3345
|
+
const shipB = state.ships[otherIndex];
|
|
3346
|
+
const shipModelA = resolveShipModel(state, shipA, shipModel);
|
|
3347
|
+
const shipModelB = resolveShipModel(state, shipB, shipModel);
|
|
2037
3348
|
collided =
|
|
2038
|
-
resolveShipCollision(state,
|
|
3349
|
+
resolveShipCollision(state, shipA, shipB, shipModelA, shipModelB) ||
|
|
2039
3350
|
collided;
|
|
2040
3351
|
}
|
|
2041
3352
|
}
|
|
@@ -2327,6 +3638,108 @@ function renderWaterLightReflection(ctx, source, state, camera, viewport) {
|
|
|
2327
3638
|
ctx.restore();
|
|
2328
3639
|
}
|
|
2329
3640
|
|
|
3641
|
+
function renderLighthouseBeam(ctx, state, camera, viewport, visuals) {
|
|
3642
|
+
const lighthousePlacement = SHOWCASE_ENVIRONMENT_LAYOUT.find(
|
|
3643
|
+
(placement) => placement.assetKey === "lighthouse"
|
|
3644
|
+
);
|
|
3645
|
+
if (!lighthousePlacement || !state.showcaseRealisticModelsEnabled) {
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
const source = transformPoint(
|
|
3650
|
+
vec3(0, 11.34, 0),
|
|
3651
|
+
{
|
|
3652
|
+
position: vec3(
|
|
3653
|
+
lighthousePlacement.position.x,
|
|
3654
|
+
lighthousePlacement.position.y,
|
|
3655
|
+
lighthousePlacement.position.z
|
|
3656
|
+
),
|
|
3657
|
+
rotationY: lighthousePlacement.rotationY,
|
|
3658
|
+
scale: lighthousePlacement.scale,
|
|
3659
|
+
}
|
|
3660
|
+
);
|
|
3661
|
+
const sweep = state.time * 0.22 + 0.8;
|
|
3662
|
+
const direction = normalizeVec3(vec3(Math.sin(sweep), -0.07, Math.cos(sweep)));
|
|
3663
|
+
const spread = perpendicularOnWater(direction);
|
|
3664
|
+
const farCenter = addVec3(source, scaleVec3(direction, 34));
|
|
3665
|
+
const left = addVec3(farCenter, scaleVec3(spread, 7.4));
|
|
3666
|
+
const right = addVec3(farCenter, scaleVec3(spread, -7.4));
|
|
3667
|
+
const projectedSource = projectPoint(source, camera, viewport);
|
|
3668
|
+
const projectedLeft = projectPoint(left, camera, viewport);
|
|
3669
|
+
const projectedRight = projectPoint(right, camera, viewport);
|
|
3670
|
+
if (!projectedSource || !projectedLeft || !projectedRight) {
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
const pulse = 0.72 + Math.sin(state.time * 1.7) * 0.08;
|
|
3675
|
+
ctx.save();
|
|
3676
|
+
ctx.globalCompositeOperation = "screen";
|
|
3677
|
+
ctx.fillStyle = colorToRgba(visuals.torchCore, 0.055 * pulse);
|
|
3678
|
+
ctx.beginPath();
|
|
3679
|
+
ctx.moveTo(projectedSource.x, projectedSource.y);
|
|
3680
|
+
ctx.lineTo(projectedLeft.x, projectedLeft.y);
|
|
3681
|
+
ctx.lineTo(projectedRight.x, projectedRight.y);
|
|
3682
|
+
ctx.closePath();
|
|
3683
|
+
ctx.fill();
|
|
3684
|
+
|
|
3685
|
+
const beamLength = Math.hypot(
|
|
3686
|
+
projectedLeft.x - projectedSource.x,
|
|
3687
|
+
projectedLeft.y - projectedSource.y
|
|
3688
|
+
);
|
|
3689
|
+
const core = ctx.createRadialGradient(
|
|
3690
|
+
projectedSource.x,
|
|
3691
|
+
projectedSource.y,
|
|
3692
|
+
2,
|
|
3693
|
+
projectedSource.x,
|
|
3694
|
+
projectedSource.y,
|
|
3695
|
+
clamp(beamLength * 0.22, 18, 80)
|
|
3696
|
+
);
|
|
3697
|
+
core.addColorStop(0, colorToRgba(visuals.torchCore, 0.58));
|
|
3698
|
+
core.addColorStop(0.5, colorToRgba(visuals.torchGlow, 0.18));
|
|
3699
|
+
core.addColorStop(1, colorToRgba(visuals.torchGlow, 0));
|
|
3700
|
+
ctx.fillStyle = core;
|
|
3701
|
+
ctx.beginPath();
|
|
3702
|
+
ctx.arc(projectedSource.x, projectedSource.y, clamp(beamLength * 0.18, 14, 64), 0, Math.PI * 2);
|
|
3703
|
+
ctx.fill();
|
|
3704
|
+
ctx.restore();
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
function renderAtmosphericGrade(ctx, canvas, state, visuals) {
|
|
3708
|
+
const vignette = ctx.createRadialGradient(
|
|
3709
|
+
canvas.width * 0.5,
|
|
3710
|
+
canvas.height * 0.48,
|
|
3711
|
+
canvas.width * 0.2,
|
|
3712
|
+
canvas.width * 0.5,
|
|
3713
|
+
canvas.height * 0.5,
|
|
3714
|
+
canvas.width * 0.72
|
|
3715
|
+
);
|
|
3716
|
+
vignette.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
3717
|
+
vignette.addColorStop(0.68, "rgba(0, 0, 0, 0.08)");
|
|
3718
|
+
vignette.addColorStop(1, "rgba(0, 0, 0, 0.32)");
|
|
3719
|
+
ctx.fillStyle = vignette;
|
|
3720
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
3721
|
+
|
|
3722
|
+
const seaHaze = ctx.createLinearGradient(0, canvas.height * 0.34, 0, canvas.height);
|
|
3723
|
+
seaHaze.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
3724
|
+
seaHaze.addColorStop(0.5, visuals.ambientMist);
|
|
3725
|
+
seaHaze.addColorStop(1, "rgba(3, 8, 16, 0.18)");
|
|
3726
|
+
ctx.fillStyle = seaHaze;
|
|
3727
|
+
ctx.fillRect(0, canvas.height * 0.34, canvas.width, canvas.height * 0.66);
|
|
3728
|
+
|
|
3729
|
+
if (state.captureMode) {
|
|
3730
|
+
ctx.save();
|
|
3731
|
+
ctx.globalCompositeOperation = "screen";
|
|
3732
|
+
for (let index = 0; index < 70; index += 1) {
|
|
3733
|
+
const x = pseudoRandom(index * 19 + 3) * canvas.width;
|
|
3734
|
+
const y = pseudoRandom(index * 23 + 7) * canvas.height;
|
|
3735
|
+
const alpha = 0.008 + pseudoRandom(index * 31 + 11) * 0.012;
|
|
3736
|
+
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
|
|
3737
|
+
ctx.fillRect(x, y, 1.1, 1.1);
|
|
3738
|
+
}
|
|
3739
|
+
ctx.restore();
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
|
|
2330
3743
|
function renderWaterMotionEffects(ctx, effects, camera, viewport) {
|
|
2331
3744
|
ctx.save();
|
|
2332
3745
|
ctx.globalCompositeOperation = "screen";
|
|
@@ -2405,12 +3818,24 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
|
|
|
2405
3818
|
ctx.restore();
|
|
2406
3819
|
}
|
|
2407
3820
|
|
|
2408
|
-
function renderScene(
|
|
3821
|
+
function renderScene(
|
|
3822
|
+
ctx,
|
|
3823
|
+
canvas,
|
|
3824
|
+
state,
|
|
3825
|
+
shipModel,
|
|
3826
|
+
dom,
|
|
3827
|
+
lightingFeatures,
|
|
3828
|
+
fluidFeatures,
|
|
3829
|
+
clothFeatures
|
|
3830
|
+
) {
|
|
2409
3831
|
const viewport = { width: canvas.width, height: canvas.height };
|
|
2410
3832
|
const camera = buildCamera(state, canvas);
|
|
2411
3833
|
state.camera.eye = camera.eye;
|
|
2412
|
-
const lightingPlan =
|
|
2413
|
-
profile:
|
|
3834
|
+
const lightingPlan = lightingFeatures.createBandPlan({
|
|
3835
|
+
profile:
|
|
3836
|
+
state.focus === "lighting"
|
|
3837
|
+
? lightingFeatures.defaultProfile
|
|
3838
|
+
: lightingFeatures.getProfile(lightingFeatures.defaultProfile).name,
|
|
2414
3839
|
importance: state.focus === "lighting" ? "critical" : "high",
|
|
2415
3840
|
});
|
|
2416
3841
|
const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
|
|
@@ -2439,7 +3864,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2439
3864
|
const water = buildWaterBands(
|
|
2440
3865
|
state,
|
|
2441
3866
|
state.fluidDetail.getSnapshot().currentLevel.config,
|
|
2442
|
-
visuals
|
|
3867
|
+
visuals,
|
|
3868
|
+
fluidFeatures
|
|
2443
3869
|
);
|
|
2444
3870
|
for (const bandMesh of water.bandMeshes) {
|
|
2445
3871
|
const bandAccent = bandMesh.band === "near" ? 0.06 : bandMesh.band === "mid" ? 0.04 : 0;
|
|
@@ -2459,6 +3885,15 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2459
3885
|
normal,
|
|
2460
3886
|
baseColor: bandMesh.color,
|
|
2461
3887
|
accent: bandAccent,
|
|
3888
|
+
material: {
|
|
3889
|
+
name: "water-surface",
|
|
3890
|
+
color: bandMesh.color,
|
|
3891
|
+
roughness: 0.2,
|
|
3892
|
+
metallic: 0,
|
|
3893
|
+
emissive: { r: 0, g: 0, b: 0 },
|
|
3894
|
+
},
|
|
3895
|
+
reflection: 1,
|
|
3896
|
+
surfaceType: "water",
|
|
2462
3897
|
});
|
|
2463
3898
|
}
|
|
2464
3899
|
}
|
|
@@ -2466,12 +3901,13 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2466
3901
|
const waterMotionEffects = buildWaterMotionEffects(state);
|
|
2467
3902
|
const lightSources = collectSceneLightSources(state, visuals);
|
|
2468
3903
|
|
|
2469
|
-
pushHarborGeometry(camera, viewport, sceneTriangles,
|
|
3904
|
+
pushHarborGeometry(camera, viewport, sceneTriangles, state);
|
|
2470
3905
|
const cloth = buildClothSurface(
|
|
2471
3906
|
state,
|
|
2472
3907
|
state,
|
|
2473
3908
|
state.clothDetail.getSnapshot().currentLevel.config,
|
|
2474
|
-
visuals
|
|
3909
|
+
visuals,
|
|
3910
|
+
clothFeatures
|
|
2475
3911
|
);
|
|
2476
3912
|
for (let index = 0; index < cloth.indices.length; index += 3) {
|
|
2477
3913
|
const a = cloth.positions[cloth.indices[index]];
|
|
@@ -2489,24 +3925,47 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2489
3925
|
normal,
|
|
2490
3926
|
baseColor: cloth.color,
|
|
2491
3927
|
accent: cloth.band === "near" ? 0.1 : 0.04,
|
|
3928
|
+
material: {
|
|
3929
|
+
name: "flag-cloth",
|
|
3930
|
+
color: cloth.color,
|
|
3931
|
+
roughness: 0.94,
|
|
3932
|
+
metallic: 0,
|
|
3933
|
+
emissive: { r: 0, g: 0, b: 0 },
|
|
3934
|
+
},
|
|
3935
|
+
reflection: 0,
|
|
3936
|
+
surfaceType: "cloth",
|
|
2492
3937
|
});
|
|
2493
3938
|
}
|
|
2494
3939
|
|
|
2495
3940
|
for (const ship of state.ships) {
|
|
3941
|
+
const activeShipModel = resolveShipModel(state, ship, shipModel);
|
|
2496
3942
|
buildTrianglesFromMesh(
|
|
2497
|
-
|
|
3943
|
+
activeShipModel,
|
|
2498
3944
|
{ position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
|
|
2499
3945
|
ship.tint,
|
|
2500
3946
|
camera,
|
|
2501
3947
|
viewport,
|
|
2502
3948
|
sceneTriangles,
|
|
2503
|
-
|
|
3949
|
+
{
|
|
3950
|
+
accent: nearLighting.rtParticipation.directShadows === "premium" ? 0.08 : 0.02,
|
|
3951
|
+
reflection: 0,
|
|
3952
|
+
surfaceType: "ship",
|
|
3953
|
+
}
|
|
2504
3954
|
);
|
|
2505
3955
|
}
|
|
2506
3956
|
|
|
2507
3957
|
drawTriangles(ctx, waterTriangles, lightDir, reflectionStrength, camera, shadowStrength);
|
|
2508
3958
|
for (const ship of state.ships) {
|
|
2509
|
-
renderShipShadow(
|
|
3959
|
+
renderShipShadow(
|
|
3960
|
+
ctx,
|
|
3961
|
+
resolveShipModel(state, ship, shipModel),
|
|
3962
|
+
ship,
|
|
3963
|
+
state,
|
|
3964
|
+
camera,
|
|
3965
|
+
viewport,
|
|
3966
|
+
lightDir,
|
|
3967
|
+
shadowStrength
|
|
3968
|
+
);
|
|
2510
3969
|
}
|
|
2511
3970
|
renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
|
|
2512
3971
|
for (const source of lightSources.reflectionLights) {
|
|
@@ -2514,9 +3973,18 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2514
3973
|
}
|
|
2515
3974
|
renderWaterMotionEffects(ctx, waterMotionEffects, camera, viewport);
|
|
2516
3975
|
renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
|
|
2517
|
-
drawTriangles(
|
|
3976
|
+
drawTriangles(
|
|
3977
|
+
ctx,
|
|
3978
|
+
sceneTriangles,
|
|
3979
|
+
lightDir,
|
|
3980
|
+
reflectionStrength,
|
|
3981
|
+
camera,
|
|
3982
|
+
shadowStrength,
|
|
3983
|
+
lightSources.directLights
|
|
3984
|
+
);
|
|
2518
3985
|
renderFlagPole(ctx, camera, viewport);
|
|
2519
3986
|
renderClothAccent(ctx, cloth, camera, viewport);
|
|
3987
|
+
renderLighthouseBeam(ctx, state, camera, viewport, visuals);
|
|
2520
3988
|
for (const source of lightSources.directLights) {
|
|
2521
3989
|
renderDirectLightGlow(ctx, source, camera, viewport);
|
|
2522
3990
|
}
|
|
@@ -2524,6 +3992,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2524
3992
|
renderShipRigging(ctx, ship, camera, viewport);
|
|
2525
3993
|
}
|
|
2526
3994
|
renderSprays(ctx, state.sprays, camera, viewport);
|
|
3995
|
+
renderAtmosphericGrade(ctx, canvas, state, visuals);
|
|
2527
3996
|
|
|
2528
3997
|
const debugSnapshot = state.debugSession.getSnapshot();
|
|
2529
3998
|
const quality = {
|
|
@@ -2534,14 +4003,14 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2534
4003
|
|
|
2535
4004
|
const sceneMetrics = [
|
|
2536
4005
|
`focus: ${state.focus}`,
|
|
2537
|
-
`ships: ${state.ships.length} active GLTF hulls`,
|
|
2538
|
-
`moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.
|
|
4006
|
+
`ships: ${state.ships.length} active GLTF hulls across ${new Set(state.ships.map((ship) => ship.modelKey)).size} model families`,
|
|
4007
|
+
`moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.reduce((total, ship) => total + (Array.isArray(ship.lanterns) ? ship.lanterns.length : 0), 0)} warm deck and harbor lights`,
|
|
2539
4008
|
`physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
|
|
2540
4009
|
`physics contacts: ${state.contactCount}`,
|
|
2541
|
-
`mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, shipModel) / 1000).toFixed(1)}t`).join(" · ")}`,
|
|
4010
|
+
`mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, resolveShipModel(state, ship, shipModel)) / 1000).toFixed(1)}t`).join(" · ")}`,
|
|
2542
4011
|
`cloth band: ${cloth.band} -> ${cloth.representation.output}`,
|
|
2543
4012
|
`fluid near band: ${water.bandMeshes[0].representation.output}`,
|
|
2544
|
-
`lighting profile: ${lightingPlan.profile} (${
|
|
4013
|
+
`lighting profile: ${lightingPlan.profile} (${lightingFeatures.distanceBands.length} bands)`,
|
|
2545
4014
|
];
|
|
2546
4015
|
const qualityMetrics = [
|
|
2547
4016
|
`fluid detail: ${quality.fluid.currentLevel.id} (${quality.fluid.currentLevel.config.nearResolution} near cells)`,
|
|
@@ -2561,12 +4030,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2561
4030
|
];
|
|
2562
4031
|
const sceneNotes =
|
|
2563
4032
|
state.focus === "physics"
|
|
2564
|
-
?
|
|
2565
|
-
|
|
2566
|
-
"The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
|
|
2567
|
-
"Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water.",
|
|
2568
|
-
]
|
|
2569
|
-
: SCENE_NOTES;
|
|
4033
|
+
? PHYSICS_SCENE_NOTE_KEYS.map((key) => state.translate(key))
|
|
4034
|
+
: SCENE_NOTE_KEYS.map((key) => state.translate(key));
|
|
2570
4035
|
const custom = state.demoDescription ?? null;
|
|
2571
4036
|
|
|
2572
4037
|
setListContent(
|
|
@@ -2586,22 +4051,33 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2586
4051
|
dom.status.textContent =
|
|
2587
4052
|
typeof custom?.status === "string"
|
|
2588
4053
|
? custom.status
|
|
2589
|
-
:
|
|
4054
|
+
: state.translate(gpuSharedTranslationKeys.statusLive, {
|
|
4055
|
+
fps: state.lastDecision.metrics.fps.toFixed(1),
|
|
4056
|
+
});
|
|
2590
4057
|
dom.details.textContent =
|
|
2591
4058
|
typeof custom?.details === "string"
|
|
2592
4059
|
? custom.details
|
|
2593
4060
|
: state.focus === "physics"
|
|
2594
|
-
?
|
|
2595
|
-
|
|
4061
|
+
? state.translate(gpuSharedTranslationKeys.detailsPhysics, {
|
|
4062
|
+
snapshotStageId: state.physics.plan.snapshotStageId,
|
|
4063
|
+
})
|
|
4064
|
+
: state.showcaseRealisticModelsEnabled
|
|
4065
|
+
? state.translate(gpuSharedTranslationKeys.detailsRealistic, {
|
|
4066
|
+
pressureLevel: state.lastDecision.pressureLevel,
|
|
4067
|
+
})
|
|
4068
|
+
: state.translate(gpuSharedTranslationKeys.detailsLegacy, {
|
|
4069
|
+
pressureLevel: state.lastDecision.pressureLevel,
|
|
4070
|
+
});
|
|
2596
4071
|
}
|
|
2597
4072
|
|
|
2598
|
-
function updateSceneState(state, dt, shipModel) {
|
|
4073
|
+
function updateSceneState(state, dt, shipModel, featureAdapters) {
|
|
2599
4074
|
updateShips(state, dt, shipModel);
|
|
2600
4075
|
updateWaveImpulses(state, dt);
|
|
2601
4076
|
updateSpray(state, dt);
|
|
2602
4077
|
const clothPresentation = resolveClothPresentation(
|
|
2603
4078
|
state,
|
|
2604
|
-
state.clothDetail.getSnapshot().currentLevel.config
|
|
4079
|
+
state.clothDetail.getSnapshot().currentLevel.config,
|
|
4080
|
+
featureAdapters.cloth
|
|
2605
4081
|
);
|
|
2606
4082
|
const clothState = ensureShowcaseClothState(
|
|
2607
4083
|
state,
|
|
@@ -2614,25 +4090,28 @@ function updateSceneState(state, dt, shipModel) {
|
|
|
2614
4090
|
flagMotion: readVisualNumber(state.demoVisuals?.flagMotion, 0.92),
|
|
2615
4091
|
waveInfluence: sampleWave(state, FLAG_LAYOUT.origin.x + FLAG_LAYOUT.width * 0.55, FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * 0.48, state.time),
|
|
2616
4092
|
});
|
|
2617
|
-
updatePhysicsSnapshot(state, shipModel);
|
|
4093
|
+
updatePhysicsSnapshot(state, shipModel, featureAdapters.physics);
|
|
2618
4094
|
}
|
|
2619
4095
|
|
|
2620
|
-
function syncTextState(state, shipModel) {
|
|
4096
|
+
function syncTextState(state, shipModel, featureAdapters) {
|
|
2621
4097
|
const snapshot = {
|
|
2622
4098
|
coordinateSystem: "right-handed world; +x right, +y up, +z forward from the shore",
|
|
2623
4099
|
focus: state.focus,
|
|
2624
4100
|
stress: state.stress,
|
|
2625
4101
|
ships: state.ships.map((ship) => ({
|
|
2626
4102
|
id: ship.id,
|
|
4103
|
+
modelKey: ship.modelKey ?? "brigantine",
|
|
2627
4104
|
x: Number(ship.position.x.toFixed(2)),
|
|
2628
4105
|
y: Number(ship.position.y.toFixed(2)),
|
|
2629
4106
|
z: Number(ship.position.z.toFixed(2)),
|
|
2630
4107
|
vx: Number(ship.velocity.x.toFixed(2)),
|
|
2631
4108
|
vz: Number(ship.velocity.z.toFixed(2)),
|
|
2632
|
-
massKg: Math.round(getShipMass(ship, shipModel)),
|
|
4109
|
+
massKg: Math.round(getShipMass(ship, resolveShipModel(state, ship, shipModel))),
|
|
2633
4110
|
lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0,
|
|
2634
4111
|
})),
|
|
2635
|
-
shipPhysics:
|
|
4112
|
+
shipPhysics: Object.fromEntries(
|
|
4113
|
+
state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
|
|
4114
|
+
),
|
|
2636
4115
|
sprays: state.sprays.length,
|
|
2637
4116
|
waveImpulses: state.waveImpulses.length,
|
|
2638
4117
|
pressure: state.lastDecision?.pressureLevel ?? "stable",
|
|
@@ -2650,41 +4129,63 @@ function syncTextState(state, shipModel) {
|
|
|
2650
4129
|
for (let index = 0; index < step; index += 1) {
|
|
2651
4130
|
state.frame += 1;
|
|
2652
4131
|
state.time += 1 / 60;
|
|
2653
|
-
updateSceneState(state, 1 / 60, shipModel);
|
|
4132
|
+
updateSceneState(state, 1 / 60, shipModel, featureAdapters);
|
|
2654
4133
|
state.lastDecision = recordTelemetry(state, 16.67 + (state.stress ? 6.5 : 0));
|
|
2655
4134
|
}
|
|
2656
4135
|
};
|
|
2657
4136
|
}
|
|
2658
4137
|
|
|
2659
|
-
export async function mountGpuShowcase(options = {}) {
|
|
4138
|
+
export async function mountGpuShowcase(options = {}, featureFlags = null) {
|
|
4139
|
+
const featureAdapters = await resolveShowcaseFeatureAdapters(options);
|
|
2660
4140
|
injectStyles();
|
|
2661
4141
|
const root = options.root ?? document.body;
|
|
2662
4142
|
root.classList?.add?.(ROOT_CLASS);
|
|
4143
|
+
const captureSettings = resolveCaptureSettings(options);
|
|
4144
|
+
if (captureSettings.captureMode) {
|
|
4145
|
+
root.classList?.add?.(CAPTURE_CLASS);
|
|
4146
|
+
}
|
|
2663
4147
|
const previousMarkup = root.innerHTML;
|
|
2664
4148
|
const previousRenderGameToText = window.render_game_to_text;
|
|
2665
4149
|
const previousAdvanceTime = window.advanceTime;
|
|
2666
4150
|
const focus = options.focus ?? new URLSearchParams(window.location.search).get("focus") ?? "integrated";
|
|
4151
|
+
const translate = createGpuSharedTranslator(options.translate);
|
|
2667
4152
|
const dom = buildDemoDom(root, {
|
|
2668
4153
|
packageName: options.packageName ?? "@plasius/gpu-demo-viewer",
|
|
2669
|
-
title: options.title ??
|
|
2670
|
-
subtitle: options.subtitle ??
|
|
4154
|
+
title: options.title ?? translate(gpuSharedTranslationKeys.showcaseTitle),
|
|
4155
|
+
subtitle: options.subtitle ?? translate(gpuSharedTranslationKeys.showcaseSubtitle),
|
|
4156
|
+
translate,
|
|
2671
4157
|
});
|
|
2672
4158
|
dom.focusMode.value = focus;
|
|
4159
|
+
const state = createSceneState(
|
|
4160
|
+
{
|
|
4161
|
+
focus,
|
|
4162
|
+
translate,
|
|
4163
|
+
realisticModelsEnabled: isFeatureEnabled(featureFlags, GPU_SHOWCASE_REALISTIC_MODELS_FEATURE, true),
|
|
4164
|
+
captureMode: captureSettings.captureMode,
|
|
4165
|
+
renderScale: captureSettings.renderScale,
|
|
4166
|
+
},
|
|
4167
|
+
featureAdapters
|
|
4168
|
+
);
|
|
4169
|
+
const assetCatalog = await (state.showcaseRealisticModelsEnabled
|
|
4170
|
+
? loadShowcaseAssetCatalog()
|
|
4171
|
+
: createLegacyShowcaseAssetCatalog());
|
|
4172
|
+
const shipModel = assetCatalog.ships[assetCatalog.primaryShipKey];
|
|
2673
4173
|
|
|
2674
|
-
|
|
2675
|
-
const shipModel = await loadGltfModel(resolveShowcaseAssetUrl());
|
|
4174
|
+
state.assetCatalog = assetCatalog;
|
|
2676
4175
|
state.shipModel = shipModel;
|
|
2677
4176
|
state.packageState =
|
|
2678
4177
|
typeof options.createState === "function" ? options.createState() : undefined;
|
|
2679
|
-
updatePhysicsSnapshot(state, shipModel);
|
|
4178
|
+
updatePhysicsSnapshot(state, shipModel, featureAdapters.physics);
|
|
2680
4179
|
state.lastDecision = recordTelemetry(state, 16.4);
|
|
2681
4180
|
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
2682
|
-
syncTextState(state, shipModel);
|
|
4181
|
+
syncTextState(state, shipModel, featureAdapters);
|
|
2683
4182
|
|
|
2684
4183
|
const ctx = dom.canvas.getContext("2d");
|
|
2685
4184
|
if (!ctx) {
|
|
2686
4185
|
throw new Error("2D canvas context is required for the shared showcase.");
|
|
2687
4186
|
}
|
|
4187
|
+
ctx.imageSmoothingEnabled = true;
|
|
4188
|
+
ctx.imageSmoothingQuality = "high";
|
|
2688
4189
|
let animationFrameId = 0;
|
|
2689
4190
|
let destroyed = false;
|
|
2690
4191
|
const renderFrame = (nowMs) => {
|
|
@@ -2699,21 +4200,33 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2699
4200
|
state.lastTimeMs = nowMs;
|
|
2700
4201
|
state.time += dt;
|
|
2701
4202
|
state.frame += 1;
|
|
2702
|
-
updateSceneState(state, dt, shipModel);
|
|
4203
|
+
updateSceneState(state, dt, shipModel, featureAdapters);
|
|
2703
4204
|
updatePackageState(state, options, shipModel, dt);
|
|
2704
4205
|
const syntheticFrame = 14.2 + state.sprays.length * 0.1 + (state.stress ? 6.4 : 0);
|
|
2705
4206
|
state.lastDecision = recordTelemetry(state, syntheticFrame);
|
|
2706
4207
|
}
|
|
2707
4208
|
|
|
2708
4209
|
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
2709
|
-
|
|
2710
|
-
|
|
4210
|
+
resizeCanvasToDisplaySize(dom.canvas, state);
|
|
4211
|
+
renderScene(
|
|
4212
|
+
ctx,
|
|
4213
|
+
dom.canvas,
|
|
4214
|
+
state,
|
|
4215
|
+
shipModel,
|
|
4216
|
+
dom,
|
|
4217
|
+
featureAdapters.lighting,
|
|
4218
|
+
featureAdapters.fluid,
|
|
4219
|
+
featureAdapters.cloth
|
|
4220
|
+
);
|
|
4221
|
+
syncTextState(state, shipModel, featureAdapters);
|
|
2711
4222
|
animationFrameId = requestAnimationFrame(renderFrame);
|
|
2712
4223
|
};
|
|
2713
4224
|
|
|
2714
4225
|
const handlePauseClick = () => {
|
|
2715
4226
|
state.paused = !state.paused;
|
|
2716
|
-
dom.pauseButton.textContent = state.paused
|
|
4227
|
+
dom.pauseButton.textContent = state.paused
|
|
4228
|
+
? state.translate(gpuSharedTranslationKeys.resume)
|
|
4229
|
+
: state.translate(gpuSharedTranslationKeys.pause);
|
|
2717
4230
|
};
|
|
2718
4231
|
const handleStressChange = () => {
|
|
2719
4232
|
state.stress = dom.stressToggle.checked;
|
|
@@ -2750,6 +4263,7 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2750
4263
|
state.packageState = undefined;
|
|
2751
4264
|
}
|
|
2752
4265
|
root.classList?.remove?.(ROOT_CLASS);
|
|
4266
|
+
root.classList?.remove?.(CAPTURE_CLASS);
|
|
2753
4267
|
root.innerHTML = previousMarkup;
|
|
2754
4268
|
if (typeof previousRenderGameToText === "function") {
|
|
2755
4269
|
window.render_game_to_text = previousRenderGameToText;
|
|
@@ -2770,7 +4284,19 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2770
4284
|
};
|
|
2771
4285
|
}
|
|
2772
4286
|
|
|
2773
|
-
function updatePhysicsSnapshot(state, shipModel) {
|
|
4287
|
+
function updatePhysicsSnapshot(state, shipModel, physicsFeatures) {
|
|
4288
|
+
const createPhysicsWorldSnapshot = assertRequiredFunction(
|
|
4289
|
+
physicsFeatures,
|
|
4290
|
+
"physics",
|
|
4291
|
+
"createWorldSnapshot"
|
|
4292
|
+
);
|
|
4293
|
+
const rigidBodyShapes = Object.fromEntries(
|
|
4294
|
+
state.ships.map((ship) => [
|
|
4295
|
+
ship.id,
|
|
4296
|
+
resolveShipModel(state, ship, shipModel)?.physics?.shape ?? "box",
|
|
4297
|
+
])
|
|
4298
|
+
);
|
|
4299
|
+
|
|
2774
4300
|
state.physics.snapshot = createPhysicsWorldSnapshot({
|
|
2775
4301
|
frameId: `showcase-${state.frame}`,
|
|
2776
4302
|
tick: state.frame,
|
|
@@ -2787,6 +4313,7 @@ function updatePhysicsSnapshot(state, shipModel) {
|
|
|
2787
4313
|
contactCount: state.contactCount,
|
|
2788
4314
|
snapshotStageId: state.physics.plan.snapshotStageId,
|
|
2789
4315
|
rigidBodyShape: shipModel.physics.shape ?? "box",
|
|
4316
|
+
rigidBodyShapes,
|
|
2790
4317
|
},
|
|
2791
4318
|
});
|
|
2792
4319
|
}
|