@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
|
@@ -0,0 +1,3785 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GPU_SHOWCASE_REALISTIC_MODELS_FEATURE,
|
|
3
|
+
createGpuSharedTranslator,
|
|
4
|
+
gpuSharedTranslationKeys
|
|
5
|
+
} from "./chunk-3ARPGHCQ.js";
|
|
6
|
+
import {
|
|
7
|
+
loadGltfModel,
|
|
8
|
+
resolveShowcaseAssetUrl
|
|
9
|
+
} from "./chunk-W5GA3VA6.js";
|
|
10
|
+
import "./chunk-2GM64LB6.js";
|
|
11
|
+
|
|
12
|
+
// src/showcase-runtime.js
|
|
13
|
+
var STYLE_ID = "plasius-shared-3d-showcase-style";
|
|
14
|
+
var ROOT_CLASS = "plasius-showcase-root";
|
|
15
|
+
var CAPTURE_CLASS = "plasius-showcase-root--capture";
|
|
16
|
+
var DEFAULT_CANVAS_WIDTH = 1280;
|
|
17
|
+
var DEFAULT_CANVAS_HEIGHT = 720;
|
|
18
|
+
var CAPTURE_CANVAS_PIXEL_BUDGET = 1920 * 1080;
|
|
19
|
+
var SHIP_SCALE = 1.1;
|
|
20
|
+
var HARBOR_BOUNDS = Object.freeze({
|
|
21
|
+
minX: -11.2,
|
|
22
|
+
maxX: 11.2,
|
|
23
|
+
minZ: 1.8,
|
|
24
|
+
maxZ: 17.2
|
|
25
|
+
});
|
|
26
|
+
var CAMERA_PRESETS = Object.freeze({
|
|
27
|
+
integrated: Object.freeze({ yaw: -0.55, pitch: 0.34, distance: 27, target: [0, 2.2, 0] }),
|
|
28
|
+
lighting: Object.freeze({ yaw: -0.28, pitch: 0.28, distance: 23, target: [0, 2.8, 0] }),
|
|
29
|
+
cloth: Object.freeze({ yaw: -1.1, pitch: 0.25, distance: 15, target: [-8.4, 5.3, -1.5] }),
|
|
30
|
+
fluid: Object.freeze({ yaw: -0.4, pitch: 0.18, distance: 18, target: [0, 1.2, 6] }),
|
|
31
|
+
physics: Object.freeze({ yaw: -0.12, pitch: 0.27, distance: 16, target: [0, 1.8, 6.8] }),
|
|
32
|
+
performance: Object.freeze({ yaw: -0.65, pitch: 0.36, distance: 24, target: [0, 2.2, 0] }),
|
|
33
|
+
debug: Object.freeze({ yaw: -0.7, pitch: 0.32, distance: 24, target: [0, 2.2, 0] })
|
|
34
|
+
});
|
|
35
|
+
var FALLBACK_LIGHTING_DISTANCE_BANDS = Object.freeze([
|
|
36
|
+
Object.freeze({
|
|
37
|
+
band: "near",
|
|
38
|
+
primaryShadowSource: "ray-traced-primary",
|
|
39
|
+
rtParticipation: Object.freeze({
|
|
40
|
+
reflections: "full",
|
|
41
|
+
globalIllumination: "high",
|
|
42
|
+
directShadows: "premium"
|
|
43
|
+
}),
|
|
44
|
+
updateCadenceDivisor: 1
|
|
45
|
+
}),
|
|
46
|
+
Object.freeze({
|
|
47
|
+
band: "mid",
|
|
48
|
+
primaryShadowSource: "ray-traced-secondary",
|
|
49
|
+
rtParticipation: Object.freeze({
|
|
50
|
+
reflections: "medium",
|
|
51
|
+
globalIllumination: "medium",
|
|
52
|
+
directShadows: "selective"
|
|
53
|
+
}),
|
|
54
|
+
updateCadenceDivisor: 2
|
|
55
|
+
}),
|
|
56
|
+
Object.freeze({
|
|
57
|
+
band: "far",
|
|
58
|
+
primaryShadowSource: "baked",
|
|
59
|
+
rtParticipation: Object.freeze({
|
|
60
|
+
reflections: "low",
|
|
61
|
+
globalIllumination: "low",
|
|
62
|
+
directShadows: "none"
|
|
63
|
+
}),
|
|
64
|
+
updateCadenceDivisor: 4
|
|
65
|
+
}),
|
|
66
|
+
Object.freeze({
|
|
67
|
+
band: "horizon",
|
|
68
|
+
primaryShadowSource: "impression",
|
|
69
|
+
rtParticipation: Object.freeze({
|
|
70
|
+
reflections: "off",
|
|
71
|
+
globalIllumination: "off",
|
|
72
|
+
directShadows: "none"
|
|
73
|
+
}),
|
|
74
|
+
updateCadenceDivisor: 8
|
|
75
|
+
})
|
|
76
|
+
]);
|
|
77
|
+
var FALLBACK_LIGHTING_PROFILE = "cinematic";
|
|
78
|
+
var FALLBACK_PHYSICS_PROFILE = "cinematic";
|
|
79
|
+
var FALLBACK_PERFORMANCE_LEVELS = Object.freeze({
|
|
80
|
+
fluid: Object.freeze([
|
|
81
|
+
Object.freeze({
|
|
82
|
+
id: "low",
|
|
83
|
+
config: Object.freeze({ nearResolution: 10, midResolution: 6, splashCount: 10 }),
|
|
84
|
+
estimatedCostMs: 0.8
|
|
85
|
+
}),
|
|
86
|
+
Object.freeze({
|
|
87
|
+
id: "medium",
|
|
88
|
+
config: Object.freeze({ nearResolution: 16, midResolution: 8, splashCount: 18 }),
|
|
89
|
+
estimatedCostMs: 1.4
|
|
90
|
+
}),
|
|
91
|
+
Object.freeze({
|
|
92
|
+
id: "high",
|
|
93
|
+
config: Object.freeze({ nearResolution: 24, midResolution: 12, splashCount: 28 }),
|
|
94
|
+
estimatedCostMs: 2.4
|
|
95
|
+
})
|
|
96
|
+
]),
|
|
97
|
+
cloth: Object.freeze([
|
|
98
|
+
Object.freeze({
|
|
99
|
+
id: "low",
|
|
100
|
+
config: Object.freeze({ cols: 10, rows: 7 }),
|
|
101
|
+
estimatedCostMs: 0.7
|
|
102
|
+
}),
|
|
103
|
+
Object.freeze({
|
|
104
|
+
id: "medium",
|
|
105
|
+
config: Object.freeze({ cols: 16, rows: 11 }),
|
|
106
|
+
estimatedCostMs: 1.3
|
|
107
|
+
}),
|
|
108
|
+
Object.freeze({
|
|
109
|
+
id: "high",
|
|
110
|
+
config: Object.freeze({ cols: 24, rows: 16 }),
|
|
111
|
+
estimatedCostMs: 2.1
|
|
112
|
+
})
|
|
113
|
+
]),
|
|
114
|
+
lighting: Object.freeze([
|
|
115
|
+
Object.freeze({
|
|
116
|
+
id: "low",
|
|
117
|
+
config: Object.freeze({ shadowStrength: 0.18, reflectionStrength: 0.08 }),
|
|
118
|
+
estimatedCostMs: 0.5
|
|
119
|
+
}),
|
|
120
|
+
Object.freeze({
|
|
121
|
+
id: "medium",
|
|
122
|
+
config: Object.freeze({ shadowStrength: 0.34, reflectionStrength: 0.16 }),
|
|
123
|
+
estimatedCostMs: 1
|
|
124
|
+
}),
|
|
125
|
+
Object.freeze({
|
|
126
|
+
id: "high",
|
|
127
|
+
config: Object.freeze({ shadowStrength: 0.5, reflectionStrength: 0.24 }),
|
|
128
|
+
estimatedCostMs: 1.8
|
|
129
|
+
})
|
|
130
|
+
])
|
|
131
|
+
});
|
|
132
|
+
function createFallbackClothFeatureModule() {
|
|
133
|
+
const fallback = createFallbackClothFeatureAdapters();
|
|
134
|
+
return {
|
|
135
|
+
clothGarmentKinds: fallback.garmentKinds,
|
|
136
|
+
clothProfileNames: fallback.profileNames,
|
|
137
|
+
createClothRepresentationPlan: fallback.createPlan,
|
|
138
|
+
selectClothRepresentationBand: fallback.selectBand
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function createFallbackFluidFeatureModule() {
|
|
142
|
+
const fallback = createFallbackFluidFeatureAdapters();
|
|
143
|
+
return {
|
|
144
|
+
fluidBodyKinds: fallback.bodyKinds,
|
|
145
|
+
fluidProfileNames: fallback.profileNames,
|
|
146
|
+
createFluidContinuityEnvelope: fallback.createContinuityEnvelope,
|
|
147
|
+
createFluidRepresentationPlan: fallback.createPlan,
|
|
148
|
+
selectFluidRepresentationBand: fallback.selectBand
|
|
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
|
+
function createFallbackPerformanceQualityState(levels = [], initialLevel = "high", profile = "") {
|
|
172
|
+
const fallbackLevels = levels.length ? levels : FALLBACK_PERFORMANCE_LEVELS[profile] ?? [];
|
|
173
|
+
const resolvedLevels = fallbackLevels.map((entry) => ({
|
|
174
|
+
id: String(entry?.id ?? "high"),
|
|
175
|
+
config: entry?.config ?? {},
|
|
176
|
+
estimatedCostMs: Number.isFinite(Number(entry?.estimatedCostMs)) ? Number(entry.estimatedCostMs) : 1
|
|
177
|
+
}));
|
|
178
|
+
if (resolvedLevels.length === 0) {
|
|
179
|
+
return {
|
|
180
|
+
module: {
|
|
181
|
+
id: "high",
|
|
182
|
+
config: Object.freeze({}),
|
|
183
|
+
estimatedCostMs: 1
|
|
184
|
+
},
|
|
185
|
+
getSnapshot() {
|
|
186
|
+
return {
|
|
187
|
+
currentLevel: {
|
|
188
|
+
id: "high",
|
|
189
|
+
config: Object.freeze({}),
|
|
190
|
+
estimatedCostMs: 1
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
id: "high"
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const initial = resolvedLevels.find((entry) => entry.id === initialLevel) ?? resolvedLevels[0];
|
|
198
|
+
return {
|
|
199
|
+
id: initial.id,
|
|
200
|
+
getSnapshot() {
|
|
201
|
+
return {
|
|
202
|
+
currentLevel: {
|
|
203
|
+
id: initial.id,
|
|
204
|
+
config: initial.config,
|
|
205
|
+
estimatedCostMs: initial.estimatedCostMs
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function createFallbackPerformanceFeatureModule() {
|
|
212
|
+
const moduleIds = /* @__PURE__ */ new Set(["fluid-detail", "cloth-detail", "lighting-detail"]);
|
|
213
|
+
return {
|
|
214
|
+
createDeviceProfile(profile = {}) {
|
|
215
|
+
return {
|
|
216
|
+
deviceClass: "desktop",
|
|
217
|
+
mode: "flat",
|
|
218
|
+
refreshRateHz: Number.isFinite(profile?.refreshRateHz) ? Number(profile.refreshRateHz) : 60,
|
|
219
|
+
supportedFrameRates: Array.isArray(profile?.supportedFrameRates) ? profile.supportedFrameRates : [60, 90],
|
|
220
|
+
supportsWebGpu: true
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
createQualityLadderAdapter({ id, domain, levels, initialLevel }) {
|
|
224
|
+
return {
|
|
225
|
+
id: String(id ?? ""),
|
|
226
|
+
domain,
|
|
227
|
+
...createFallbackPerformanceQualityState(
|
|
228
|
+
domain === "fluid" ? FALLBACK_PERFORMANCE_LEVELS.fluid : domain === "cloth" ? FALLBACK_PERFORMANCE_LEVELS.cloth : FALLBACK_PERFORMANCE_LEVELS.lighting,
|
|
229
|
+
initialLevel,
|
|
230
|
+
domain
|
|
231
|
+
)
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
createGpuPerformanceGovernor({ device, modules = [], adaptation = {} } = {}) {
|
|
235
|
+
let pressureLevel = "stable";
|
|
236
|
+
let frameSamples = 0;
|
|
237
|
+
let averageMs = 16.67;
|
|
238
|
+
const clamp2 = (next = 16.67) => Number.isFinite(next) ? Math.max(1, next) : 16.67;
|
|
239
|
+
const target = Object.freeze({
|
|
240
|
+
targetFrameTimeMs: 16.67,
|
|
241
|
+
downgradeFrameTimeMs: clamp2(adaptation?.degradeThresholdMs ?? 20),
|
|
242
|
+
upgradeFrameTimeMs: clamp2(adaptation?.upgradeThresholdMs ?? 14)
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
recordFrame({ frameTimeMs = averageMs } = {}) {
|
|
246
|
+
const sample = Number.isFinite(Number(frameTimeMs)) ? Number(frameTimeMs) : averageMs;
|
|
247
|
+
frameSamples += 1;
|
|
248
|
+
averageMs = clamp2((averageMs * (frameSamples - 1) + sample) / frameSamples);
|
|
249
|
+
const fps = 1e3 / averageMs;
|
|
250
|
+
pressureLevel = sample > target.downgradeFrameTimeMs ? "degrade" : pressureLevel === "degrade" && sample <= target.upgradeFrameTimeMs ? "stable" : pressureLevel;
|
|
251
|
+
return {
|
|
252
|
+
pressureLevel,
|
|
253
|
+
metrics: {
|
|
254
|
+
fps,
|
|
255
|
+
averageFrameTimeMs: averageMs
|
|
256
|
+
},
|
|
257
|
+
adjustments: pressureLevel === "degrade" ? [{ type: "capability-step-down" }] : []
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
getTarget() {
|
|
261
|
+
return target;
|
|
262
|
+
},
|
|
263
|
+
getState() {
|
|
264
|
+
return {
|
|
265
|
+
modules: modules.filter((entry) => entry != null && typeof entry.id === "string" && moduleIds.has(entry.id)).map((entry) => ({
|
|
266
|
+
isAtMaximum: pressureLevel === "stable"
|
|
267
|
+
}))
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function createFallbackDebugFeatureModule() {
|
|
275
|
+
let queueSamples = 0;
|
|
276
|
+
let queuePeakDepth = 0;
|
|
277
|
+
let readyLaneSamples = 0;
|
|
278
|
+
let readyLanePeakDepth = 0;
|
|
279
|
+
let dispatchSamples = 0;
|
|
280
|
+
let dispatchDurationTotal = 0;
|
|
281
|
+
let dependencyUnlockSamples = 0;
|
|
282
|
+
let dependencyUnlockCount = 0;
|
|
283
|
+
let pipelineSamples = 0;
|
|
284
|
+
let pipelineAgeTotal = 0;
|
|
285
|
+
let frameSamples = 0;
|
|
286
|
+
let frameTimeTotal = 0;
|
|
287
|
+
let gpuBusyTotal = 0;
|
|
288
|
+
let frameDroppedSamples = 0;
|
|
289
|
+
let memoryTotalBytes = 0;
|
|
290
|
+
function ensureNumber(value, fallback = 0) {
|
|
291
|
+
const asNumber = Number(value);
|
|
292
|
+
return Number.isFinite(asNumber) ? asNumber : fallback;
|
|
293
|
+
}
|
|
294
|
+
function createDebugSession({ adapter } = {}) {
|
|
295
|
+
const memoryHint = Number.isFinite(Number(adapter?.memoryCapacityHintBytes)) ? Number(adapter.memoryCapacityHintBytes) : 0;
|
|
296
|
+
memoryTotalBytes = Math.max(0, memoryHint);
|
|
297
|
+
return {
|
|
298
|
+
trackAllocation({ sizeBytes = 0 } = {}) {
|
|
299
|
+
memoryTotalBytes += ensureNumber(sizeBytes);
|
|
300
|
+
},
|
|
301
|
+
recordQueue({ depth = 0 } = {}) {
|
|
302
|
+
queueSamples += 1;
|
|
303
|
+
queuePeakDepth = Math.max(queuePeakDepth, ensureNumber(depth, 0));
|
|
304
|
+
},
|
|
305
|
+
recordReadyLane({ depth = 0 } = {}) {
|
|
306
|
+
readyLaneSamples += 1;
|
|
307
|
+
readyLanePeakDepth = Math.max(readyLanePeakDepth, ensureNumber(depth, 0));
|
|
308
|
+
},
|
|
309
|
+
recordDispatch({ durationMs = 0 } = {}) {
|
|
310
|
+
dispatchSamples += 1;
|
|
311
|
+
dispatchDurationTotal += ensureNumber(durationMs);
|
|
312
|
+
},
|
|
313
|
+
recordDependencyUnlock({ unlockCount = 0 } = {}) {
|
|
314
|
+
dependencyUnlockSamples += 1;
|
|
315
|
+
dependencyUnlockCount += ensureNumber(unlockCount);
|
|
316
|
+
},
|
|
317
|
+
recordPipelinePhase({ snapshotAgeMs = 0 } = {}) {
|
|
318
|
+
pipelineSamples += 1;
|
|
319
|
+
pipelineAgeTotal += ensureNumber(snapshotAgeMs);
|
|
320
|
+
},
|
|
321
|
+
recordFrame({
|
|
322
|
+
frameTimeMs = 16.67,
|
|
323
|
+
targetFrameTimeMs = 16.67,
|
|
324
|
+
gpuBusyMs = 0,
|
|
325
|
+
dropped = false
|
|
326
|
+
} = {}) {
|
|
327
|
+
frameSamples += 1;
|
|
328
|
+
frameTimeTotal += ensureNumber(frameTimeMs);
|
|
329
|
+
gpuBusyTotal += ensureNumber(gpuBusyMs);
|
|
330
|
+
frameDroppedSamples += dropped === true ? 1 : 0;
|
|
331
|
+
targetFrameTimeMs;
|
|
332
|
+
},
|
|
333
|
+
getSnapshot() {
|
|
334
|
+
const queueAverageDepth = queueSamples > 0 ? queuePeakDepth / queueSamples : 0;
|
|
335
|
+
return {
|
|
336
|
+
frames: {
|
|
337
|
+
sampleCount: frameSamples,
|
|
338
|
+
averageFrameTimeMs: frameSamples > 0 ? frameTimeTotal / frameSamples : 0,
|
|
339
|
+
averageGpuBusyMs: frameSamples > 0 ? gpuBusyTotal / frameSamples : 0,
|
|
340
|
+
droppedCount: frameDroppedSamples
|
|
341
|
+
},
|
|
342
|
+
queues: {
|
|
343
|
+
sampleCount: queueSamples,
|
|
344
|
+
peakDepth: queuePeakDepth,
|
|
345
|
+
averageDepth: queueAverageDepth
|
|
346
|
+
},
|
|
347
|
+
dispatch: {
|
|
348
|
+
sampleCount: dispatchSamples,
|
|
349
|
+
averageDurationMs: dispatchSamples > 0 ? dispatchDurationTotal / dispatchSamples : 0
|
|
350
|
+
},
|
|
351
|
+
dag: {
|
|
352
|
+
peakReadyLaneDepth: readyLanePeakDepth,
|
|
353
|
+
totalUnlockCount: dependencyUnlockCount,
|
|
354
|
+
unlockSamples: dependencyUnlockSamples
|
|
355
|
+
},
|
|
356
|
+
pipeline: {
|
|
357
|
+
sampleCount: pipelineSamples,
|
|
358
|
+
averageSnapshotAgeMs: pipelineSamples > 0 ? pipelineAgeTotal / pipelineSamples : 0
|
|
359
|
+
},
|
|
360
|
+
memory: {
|
|
361
|
+
totalTrackedBytes: memoryTotalBytes
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
createGpuDebugSession({ adapter } = {}) {
|
|
369
|
+
return createDebugSession({ adapter });
|
|
370
|
+
},
|
|
371
|
+
createSession({ adapter } = {}) {
|
|
372
|
+
return createDebugSession({ adapter });
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function createFallbackPhysicsFeatureModule() {
|
|
377
|
+
const fallbackPlan = Object.freeze({
|
|
378
|
+
snapshotStageId: "baseline",
|
|
379
|
+
stageOrder: Object.freeze(["authoritative"]),
|
|
380
|
+
secondarySimulationStageIds: Object.freeze(["visual"])
|
|
381
|
+
});
|
|
382
|
+
return {
|
|
383
|
+
createPhysicsSimulationPlan() {
|
|
384
|
+
return {
|
|
385
|
+
snapshotStageId: "baseline",
|
|
386
|
+
stageOrder: fallbackPlan.stageOrder,
|
|
387
|
+
secondarySimulationStageIds: fallbackPlan.secondarySimulationStageIds
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
createPhysicsWorldSnapshot(input = {}) {
|
|
391
|
+
return {
|
|
392
|
+
stage: "baseline",
|
|
393
|
+
stability: "stable",
|
|
394
|
+
stageId: fallbackPlan.snapshotStageId,
|
|
395
|
+
metadata: input.metadata ?? {},
|
|
396
|
+
bodyCount: input.bodyCount ?? 0,
|
|
397
|
+
dynamicBodyCount: input.dynamicBodyCount ?? 0,
|
|
398
|
+
contactCount: input.contactCount ?? 0,
|
|
399
|
+
snapshotStageId: fallbackPlan.snapshotStageId
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
defaultPhysicsWorkerProfile: FALLBACK_PHYSICS_PROFILE,
|
|
403
|
+
getPhysicsWorkerManifest() {
|
|
404
|
+
return {
|
|
405
|
+
jobs: [
|
|
406
|
+
Object.freeze({
|
|
407
|
+
worker: Object.freeze({ authority: "authoritative", jobType: "simulate" })
|
|
408
|
+
})
|
|
409
|
+
],
|
|
410
|
+
suggestedAllocationIds: Object.freeze(["physics-workspace"])
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
var SHOWCASE_FEATURE_LOADERS = Object.freeze({
|
|
416
|
+
cloth: () => Promise.resolve(createFallbackClothFeatureModule()),
|
|
417
|
+
fluid: () => Promise.resolve(createFallbackFluidFeatureModule()),
|
|
418
|
+
lighting: () => Promise.resolve(createFallbackLightingFeatureModule()),
|
|
419
|
+
performance: () => Promise.resolve(createFallbackPerformanceFeatureModule()),
|
|
420
|
+
debug: () => Promise.resolve(createFallbackDebugFeatureModule()),
|
|
421
|
+
physics: () => Promise.resolve(createFallbackPhysicsFeatureModule())
|
|
422
|
+
});
|
|
423
|
+
var DEFAULT_FLUID_BAND_THRESHOLDS = Object.freeze({
|
|
424
|
+
near: Object.freeze({ minDistance: 0, maxDistance: 22 }),
|
|
425
|
+
mid: Object.freeze({ minDistance: 22, maxDistance: 90 }),
|
|
426
|
+
far: Object.freeze({ minDistance: 90, maxDistance: 260 }),
|
|
427
|
+
horizon: Object.freeze({ minDistance: 260, maxDistance: Number.POSITIVE_INFINITY })
|
|
428
|
+
});
|
|
429
|
+
var DEFAULT_CLOTH_BAND_THRESHOLDS = Object.freeze({
|
|
430
|
+
near: Object.freeze({ minDistance: 0, maxDistance: 24 }),
|
|
431
|
+
mid: Object.freeze({ minDistance: 24, maxDistance: 86 }),
|
|
432
|
+
far: Object.freeze({ minDistance: 86, maxDistance: 190 }),
|
|
433
|
+
horizon: Object.freeze({ minDistance: 190, maxDistance: Number.POSITIVE_INFINITY })
|
|
434
|
+
});
|
|
435
|
+
function resolveFluidBandSelection(distance, thresholds = DEFAULT_FLUID_BAND_THRESHOLDS) {
|
|
436
|
+
if (!Number.isFinite(distance)) {
|
|
437
|
+
return "horizon";
|
|
438
|
+
}
|
|
439
|
+
if (distance <= (thresholds.near?.maxDistance ?? DEFAULT_FLUID_BAND_THRESHOLDS.near.maxDistance)) {
|
|
440
|
+
return "near";
|
|
441
|
+
}
|
|
442
|
+
if (distance <= (thresholds.mid?.maxDistance ?? DEFAULT_FLUID_BAND_THRESHOLDS.mid.maxDistance)) {
|
|
443
|
+
return "mid";
|
|
444
|
+
}
|
|
445
|
+
if (distance <= (thresholds.far?.maxDistance ?? DEFAULT_FLUID_BAND_THRESHOLDS.far.maxDistance)) {
|
|
446
|
+
return "far";
|
|
447
|
+
}
|
|
448
|
+
return "horizon";
|
|
449
|
+
}
|
|
450
|
+
function createFallbackFluidFeatureAdapters() {
|
|
451
|
+
const defaultContinuityBand = Object.freeze({
|
|
452
|
+
amplitudeFloor: 0.22,
|
|
453
|
+
frequencyFloor: 0.05,
|
|
454
|
+
blendWindowMeters: 14,
|
|
455
|
+
retainFoamHistory: true,
|
|
456
|
+
retainDirectionality: true
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
bodyKinds: Object.freeze(["ocean"]),
|
|
460
|
+
profileNames: Object.freeze(["cinematic"]),
|
|
461
|
+
createContinuityEnvelope() {
|
|
462
|
+
return Object.freeze({
|
|
463
|
+
bands: Object.freeze({
|
|
464
|
+
near: defaultContinuityBand,
|
|
465
|
+
mid: Object.freeze({
|
|
466
|
+
...defaultContinuityBand,
|
|
467
|
+
blendWindowMeters: 22,
|
|
468
|
+
amplitudeFloor: 0.17
|
|
469
|
+
}),
|
|
470
|
+
far: Object.freeze({
|
|
471
|
+
...defaultContinuityBand,
|
|
472
|
+
blendWindowMeters: 34,
|
|
473
|
+
amplitudeFloor: 0.12
|
|
474
|
+
}),
|
|
475
|
+
horizon: Object.freeze({
|
|
476
|
+
...defaultContinuityBand,
|
|
477
|
+
blendWindowMeters: 42,
|
|
478
|
+
amplitudeFloor: 0.09
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
});
|
|
482
|
+
},
|
|
483
|
+
createPlan({ fluidBodyId = "harbor", kind = "ocean", profile = "cinematic" }) {
|
|
484
|
+
return Object.freeze({
|
|
485
|
+
fluidBodyId,
|
|
486
|
+
kind,
|
|
487
|
+
profile,
|
|
488
|
+
thresholds: DEFAULT_FLUID_BAND_THRESHOLDS,
|
|
489
|
+
representations: Object.freeze([
|
|
490
|
+
Object.freeze({
|
|
491
|
+
band: "near",
|
|
492
|
+
output: "raster",
|
|
493
|
+
rtParticipation: "full",
|
|
494
|
+
shading: Object.freeze({ refraction: 0.14, reflectionMode: "full", caustics: true })
|
|
495
|
+
}),
|
|
496
|
+
Object.freeze({
|
|
497
|
+
band: "mid",
|
|
498
|
+
output: "raster",
|
|
499
|
+
rtParticipation: "reduced",
|
|
500
|
+
shading: Object.freeze({ reflectionMode: "partial" })
|
|
501
|
+
}),
|
|
502
|
+
Object.freeze({
|
|
503
|
+
band: "far",
|
|
504
|
+
output: "raster",
|
|
505
|
+
rtParticipation: "low",
|
|
506
|
+
shading: Object.freeze({ reflectionMode: "partial" })
|
|
507
|
+
}),
|
|
508
|
+
Object.freeze({
|
|
509
|
+
band: "horizon",
|
|
510
|
+
output: "coarse",
|
|
511
|
+
rtParticipation: "off",
|
|
512
|
+
shading: Object.freeze({ reflectionMode: "reduced" })
|
|
513
|
+
})
|
|
514
|
+
])
|
|
515
|
+
});
|
|
516
|
+
},
|
|
517
|
+
selectBand(distance, thresholds = DEFAULT_FLUID_BAND_THRESHOLDS) {
|
|
518
|
+
return resolveFluidBandSelection(distance, thresholds);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function createFallbackClothFeatureAdapters() {
|
|
523
|
+
const defaultContinuity = Object.freeze({
|
|
524
|
+
amplitudeFloor: 0.22,
|
|
525
|
+
wrinkleFloor: 0.32,
|
|
526
|
+
damping: 0.58,
|
|
527
|
+
creaseBias: 0.14
|
|
528
|
+
});
|
|
529
|
+
return {
|
|
530
|
+
garmentKinds: Object.freeze(["flag"]),
|
|
531
|
+
profileNames: Object.freeze(["cinematic"]),
|
|
532
|
+
createPlan() {
|
|
533
|
+
return Object.freeze({
|
|
534
|
+
thresholds: DEFAULT_CLOTH_BAND_THRESHOLDS,
|
|
535
|
+
representations: Object.freeze([
|
|
536
|
+
Object.freeze({
|
|
537
|
+
band: "near",
|
|
538
|
+
continuity: defaultContinuity
|
|
539
|
+
}),
|
|
540
|
+
Object.freeze({
|
|
541
|
+
band: "mid",
|
|
542
|
+
continuity: defaultContinuity
|
|
543
|
+
}),
|
|
544
|
+
Object.freeze({
|
|
545
|
+
band: "far",
|
|
546
|
+
continuity: defaultContinuity
|
|
547
|
+
}),
|
|
548
|
+
Object.freeze({
|
|
549
|
+
band: "horizon",
|
|
550
|
+
continuity: defaultContinuity
|
|
551
|
+
})
|
|
552
|
+
])
|
|
553
|
+
});
|
|
554
|
+
},
|
|
555
|
+
selectBand(distance, thresholds = DEFAULT_CLOTH_BAND_THRESHOLDS) {
|
|
556
|
+
if (!Number.isFinite(distance)) {
|
|
557
|
+
return "horizon";
|
|
558
|
+
}
|
|
559
|
+
if (distance <= (thresholds.near?.maxDistance ?? DEFAULT_CLOTH_BAND_THRESHOLDS.near.maxDistance)) {
|
|
560
|
+
return "near";
|
|
561
|
+
}
|
|
562
|
+
if (distance <= (thresholds.mid?.maxDistance ?? DEFAULT_CLOTH_BAND_THRESHOLDS.mid.maxDistance)) {
|
|
563
|
+
return "mid";
|
|
564
|
+
}
|
|
565
|
+
if (distance <= (thresholds.far?.maxDistance ?? DEFAULT_CLOTH_BAND_THRESHOLDS.far.maxDistance)) {
|
|
566
|
+
return "far";
|
|
567
|
+
}
|
|
568
|
+
return "horizon";
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function normalizeClothFeatureAdapters(clothFeatures) {
|
|
573
|
+
const fallback = createFallbackClothFeatureAdapters();
|
|
574
|
+
if (clothFeatures == null || typeof clothFeatures !== "object") {
|
|
575
|
+
return fallback;
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
garmentKinds: Array.isArray(clothFeatures.garmentKinds) && clothFeatures.garmentKinds.length > 0 ? clothFeatures.garmentKinds : fallback.garmentKinds,
|
|
579
|
+
profileNames: Array.isArray(clothFeatures.profileNames) && clothFeatures.profileNames.length > 0 ? clothFeatures.profileNames : fallback.profileNames,
|
|
580
|
+
createPlan: assertRequiredFunction(clothFeatures, "cloth", "createPlan"),
|
|
581
|
+
selectBand: assertRequiredFunction(clothFeatures, "cloth", "selectBand")
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function normalizeFluidFeatureAdapters(fluidFeatures) {
|
|
585
|
+
const fallback = createFallbackFluidFeatureAdapters();
|
|
586
|
+
if (fluidFeatures == null || typeof fluidFeatures !== "object") {
|
|
587
|
+
return fallback;
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
bodyKinds: Array.isArray(fluidFeatures.bodyKinds) && fluidFeatures.bodyKinds.length > 0 ? fluidFeatures.bodyKinds : fallback.bodyKinds,
|
|
591
|
+
profileNames: Array.isArray(fluidFeatures.profileNames) && fluidFeatures.profileNames.length > 0 ? fluidFeatures.profileNames : fallback.profileNames,
|
|
592
|
+
createContinuityEnvelope: assertRequiredFunction(
|
|
593
|
+
fluidFeatures,
|
|
594
|
+
"fluid",
|
|
595
|
+
"createContinuityEnvelope"
|
|
596
|
+
),
|
|
597
|
+
createPlan: assertRequiredFunction(fluidFeatures, "fluid", "createPlan"),
|
|
598
|
+
selectBand: assertRequiredFunction(fluidFeatures, "fluid", "selectBand")
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function assertRequiredFunction(module, featureLabel, exportName) {
|
|
602
|
+
const value = module?.[exportName];
|
|
603
|
+
if (typeof value !== "function") {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`Showcase ${featureLabel} feature package must export "${exportName}" as a function.`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return value;
|
|
609
|
+
}
|
|
610
|
+
function assertRequiredArray(module, featureLabel, exportName) {
|
|
611
|
+
const value = module?.[exportName];
|
|
612
|
+
if (!Array.isArray(value)) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
`Showcase ${featureLabel} feature package must export "${exportName}" as an array.`
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
return value;
|
|
618
|
+
}
|
|
619
|
+
async function loadShowcaseFeatureModule(featureLabel, loader) {
|
|
620
|
+
try {
|
|
621
|
+
const module = await loader();
|
|
622
|
+
if (module == null || typeof module !== "object") {
|
|
623
|
+
throw new Error("module is missing or not an object.");
|
|
624
|
+
}
|
|
625
|
+
return module;
|
|
626
|
+
} catch (error) {
|
|
627
|
+
const message = error?.message ?? String(error);
|
|
628
|
+
throw new Error(`Unable to load showcase ${featureLabel} feature package: ${message}`, {
|
|
629
|
+
cause: error
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function resolveShowcaseFeatureLoaders(options = {}) {
|
|
634
|
+
const overrides = options.__showcaseFeatureLoaders;
|
|
635
|
+
return {
|
|
636
|
+
cloth: typeof overrides?.cloth === "function" ? overrides.cloth : SHOWCASE_FEATURE_LOADERS.cloth,
|
|
637
|
+
fluid: typeof overrides?.fluid === "function" ? overrides.fluid : SHOWCASE_FEATURE_LOADERS.fluid,
|
|
638
|
+
lighting: typeof overrides?.lighting === "function" ? overrides.lighting : SHOWCASE_FEATURE_LOADERS.lighting,
|
|
639
|
+
performance: typeof overrides?.performance === "function" ? overrides.performance : SHOWCASE_FEATURE_LOADERS.performance,
|
|
640
|
+
debug: typeof overrides?.debug === "function" ? overrides.debug : SHOWCASE_FEATURE_LOADERS.debug,
|
|
641
|
+
physics: typeof overrides?.physics === "function" ? overrides.physics : SHOWCASE_FEATURE_LOADERS.physics
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
async function resolveShowcaseFeatureAdapters(options = {}) {
|
|
645
|
+
const loaders = resolveShowcaseFeatureLoaders(options);
|
|
646
|
+
const [
|
|
647
|
+
clothModule,
|
|
648
|
+
fluidModule,
|
|
649
|
+
lightingModule,
|
|
650
|
+
performanceModule,
|
|
651
|
+
debugModule,
|
|
652
|
+
physicsModule
|
|
653
|
+
] = await Promise.all([
|
|
654
|
+
loadShowcaseFeatureModule("cloth", loaders.cloth),
|
|
655
|
+
loadShowcaseFeatureModule("fluid", loaders.fluid),
|
|
656
|
+
loadShowcaseFeatureModule("lighting", loaders.lighting),
|
|
657
|
+
loadShowcaseFeatureModule("performance", loaders.performance),
|
|
658
|
+
loadShowcaseFeatureModule("debug", loaders.debug),
|
|
659
|
+
loadShowcaseFeatureModule("physics", loaders.physics)
|
|
660
|
+
]);
|
|
661
|
+
return {
|
|
662
|
+
cloth: {
|
|
663
|
+
garmentKinds: assertRequiredArray(clothModule, "cloth", "clothGarmentKinds"),
|
|
664
|
+
profileNames: assertRequiredArray(clothModule, "cloth", "clothProfileNames"),
|
|
665
|
+
createPlan: assertRequiredFunction(clothModule, "cloth", "createClothRepresentationPlan"),
|
|
666
|
+
selectBand: assertRequiredFunction(clothModule, "cloth", "selectClothRepresentationBand")
|
|
667
|
+
},
|
|
668
|
+
fluid: {
|
|
669
|
+
bodyKinds: assertRequiredArray(fluidModule, "fluid", "fluidBodyKinds"),
|
|
670
|
+
profileNames: assertRequiredArray(fluidModule, "fluid", "fluidProfileNames"),
|
|
671
|
+
createContinuityEnvelope: assertRequiredFunction(
|
|
672
|
+
fluidModule,
|
|
673
|
+
"fluid",
|
|
674
|
+
"createFluidContinuityEnvelope"
|
|
675
|
+
),
|
|
676
|
+
createPlan: assertRequiredFunction(fluidModule, "fluid", "createFluidRepresentationPlan"),
|
|
677
|
+
selectBand: assertRequiredFunction(fluidModule, "fluid", "selectFluidRepresentationBand")
|
|
678
|
+
},
|
|
679
|
+
lighting: {
|
|
680
|
+
createBandPlan: assertRequiredFunction(lightingModule, "lighting", "createLightingBandPlan"),
|
|
681
|
+
defaultProfile: lightingModule.defaultLightingProfile,
|
|
682
|
+
getProfile: assertRequiredFunction(lightingModule, "lighting", "getLightingProfile"),
|
|
683
|
+
distanceBands: assertRequiredArray(lightingModule, "lighting", "lightingDistanceBands")
|
|
684
|
+
},
|
|
685
|
+
performance: {
|
|
686
|
+
createDeviceProfile: assertRequiredFunction(
|
|
687
|
+
performanceModule,
|
|
688
|
+
"performance",
|
|
689
|
+
"createDeviceProfile"
|
|
690
|
+
),
|
|
691
|
+
createGovernor: assertRequiredFunction(
|
|
692
|
+
performanceModule,
|
|
693
|
+
"performance",
|
|
694
|
+
"createGpuPerformanceGovernor"
|
|
695
|
+
),
|
|
696
|
+
createQualityAdapter: assertRequiredFunction(
|
|
697
|
+
performanceModule,
|
|
698
|
+
"performance",
|
|
699
|
+
"createQualityLadderAdapter"
|
|
700
|
+
)
|
|
701
|
+
},
|
|
702
|
+
debug: {
|
|
703
|
+
createSession: assertRequiredFunction(debugModule, "debug", "createGpuDebugSession")
|
|
704
|
+
},
|
|
705
|
+
physics: {
|
|
706
|
+
createSimulationPlan: assertRequiredFunction(
|
|
707
|
+
physicsModule,
|
|
708
|
+
"physics",
|
|
709
|
+
"createPhysicsSimulationPlan"
|
|
710
|
+
),
|
|
711
|
+
createWorldSnapshot: assertRequiredFunction(
|
|
712
|
+
physicsModule,
|
|
713
|
+
"physics",
|
|
714
|
+
"createPhysicsWorldSnapshot"
|
|
715
|
+
),
|
|
716
|
+
defaultProfile: physicsModule.defaultPhysicsWorkerProfile,
|
|
717
|
+
getManifest: assertRequiredFunction(physicsModule, "physics", "getPhysicsWorkerManifest")
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
var showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
|
|
722
|
+
var FOCUS_MODE_TRANSLATION_KEYS = Object.freeze({
|
|
723
|
+
integrated: gpuSharedTranslationKeys.focusIntegrated,
|
|
724
|
+
lighting: gpuSharedTranslationKeys.focusLighting,
|
|
725
|
+
cloth: gpuSharedTranslationKeys.focusCloth,
|
|
726
|
+
fluid: gpuSharedTranslationKeys.focusFluid,
|
|
727
|
+
physics: gpuSharedTranslationKeys.focusPhysics,
|
|
728
|
+
performance: gpuSharedTranslationKeys.focusPerformance,
|
|
729
|
+
debug: gpuSharedTranslationKeys.focusDebug
|
|
730
|
+
});
|
|
731
|
+
var SCENE_NOTE_KEYS = Object.freeze([
|
|
732
|
+
gpuSharedTranslationKeys.noteAssetLoading,
|
|
733
|
+
gpuSharedTranslationKeys.noteMoonlight,
|
|
734
|
+
gpuSharedTranslationKeys.noteContinuity,
|
|
735
|
+
gpuSharedTranslationKeys.notePerformance
|
|
736
|
+
]);
|
|
737
|
+
var PHYSICS_SCENE_NOTE_KEYS = Object.freeze([
|
|
738
|
+
gpuSharedTranslationKeys.notePhysicsSnapshots,
|
|
739
|
+
gpuSharedTranslationKeys.notePhysicsCollisions,
|
|
740
|
+
gpuSharedTranslationKeys.notePhysicsLighting
|
|
741
|
+
]);
|
|
742
|
+
var LEGACY_HARBOR_LAYOUT = Object.freeze([
|
|
743
|
+
Object.freeze({
|
|
744
|
+
position: Object.freeze({ x: -8.2, y: 1.1, z: -0.9 }),
|
|
745
|
+
rotationY: -0.16,
|
|
746
|
+
scale: 5.4,
|
|
747
|
+
color: { r: 0.32, g: 0.27, b: 0.23, a: 1 },
|
|
748
|
+
accent: 0.06
|
|
749
|
+
}),
|
|
750
|
+
Object.freeze({
|
|
751
|
+
position: Object.freeze({ x: -5.7, y: 0.45, z: 1.4 }),
|
|
752
|
+
rotationY: -0.08,
|
|
753
|
+
scale: { x: 6.8, y: 0.3, z: 2.1 },
|
|
754
|
+
color: { r: 0.31, g: 0.31, b: 0.34, a: 1 },
|
|
755
|
+
accent: 0.04
|
|
756
|
+
}),
|
|
757
|
+
Object.freeze({
|
|
758
|
+
position: Object.freeze({ x: -10.4, y: 0.28, z: 0.8 }),
|
|
759
|
+
rotationY: 0.22,
|
|
760
|
+
scale: { x: 1.2, y: 0.9, z: 1.2 },
|
|
761
|
+
color: { r: 0.31, g: 0.35, b: 0.39, a: 1 },
|
|
762
|
+
accent: 0.02
|
|
763
|
+
})
|
|
764
|
+
]);
|
|
765
|
+
var SHOWCASE_ENVIRONMENT_LAYOUT = Object.freeze([
|
|
766
|
+
Object.freeze({
|
|
767
|
+
assetKey: "harbor-dock",
|
|
768
|
+
position: Object.freeze({ x: -4.6, y: 0.16, z: 0.7 }),
|
|
769
|
+
rotationY: -0.08,
|
|
770
|
+
scale: 0.84,
|
|
771
|
+
accent: 0.04
|
|
772
|
+
}),
|
|
773
|
+
Object.freeze({
|
|
774
|
+
assetKey: "lighthouse",
|
|
775
|
+
position: Object.freeze({ x: -9.8, y: 0, z: -0.58 }),
|
|
776
|
+
rotationY: 0.12,
|
|
777
|
+
scale: 0.56,
|
|
778
|
+
accent: 0.08
|
|
779
|
+
})
|
|
780
|
+
]);
|
|
781
|
+
var SHIP_LANTERNS = Object.freeze([
|
|
782
|
+
Object.freeze({ x: 0.94, y: 1.54, z: 2.52, glow: 1 }),
|
|
783
|
+
Object.freeze({ x: -0.9, y: 1.58, z: 2.44, glow: 0.92 }),
|
|
784
|
+
Object.freeze({ x: 0.62, y: 1.42, z: -2.18, glow: 0.88 }),
|
|
785
|
+
Object.freeze({ x: -0.58, y: 1.46, z: -2.04, glow: 0.84 })
|
|
786
|
+
]);
|
|
787
|
+
var CUTTER_LANTERNS = Object.freeze([
|
|
788
|
+
Object.freeze({ x: 0.42, y: 1.04, z: 1.18, glow: 0.94 }),
|
|
789
|
+
Object.freeze({ x: -0.42, y: 1.04, z: 1.12, glow: 0.88 })
|
|
790
|
+
]);
|
|
791
|
+
var HARBOR_TORCHES = Object.freeze([
|
|
792
|
+
Object.freeze({ x: -5.2, y: 1.25, z: 1.36, glow: 1.1 }),
|
|
793
|
+
Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
|
|
794
|
+
Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 })
|
|
795
|
+
]);
|
|
796
|
+
var FLAG_LAYOUT = Object.freeze({
|
|
797
|
+
origin: Object.freeze({ x: -3.5, y: 5.9, z: 2.4 }),
|
|
798
|
+
width: 4.8,
|
|
799
|
+
height: 2.7,
|
|
800
|
+
mastOffsetX: 1.8
|
|
801
|
+
});
|
|
802
|
+
function injectStyles() {
|
|
803
|
+
if (document.getElementById(STYLE_ID)) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const style = document.createElement("style");
|
|
807
|
+
style.id = STYLE_ID;
|
|
808
|
+
style.textContent = `
|
|
809
|
+
.${ROOT_CLASS} {
|
|
810
|
+
color-scheme: dark;
|
|
811
|
+
--plasius-paper: #081321;
|
|
812
|
+
--plasius-ink: #edf4ff;
|
|
813
|
+
--plasius-muted: #b6c5dd;
|
|
814
|
+
--plasius-accent: #f3b16a;
|
|
815
|
+
--plasius-panel: rgba(8, 19, 33, 0.72);
|
|
816
|
+
--plasius-border: rgba(159, 185, 223, 0.18);
|
|
817
|
+
--plasius-shadow: 0 24px 56px rgba(1, 6, 14, 0.44);
|
|
818
|
+
margin: 0;
|
|
819
|
+
min-height: 100%;
|
|
820
|
+
font-family: "Fraunces", "Iowan Old Style", serif;
|
|
821
|
+
color: var(--plasius-ink);
|
|
822
|
+
background:
|
|
823
|
+
radial-gradient(circle at 18% 12%, rgba(73, 101, 170, 0.28), transparent 30%),
|
|
824
|
+
radial-gradient(circle at 82% 18%, rgba(240, 188, 103, 0.08), transparent 18%),
|
|
825
|
+
linear-gradient(180deg, #04101d 0%, #0b1930 42%, #081321 100%);
|
|
826
|
+
}
|
|
827
|
+
.${ROOT_CLASS}.${CAPTURE_CLASS} {
|
|
828
|
+
min-height: 100vh;
|
|
829
|
+
overflow: hidden;
|
|
830
|
+
background: #030710;
|
|
831
|
+
}
|
|
832
|
+
.${ROOT_CLASS},
|
|
833
|
+
.${ROOT_CLASS} * {
|
|
834
|
+
box-sizing: border-box;
|
|
835
|
+
}
|
|
836
|
+
.plasius-demo {
|
|
837
|
+
width: min(1560px, calc(100vw - 32px));
|
|
838
|
+
margin: 0 auto;
|
|
839
|
+
padding: 28px 0 40px;
|
|
840
|
+
display: grid;
|
|
841
|
+
gap: 20px;
|
|
842
|
+
}
|
|
843
|
+
.plasius-demo__hero,
|
|
844
|
+
.plasius-demo__layout {
|
|
845
|
+
display: grid;
|
|
846
|
+
gap: 20px;
|
|
847
|
+
}
|
|
848
|
+
.plasius-demo__hero {
|
|
849
|
+
grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.85fr);
|
|
850
|
+
align-items: start;
|
|
851
|
+
}
|
|
852
|
+
.plasius-panel {
|
|
853
|
+
border: 1px solid var(--plasius-border);
|
|
854
|
+
border-radius: 24px;
|
|
855
|
+
background: var(--plasius-panel);
|
|
856
|
+
box-shadow: var(--plasius-shadow);
|
|
857
|
+
backdrop-filter: blur(12px);
|
|
858
|
+
}
|
|
859
|
+
.plasius-demo__hero-card,
|
|
860
|
+
.plasius-demo__status {
|
|
861
|
+
padding: 20px 22px;
|
|
862
|
+
}
|
|
863
|
+
.plasius-demo__eyebrow {
|
|
864
|
+
margin: 0 0 8px;
|
|
865
|
+
text-transform: uppercase;
|
|
866
|
+
letter-spacing: 0.18em;
|
|
867
|
+
font-size: 12px;
|
|
868
|
+
color: rgba(226, 236, 255, 0.58);
|
|
869
|
+
}
|
|
870
|
+
.plasius-demo h1,
|
|
871
|
+
.plasius-demo h2,
|
|
872
|
+
.plasius-demo h3 {
|
|
873
|
+
margin: 0;
|
|
874
|
+
}
|
|
875
|
+
.plasius-demo__lead {
|
|
876
|
+
margin: 12px 0 0;
|
|
877
|
+
color: var(--plasius-muted);
|
|
878
|
+
line-height: 1.6;
|
|
879
|
+
max-width: 760px;
|
|
880
|
+
}
|
|
881
|
+
.plasius-demo__status-badge {
|
|
882
|
+
width: fit-content;
|
|
883
|
+
margin: 0;
|
|
884
|
+
padding: 8px 12px;
|
|
885
|
+
border-radius: 999px;
|
|
886
|
+
background: rgba(243, 177, 106, 0.14);
|
|
887
|
+
color: var(--plasius-accent);
|
|
888
|
+
font-weight: 700;
|
|
889
|
+
}
|
|
890
|
+
.plasius-demo__status-text {
|
|
891
|
+
margin: 10px 0 0;
|
|
892
|
+
color: var(--plasius-muted);
|
|
893
|
+
line-height: 1.6;
|
|
894
|
+
}
|
|
895
|
+
.plasius-demo__layout {
|
|
896
|
+
grid-template-columns: minmax(0, 1.45fr) minmax(320px, 0.68fr);
|
|
897
|
+
align-items: start;
|
|
898
|
+
}
|
|
899
|
+
.plasius-demo__canvas-panel {
|
|
900
|
+
padding: 18px;
|
|
901
|
+
position: relative;
|
|
902
|
+
}
|
|
903
|
+
.plasius-demo__canvas {
|
|
904
|
+
width: 100%;
|
|
905
|
+
aspect-ratio: 16 / 9;
|
|
906
|
+
display: block;
|
|
907
|
+
border-radius: 20px;
|
|
908
|
+
border: 1px solid rgba(159, 185, 223, 0.12);
|
|
909
|
+
background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
|
|
910
|
+
}
|
|
911
|
+
.${CAPTURE_CLASS} .plasius-demo {
|
|
912
|
+
width: 100vw;
|
|
913
|
+
height: 100vh;
|
|
914
|
+
padding: 0;
|
|
915
|
+
display: block;
|
|
916
|
+
}
|
|
917
|
+
.${CAPTURE_CLASS} .plasius-demo__hero,
|
|
918
|
+
.${CAPTURE_CLASS} .plasius-demo__toolbar,
|
|
919
|
+
.${CAPTURE_CLASS} .plasius-demo__legend,
|
|
920
|
+
.${CAPTURE_CLASS} .plasius-demo__sidebar,
|
|
921
|
+
.${CAPTURE_CLASS} .plasius-demo__footer {
|
|
922
|
+
display: none;
|
|
923
|
+
}
|
|
924
|
+
.${CAPTURE_CLASS} .plasius-demo__layout {
|
|
925
|
+
display: block;
|
|
926
|
+
height: 100%;
|
|
927
|
+
}
|
|
928
|
+
.${CAPTURE_CLASS} .plasius-demo__canvas-panel {
|
|
929
|
+
height: 100%;
|
|
930
|
+
padding: 0;
|
|
931
|
+
border: 0;
|
|
932
|
+
border-radius: 0;
|
|
933
|
+
background: transparent;
|
|
934
|
+
box-shadow: none;
|
|
935
|
+
backdrop-filter: none;
|
|
936
|
+
}
|
|
937
|
+
.${CAPTURE_CLASS} .plasius-demo__canvas {
|
|
938
|
+
width: 100%;
|
|
939
|
+
height: 100%;
|
|
940
|
+
aspect-ratio: auto;
|
|
941
|
+
border: 0;
|
|
942
|
+
border-radius: 0;
|
|
943
|
+
background: #030710;
|
|
944
|
+
}
|
|
945
|
+
.plasius-demo__toolbar {
|
|
946
|
+
position: absolute;
|
|
947
|
+
top: 26px;
|
|
948
|
+
left: 26px;
|
|
949
|
+
display: flex;
|
|
950
|
+
gap: 12px;
|
|
951
|
+
flex-wrap: wrap;
|
|
952
|
+
align-items: center;
|
|
953
|
+
}
|
|
954
|
+
.plasius-demo button,
|
|
955
|
+
.plasius-demo label,
|
|
956
|
+
.plasius-demo select {
|
|
957
|
+
font-family: "JetBrains Mono", monospace;
|
|
958
|
+
font-size: 13px;
|
|
959
|
+
}
|
|
960
|
+
.plasius-demo button,
|
|
961
|
+
.plasius-demo .plasius-toggle,
|
|
962
|
+
.plasius-demo select {
|
|
963
|
+
border: 1px solid rgba(159, 185, 223, 0.18);
|
|
964
|
+
border-radius: 999px;
|
|
965
|
+
background: rgba(9, 20, 34, 0.84);
|
|
966
|
+
color: var(--plasius-ink);
|
|
967
|
+
padding: 10px 14px;
|
|
968
|
+
}
|
|
969
|
+
.plasius-toggle {
|
|
970
|
+
display: inline-flex;
|
|
971
|
+
align-items: center;
|
|
972
|
+
gap: 8px;
|
|
973
|
+
}
|
|
974
|
+
.plasius-demo__sidebar {
|
|
975
|
+
display: grid;
|
|
976
|
+
gap: 18px;
|
|
977
|
+
}
|
|
978
|
+
.plasius-demo__card {
|
|
979
|
+
padding: 18px;
|
|
980
|
+
}
|
|
981
|
+
.plasius-demo__metrics,
|
|
982
|
+
.plasius-demo__metrics li {
|
|
983
|
+
margin: 0;
|
|
984
|
+
padding: 0;
|
|
985
|
+
list-style: none;
|
|
986
|
+
}
|
|
987
|
+
.plasius-demo__metrics {
|
|
988
|
+
margin-top: 12px;
|
|
989
|
+
display: grid;
|
|
990
|
+
gap: 8px;
|
|
991
|
+
color: var(--plasius-muted);
|
|
992
|
+
line-height: 1.55;
|
|
993
|
+
}
|
|
994
|
+
.plasius-demo__metrics li {
|
|
995
|
+
border-top: 1px solid rgba(21, 32, 40, 0.08);
|
|
996
|
+
padding-top: 8px;
|
|
997
|
+
}
|
|
998
|
+
.plasius-demo__legend {
|
|
999
|
+
position: absolute;
|
|
1000
|
+
right: 24px;
|
|
1001
|
+
bottom: 24px;
|
|
1002
|
+
padding: 10px 14px;
|
|
1003
|
+
border-radius: 16px;
|
|
1004
|
+
background: rgba(9, 20, 34, 0.82);
|
|
1005
|
+
border: 1px solid rgba(159, 185, 223, 0.16);
|
|
1006
|
+
color: var(--plasius-muted);
|
|
1007
|
+
font-size: 12px;
|
|
1008
|
+
line-height: 1.45;
|
|
1009
|
+
}
|
|
1010
|
+
.plasius-demo__legend strong {
|
|
1011
|
+
display: block;
|
|
1012
|
+
color: var(--plasius-ink);
|
|
1013
|
+
margin-bottom: 4px;
|
|
1014
|
+
}
|
|
1015
|
+
.plasius-demo__footer {
|
|
1016
|
+
margin-top: 4px;
|
|
1017
|
+
color: rgba(226, 236, 255, 0.68);
|
|
1018
|
+
font-size: 13px;
|
|
1019
|
+
line-height: 1.6;
|
|
1020
|
+
}
|
|
1021
|
+
@media (max-width: 1200px) {
|
|
1022
|
+
.plasius-demo__hero,
|
|
1023
|
+
.plasius-demo__layout {
|
|
1024
|
+
grid-template-columns: 1fr;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
`;
|
|
1028
|
+
document.head.appendChild(style);
|
|
1029
|
+
}
|
|
1030
|
+
function clamp(value, min, max) {
|
|
1031
|
+
return Math.max(min, Math.min(max, value));
|
|
1032
|
+
}
|
|
1033
|
+
function mix(a, b, t) {
|
|
1034
|
+
return a + (b - a) * t;
|
|
1035
|
+
}
|
|
1036
|
+
function smoothstep(min, max, value) {
|
|
1037
|
+
const t = clamp((value - min) / Math.max(1e-4, max - min), 0, 1);
|
|
1038
|
+
return t * t * (3 - 2 * t);
|
|
1039
|
+
}
|
|
1040
|
+
function pseudoRandom(seed) {
|
|
1041
|
+
const value = Math.sin(seed * 12.9898 + seed * seed * 17e-4) * 43758.5453;
|
|
1042
|
+
return value - Math.floor(value);
|
|
1043
|
+
}
|
|
1044
|
+
function vec3(x = 0, y = 0, z = 0) {
|
|
1045
|
+
return { x, y, z };
|
|
1046
|
+
}
|
|
1047
|
+
function addVec3(a, b) {
|
|
1048
|
+
return vec3(a.x + b.x, a.y + b.y, a.z + b.z);
|
|
1049
|
+
}
|
|
1050
|
+
function subVec3(a, b) {
|
|
1051
|
+
return vec3(a.x - b.x, a.y - b.y, a.z - b.z);
|
|
1052
|
+
}
|
|
1053
|
+
function scaleVec3(a, s) {
|
|
1054
|
+
return vec3(a.x * s, a.y * s, a.z * s);
|
|
1055
|
+
}
|
|
1056
|
+
function dotVec3(a, b) {
|
|
1057
|
+
return a.x * b.x + a.y * b.y + a.z * b.z;
|
|
1058
|
+
}
|
|
1059
|
+
function crossVec3(a, b) {
|
|
1060
|
+
return vec3(
|
|
1061
|
+
a.y * b.z - a.z * b.y,
|
|
1062
|
+
a.z * b.x - a.x * b.z,
|
|
1063
|
+
a.x * b.y - a.y * b.x
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
function lengthVec3(a) {
|
|
1067
|
+
return Math.hypot(a.x, a.y, a.z);
|
|
1068
|
+
}
|
|
1069
|
+
function normalizeVec3(a) {
|
|
1070
|
+
const length = lengthVec3(a) || 1;
|
|
1071
|
+
return vec3(a.x / length, a.y / length, a.z / length);
|
|
1072
|
+
}
|
|
1073
|
+
function reflectVec3(vector, normal) {
|
|
1074
|
+
const unitNormal = normalizeVec3(normal);
|
|
1075
|
+
return subVec3(vector, scaleVec3(unitNormal, 2 * dotVec3(vector, unitNormal)));
|
|
1076
|
+
}
|
|
1077
|
+
function directionFromYaw(yaw) {
|
|
1078
|
+
return normalizeVec3(vec3(Math.sin(yaw), 0, Math.cos(yaw)));
|
|
1079
|
+
}
|
|
1080
|
+
function perpendicularOnWater(direction) {
|
|
1081
|
+
return vec3(-direction.z, 0, direction.x);
|
|
1082
|
+
}
|
|
1083
|
+
function rotateY(point, angle) {
|
|
1084
|
+
const cosine = Math.cos(angle);
|
|
1085
|
+
const sine = Math.sin(angle);
|
|
1086
|
+
return vec3(
|
|
1087
|
+
point.x * cosine - point.z * sine,
|
|
1088
|
+
point.y,
|
|
1089
|
+
point.x * sine + point.z * cosine
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
function transformPoint(point, transform) {
|
|
1093
|
+
const scale = typeof transform.scale === "number" ? { x: transform.scale, y: transform.scale, z: transform.scale } : transform.scale;
|
|
1094
|
+
const scaled = vec3(point.x * scale.x, point.y * scale.y, point.z * scale.z);
|
|
1095
|
+
const rotated = rotateY(scaled, transform.rotationY);
|
|
1096
|
+
return addVec3(rotated, transform.position);
|
|
1097
|
+
}
|
|
1098
|
+
function transformDirection(direction, transform) {
|
|
1099
|
+
const scale = typeof transform.scale === "number" ? { x: transform.scale, y: transform.scale, z: transform.scale } : transform.scale;
|
|
1100
|
+
const scaled = vec3(direction.x * scale.x, direction.y * scale.y, direction.z * scale.z);
|
|
1101
|
+
return normalizeVec3(rotateY(scaled, transform.rotationY));
|
|
1102
|
+
}
|
|
1103
|
+
function projectPoint(point, camera, viewport) {
|
|
1104
|
+
const relative = subVec3(point, camera.eye);
|
|
1105
|
+
const viewX = dotVec3(relative, camera.right);
|
|
1106
|
+
const viewY = dotVec3(relative, camera.up);
|
|
1107
|
+
const viewZ = dotVec3(relative, camera.forward);
|
|
1108
|
+
if (viewZ <= 0.1) {
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
const focal = 1 / Math.tan(camera.fov * Math.PI / 360);
|
|
1112
|
+
const ndcX = viewX * focal / (viewZ * camera.aspect);
|
|
1113
|
+
const ndcY = viewY * focal / viewZ;
|
|
1114
|
+
return {
|
|
1115
|
+
x: (ndcX * 0.5 + 0.5) * viewport.width,
|
|
1116
|
+
y: (-ndcY * 0.5 + 0.5) * viewport.height,
|
|
1117
|
+
depth: viewZ
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
function colorToRgba(color, alpha = 1) {
|
|
1121
|
+
const r = Math.round(clamp(color.r, 0, 1) * 255);
|
|
1122
|
+
const g = Math.round(clamp(color.g, 0, 1) * 255);
|
|
1123
|
+
const b = Math.round(clamp(color.b, 0, 1) * 255);
|
|
1124
|
+
return `rgba(${r}, ${g}, ${b}, ${clamp(alpha, 0, 1)})`;
|
|
1125
|
+
}
|
|
1126
|
+
function mixColor(a, b, t) {
|
|
1127
|
+
return {
|
|
1128
|
+
r: mix(a.r, b.r, t),
|
|
1129
|
+
g: mix(a.g, b.g, t),
|
|
1130
|
+
b: mix(a.b, b.b, t),
|
|
1131
|
+
a: mix(a.a ?? 1, b.a ?? 1, t)
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function multiplyColor(a, b) {
|
|
1135
|
+
return {
|
|
1136
|
+
r: a.r * b.r,
|
|
1137
|
+
g: a.g * b.g,
|
|
1138
|
+
b: a.b * b.b,
|
|
1139
|
+
a: (a.a ?? 1) * (b.a ?? 1)
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function createLegacyMeshPrimitive(mesh) {
|
|
1143
|
+
return Object.freeze({
|
|
1144
|
+
name: mesh.name ?? "legacy-mesh",
|
|
1145
|
+
positions: mesh.positions,
|
|
1146
|
+
indices: mesh.indices,
|
|
1147
|
+
normals: null,
|
|
1148
|
+
colors: null,
|
|
1149
|
+
material: Object.freeze({
|
|
1150
|
+
name: "legacy-material",
|
|
1151
|
+
color: mesh.color ?? { r: 0.56, g: 0.33, b: 0.22, a: 1 },
|
|
1152
|
+
roughness: 0.88,
|
|
1153
|
+
metallic: 0.08,
|
|
1154
|
+
emissive: Object.freeze({ r: 0, g: 0, b: 0 })
|
|
1155
|
+
})
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
function isFeatureEnabled(featureFlags, featureName, fallback = true) {
|
|
1159
|
+
const directValue = typeof featureFlags?.[featureName] === "boolean" ? featureFlags[featureName] : featureFlags?.flags?.[featureName];
|
|
1160
|
+
if (typeof directValue === "boolean") {
|
|
1161
|
+
return directValue;
|
|
1162
|
+
}
|
|
1163
|
+
const enabledValue = typeof featureFlags?.enabled?.[featureName] === "boolean" ? featureFlags.enabled[featureName] : void 0;
|
|
1164
|
+
if (typeof enabledValue === "boolean") {
|
|
1165
|
+
return enabledValue;
|
|
1166
|
+
}
|
|
1167
|
+
return fallback;
|
|
1168
|
+
}
|
|
1169
|
+
function getMeshPrimitives(mesh) {
|
|
1170
|
+
return Array.isArray(mesh?.primitives) && mesh.primitives.length > 0 ? mesh.primitives : [createLegacyMeshPrimitive(mesh)];
|
|
1171
|
+
}
|
|
1172
|
+
function tintPrimitiveColor(material, colorOverride) {
|
|
1173
|
+
if (!colorOverride) {
|
|
1174
|
+
return material.color;
|
|
1175
|
+
}
|
|
1176
|
+
const name = String(material.name ?? "").toLowerCase();
|
|
1177
|
+
if (name.includes("sail") || name.includes("glass") || name.includes("roof")) {
|
|
1178
|
+
return material.color;
|
|
1179
|
+
}
|
|
1180
|
+
const tintAmount = name.includes("hull") ? 0.54 : name.includes("trim") ? 0.22 : name.includes("deck") ? 0.12 : 0;
|
|
1181
|
+
if (tintAmount <= 0) {
|
|
1182
|
+
return material.color;
|
|
1183
|
+
}
|
|
1184
|
+
return mixColor(material.color, multiplyColor(material.color, colorOverride), tintAmount);
|
|
1185
|
+
}
|
|
1186
|
+
function projectShadowPoint(point, lightDir, planeY) {
|
|
1187
|
+
const shadowDir = scaleVec3(lightDir, -1);
|
|
1188
|
+
if (Math.abs(shadowDir.y) < 1e-4) {
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
const distance = (planeY - point.y) / shadowDir.y;
|
|
1192
|
+
if (!Number.isFinite(distance) || distance < 0) {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
return addVec3(point, scaleVec3(shadowDir, distance));
|
|
1196
|
+
}
|
|
1197
|
+
function shadeColor(base, normal, lightDir, heightBias = 0, accent = 0) {
|
|
1198
|
+
const diffuse = clamp(dotVec3(normalizeVec3(normal), lightDir), 0, 1);
|
|
1199
|
+
const brightness = 0.24 + diffuse * 0.72 + heightBias * 0.08 + accent;
|
|
1200
|
+
return {
|
|
1201
|
+
r: clamp(base.r * brightness, 0, 1),
|
|
1202
|
+
g: clamp(base.g * brightness, 0, 1),
|
|
1203
|
+
b: clamp(base.b * (brightness + 0.03), 0, 1)
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
function getMaterialSeed(materialName) {
|
|
1207
|
+
let seed = 0;
|
|
1208
|
+
for (let index = 0; index < materialName.length; index += 1) {
|
|
1209
|
+
seed += materialName.charCodeAt(index) * (index + 1);
|
|
1210
|
+
}
|
|
1211
|
+
return seed;
|
|
1212
|
+
}
|
|
1213
|
+
function getMaterialDetailStrength(material, surfaceType) {
|
|
1214
|
+
const name = String(material?.name ?? "").toLowerCase();
|
|
1215
|
+
if (surfaceType === "water" || name.includes("glass")) {
|
|
1216
|
+
return 0.018;
|
|
1217
|
+
}
|
|
1218
|
+
if (name.includes("wood") || name.includes("timber") || name.includes("plank")) {
|
|
1219
|
+
return 0.13;
|
|
1220
|
+
}
|
|
1221
|
+
if (name.includes("stone") || name.includes("concrete") || name.includes("plaster")) {
|
|
1222
|
+
return 0.1;
|
|
1223
|
+
}
|
|
1224
|
+
if (name.includes("roof") || name.includes("crate")) {
|
|
1225
|
+
return 0.09;
|
|
1226
|
+
}
|
|
1227
|
+
if (name.includes("paint")) {
|
|
1228
|
+
return 0.045;
|
|
1229
|
+
}
|
|
1230
|
+
if (name.includes("metal")) {
|
|
1231
|
+
return 0.035;
|
|
1232
|
+
}
|
|
1233
|
+
return 0.04;
|
|
1234
|
+
}
|
|
1235
|
+
function applyMaterialDetail(color, material, worldCenter, normal, surfaceType) {
|
|
1236
|
+
const materialName = String(material?.name ?? surfaceType ?? "material");
|
|
1237
|
+
const detailStrength = getMaterialDetailStrength(material, surfaceType);
|
|
1238
|
+
const sample = worldCenter.x * 3.17 + worldCenter.y * 5.29 + worldCenter.z * 7.83 + getMaterialSeed(materialName) * 0.013;
|
|
1239
|
+
const grain = (pseudoRandom(sample) - 0.5) * detailStrength;
|
|
1240
|
+
const lowerSurface = smoothstep(7.5, -0.8, worldCenter.y);
|
|
1241
|
+
const verticalSurface = 1 - clamp(Math.abs(normal.y), 0, 1);
|
|
1242
|
+
const materialLowerWear = /stone|concrete|plaster|paint|wood|timber|plank|crate/.test(materialName.toLowerCase()) ? lowerSurface * verticalSurface * 0.055 : 0;
|
|
1243
|
+
const wetlineWear = surfaceType === "ship" && worldCenter.y < 0.72 ? smoothstep(0.72, -0.1, worldCenter.y) * 0.05 : 0;
|
|
1244
|
+
return {
|
|
1245
|
+
r: clamp(color.r * (1 + grain) - materialLowerWear - wetlineWear, 0, 1),
|
|
1246
|
+
g: clamp(color.g * (1 + grain * 0.82) - materialLowerWear * 0.9 - wetlineWear, 0, 1),
|
|
1247
|
+
b: clamp(color.b * (1 + grain * 0.62) - materialLowerWear * 0.68 - wetlineWear * 0.75, 0, 1)
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function buildCamera(state, canvas) {
|
|
1251
|
+
const preset = CAMERA_PRESETS[state.focus] ?? CAMERA_PRESETS.integrated;
|
|
1252
|
+
const yaw = state.camera.yaw ?? preset.yaw;
|
|
1253
|
+
const pitch = state.camera.pitch ?? preset.pitch;
|
|
1254
|
+
const distance = state.camera.distance ?? preset.distance;
|
|
1255
|
+
const target = state.camera.target ?? vec3(...preset.target);
|
|
1256
|
+
const eye = vec3(
|
|
1257
|
+
target.x + Math.sin(yaw) * Math.cos(pitch) * distance,
|
|
1258
|
+
target.y + Math.sin(pitch) * distance,
|
|
1259
|
+
target.z + Math.cos(yaw) * Math.cos(pitch) * distance
|
|
1260
|
+
);
|
|
1261
|
+
const forward = normalizeVec3(subVec3(target, eye));
|
|
1262
|
+
const right = normalizeVec3(crossVec3(forward, vec3(0, 1, 0)));
|
|
1263
|
+
const up = normalizeVec3(crossVec3(right, forward));
|
|
1264
|
+
return {
|
|
1265
|
+
eye,
|
|
1266
|
+
target,
|
|
1267
|
+
forward,
|
|
1268
|
+
right,
|
|
1269
|
+
up,
|
|
1270
|
+
fov: 54,
|
|
1271
|
+
aspect: canvas.width / canvas.height
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function buildTrianglesFromMesh(mesh, transform, colorOverride, camera, viewport, triangles, options = {}) {
|
|
1275
|
+
const primitives = getMeshPrimitives(mesh);
|
|
1276
|
+
for (const primitive of primitives) {
|
|
1277
|
+
const resolvedColor = tintPrimitiveColor(primitive.material, colorOverride);
|
|
1278
|
+
for (let index = 0; index < primitive.indices.length; index += 3) {
|
|
1279
|
+
const aIndex = primitive.indices[index] * 3;
|
|
1280
|
+
const bIndex = primitive.indices[index + 1] * 3;
|
|
1281
|
+
const cIndex = primitive.indices[index + 2] * 3;
|
|
1282
|
+
const a = transformPoint(
|
|
1283
|
+
vec3(
|
|
1284
|
+
primitive.positions[aIndex],
|
|
1285
|
+
primitive.positions[aIndex + 1],
|
|
1286
|
+
primitive.positions[aIndex + 2]
|
|
1287
|
+
),
|
|
1288
|
+
transform
|
|
1289
|
+
);
|
|
1290
|
+
const b = transformPoint(
|
|
1291
|
+
vec3(
|
|
1292
|
+
primitive.positions[bIndex],
|
|
1293
|
+
primitive.positions[bIndex + 1],
|
|
1294
|
+
primitive.positions[bIndex + 2]
|
|
1295
|
+
),
|
|
1296
|
+
transform
|
|
1297
|
+
);
|
|
1298
|
+
const c = transformPoint(
|
|
1299
|
+
vec3(
|
|
1300
|
+
primitive.positions[cIndex],
|
|
1301
|
+
primitive.positions[cIndex + 1],
|
|
1302
|
+
primitive.positions[cIndex + 2]
|
|
1303
|
+
),
|
|
1304
|
+
transform
|
|
1305
|
+
);
|
|
1306
|
+
const ab = subVec3(b, a);
|
|
1307
|
+
const ac = subVec3(c, a);
|
|
1308
|
+
const faceNormal = normalizeVec3(crossVec3(ab, ac));
|
|
1309
|
+
let normal = faceNormal;
|
|
1310
|
+
if (Array.isArray(primitive.normals)) {
|
|
1311
|
+
const aNormal = transformDirection(
|
|
1312
|
+
vec3(
|
|
1313
|
+
primitive.normals[aIndex],
|
|
1314
|
+
primitive.normals[aIndex + 1],
|
|
1315
|
+
primitive.normals[aIndex + 2]
|
|
1316
|
+
),
|
|
1317
|
+
transform
|
|
1318
|
+
);
|
|
1319
|
+
const bNormal = transformDirection(
|
|
1320
|
+
vec3(
|
|
1321
|
+
primitive.normals[bIndex],
|
|
1322
|
+
primitive.normals[bIndex + 1],
|
|
1323
|
+
primitive.normals[bIndex + 2]
|
|
1324
|
+
),
|
|
1325
|
+
transform
|
|
1326
|
+
);
|
|
1327
|
+
const cNormal = transformDirection(
|
|
1328
|
+
vec3(
|
|
1329
|
+
primitive.normals[cIndex],
|
|
1330
|
+
primitive.normals[cIndex + 1],
|
|
1331
|
+
primitive.normals[cIndex + 2]
|
|
1332
|
+
),
|
|
1333
|
+
transform
|
|
1334
|
+
);
|
|
1335
|
+
normal = normalizeVec3(
|
|
1336
|
+
scaleVec3(addVec3(addVec3(aNormal, bNormal), cNormal), 1 / 3)
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
const viewDir = normalizeVec3(subVec3(camera.eye, a));
|
|
1340
|
+
if (dotVec3(faceNormal, viewDir) <= 0) {
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
const projected = [
|
|
1344
|
+
projectPoint(a, camera, viewport),
|
|
1345
|
+
projectPoint(b, camera, viewport),
|
|
1346
|
+
projectPoint(c, camera, viewport)
|
|
1347
|
+
];
|
|
1348
|
+
if (projected.some((value) => value === null)) {
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
triangles.push({
|
|
1352
|
+
points: projected,
|
|
1353
|
+
depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
|
|
1354
|
+
worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
|
|
1355
|
+
normal,
|
|
1356
|
+
baseColor: resolvedColor,
|
|
1357
|
+
accent: options.accent ?? 0,
|
|
1358
|
+
material: primitive.material,
|
|
1359
|
+
reflection: options.reflection ?? 0,
|
|
1360
|
+
surfaceType: options.surfaceType ?? "solid"
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
async function loadShowcaseAssetCatalog() {
|
|
1366
|
+
const [brigantine, cutter, lighthouse, harborDock] = await Promise.all([
|
|
1367
|
+
loadGltfModel(resolveShowcaseAssetUrl("brigantine")),
|
|
1368
|
+
loadGltfModel(resolveShowcaseAssetUrl("cutter")),
|
|
1369
|
+
loadGltfModel(resolveShowcaseAssetUrl("lighthouse")),
|
|
1370
|
+
loadGltfModel(resolveShowcaseAssetUrl("harbor-dock"))
|
|
1371
|
+
]);
|
|
1372
|
+
return Object.freeze({
|
|
1373
|
+
primaryShipKey: "brigantine",
|
|
1374
|
+
ships: Object.freeze({
|
|
1375
|
+
brigantine,
|
|
1376
|
+
cutter
|
|
1377
|
+
}),
|
|
1378
|
+
environment: Object.freeze({
|
|
1379
|
+
lighthouse,
|
|
1380
|
+
"harbor-dock": harborDock
|
|
1381
|
+
})
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
function createLegacyShowcaseAssetCatalog() {
|
|
1385
|
+
const brigantine = loadGltfModel(resolveShowcaseAssetUrl("brigantine"));
|
|
1386
|
+
return Promise.resolve(brigantine).then(
|
|
1387
|
+
(primary) => Object.freeze({
|
|
1388
|
+
primaryShipKey: "brigantine",
|
|
1389
|
+
ships: Object.freeze({
|
|
1390
|
+
brigantine: primary
|
|
1391
|
+
}),
|
|
1392
|
+
environment: Object.freeze({})
|
|
1393
|
+
})
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
function resolveShipModel(state, ship, fallbackModel = null) {
|
|
1397
|
+
return state.assetCatalog?.ships?.[ship.modelKey ?? state.assetCatalog?.primaryShipKey ?? "brigantine"] ?? fallbackModel ?? state.shipModel;
|
|
1398
|
+
}
|
|
1399
|
+
function createPerformanceGovernor(performanceFeatures) {
|
|
1400
|
+
const createQualityLadderAdapter = assertRequiredFunction(
|
|
1401
|
+
performanceFeatures,
|
|
1402
|
+
"performance",
|
|
1403
|
+
"createQualityAdapter"
|
|
1404
|
+
);
|
|
1405
|
+
const createDeviceProfile = assertRequiredFunction(
|
|
1406
|
+
performanceFeatures,
|
|
1407
|
+
"performance",
|
|
1408
|
+
"createDeviceProfile"
|
|
1409
|
+
);
|
|
1410
|
+
const createGpuPerformanceGovernor = assertRequiredFunction(
|
|
1411
|
+
performanceFeatures,
|
|
1412
|
+
"performance",
|
|
1413
|
+
"createGovernor"
|
|
1414
|
+
);
|
|
1415
|
+
const fluidDetail = createQualityLadderAdapter({
|
|
1416
|
+
id: "fluid-detail",
|
|
1417
|
+
domain: "geometry",
|
|
1418
|
+
levels: [
|
|
1419
|
+
{ id: "low", config: { nearResolution: 10, midResolution: 6, splashCount: 10 }, estimatedCostMs: 0.8 },
|
|
1420
|
+
{ id: "medium", config: { nearResolution: 16, midResolution: 8, splashCount: 18 }, estimatedCostMs: 1.4 },
|
|
1421
|
+
{ id: "high", config: { nearResolution: 24, midResolution: 12, splashCount: 28 }, estimatedCostMs: 2.4 }
|
|
1422
|
+
],
|
|
1423
|
+
initialLevel: "high"
|
|
1424
|
+
});
|
|
1425
|
+
const clothDetail = createQualityLadderAdapter({
|
|
1426
|
+
id: "cloth-detail",
|
|
1427
|
+
domain: "cloth",
|
|
1428
|
+
levels: [
|
|
1429
|
+
{ id: "low", config: { cols: 10, rows: 7 }, estimatedCostMs: 0.7 },
|
|
1430
|
+
{ id: "medium", config: { cols: 16, rows: 11 }, estimatedCostMs: 1.3 },
|
|
1431
|
+
{ id: "high", config: { cols: 24, rows: 16 }, estimatedCostMs: 2.1 }
|
|
1432
|
+
],
|
|
1433
|
+
initialLevel: "high"
|
|
1434
|
+
});
|
|
1435
|
+
const lightingDetail = createQualityLadderAdapter({
|
|
1436
|
+
id: "lighting-detail",
|
|
1437
|
+
domain: "lighting",
|
|
1438
|
+
levels: [
|
|
1439
|
+
{ id: "low", config: { shadowStrength: 0.18, reflectionStrength: 0.08 }, estimatedCostMs: 0.5 },
|
|
1440
|
+
{ id: "medium", config: { shadowStrength: 0.34, reflectionStrength: 0.16 }, estimatedCostMs: 1 },
|
|
1441
|
+
{ id: "high", config: { shadowStrength: 0.5, reflectionStrength: 0.24 }, estimatedCostMs: 1.8 }
|
|
1442
|
+
],
|
|
1443
|
+
initialLevel: "high"
|
|
1444
|
+
});
|
|
1445
|
+
const governor = createGpuPerformanceGovernor({
|
|
1446
|
+
device: createDeviceProfile({
|
|
1447
|
+
deviceClass: "desktop",
|
|
1448
|
+
mode: "flat",
|
|
1449
|
+
refreshRateHz: 60,
|
|
1450
|
+
supportedFrameRates: [60, 90],
|
|
1451
|
+
supportsWebGpu: true
|
|
1452
|
+
}),
|
|
1453
|
+
modules: [fluidDetail, clothDetail, lightingDetail],
|
|
1454
|
+
adaptation: {
|
|
1455
|
+
sampleWindowSize: 10,
|
|
1456
|
+
minimumSamplesBeforeAdjustment: 4,
|
|
1457
|
+
degradeCooldownFrames: 1,
|
|
1458
|
+
upgradeCooldownFrames: 4,
|
|
1459
|
+
minStableFramesForRecovery: 3
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
return { governor, fluidDetail, clothDetail, lightingDetail };
|
|
1463
|
+
}
|
|
1464
|
+
function buildDemoDom(root, options) {
|
|
1465
|
+
const t = options.translate;
|
|
1466
|
+
root.innerHTML = `
|
|
1467
|
+
<main class="plasius-demo">
|
|
1468
|
+
<section class="plasius-demo__hero">
|
|
1469
|
+
<section class="plasius-panel plasius-demo__hero-card">
|
|
1470
|
+
<p class="plasius-demo__eyebrow">${options.packageName}</p>
|
|
1471
|
+
<h1>${options.title}</h1>
|
|
1472
|
+
<p class="plasius-demo__lead">${options.subtitle}</p>
|
|
1473
|
+
</section>
|
|
1474
|
+
<section class="plasius-panel plasius-demo__status">
|
|
1475
|
+
<p id="demoStatus" class="plasius-demo__status-badge">${t(gpuSharedTranslationKeys.statusBooting)}</p>
|
|
1476
|
+
<p id="demoDetails" class="plasius-demo__status-text">
|
|
1477
|
+
${t(gpuSharedTranslationKeys.detailsBooting)}
|
|
1478
|
+
</p>
|
|
1479
|
+
</section>
|
|
1480
|
+
</section>
|
|
1481
|
+
<section class="plasius-demo__layout">
|
|
1482
|
+
<section class="plasius-panel plasius-demo__canvas-panel">
|
|
1483
|
+
<canvas id="demoCanvas" class="plasius-demo__canvas" width="${DEFAULT_CANVAS_WIDTH}" height="${DEFAULT_CANVAS_HEIGHT}"></canvas>
|
|
1484
|
+
<div class="plasius-demo__toolbar">
|
|
1485
|
+
<button id="pauseButton" type="button">${t(gpuSharedTranslationKeys.pause)}</button>
|
|
1486
|
+
<label class="plasius-toggle">
|
|
1487
|
+
<input id="stressToggle" type="checkbox" />
|
|
1488
|
+
${t(gpuSharedTranslationKeys.stressMode)}
|
|
1489
|
+
</label>
|
|
1490
|
+
<label class="plasius-toggle">
|
|
1491
|
+
${t(gpuSharedTranslationKeys.focus)}
|
|
1492
|
+
<select id="focusMode">
|
|
1493
|
+
${showcaseFocusModes.map(
|
|
1494
|
+
(mode) => `<option value="${mode}">${t(FOCUS_MODE_TRANSLATION_KEYS[mode])}</option>`
|
|
1495
|
+
).join("")}
|
|
1496
|
+
</select>
|
|
1497
|
+
</label>
|
|
1498
|
+
</div>
|
|
1499
|
+
<div class="plasius-demo__legend">
|
|
1500
|
+
<strong>${t(gpuSharedTranslationKeys.legendTitle)}</strong>
|
|
1501
|
+
${t(gpuSharedTranslationKeys.legendShipMetadata)}<br />
|
|
1502
|
+
${t(gpuSharedTranslationKeys.legendLighting)}<br />
|
|
1503
|
+
${t(gpuSharedTranslationKeys.legendCollisions)}
|
|
1504
|
+
</div>
|
|
1505
|
+
</section>
|
|
1506
|
+
<aside class="plasius-demo__sidebar">
|
|
1507
|
+
<section class="plasius-panel plasius-demo__card">
|
|
1508
|
+
<h2>${t(gpuSharedTranslationKeys.sceneState)}</h2>
|
|
1509
|
+
<ul id="sceneMetrics" class="plasius-demo__metrics"></ul>
|
|
1510
|
+
</section>
|
|
1511
|
+
<section class="plasius-panel plasius-demo__card">
|
|
1512
|
+
<h2>${t(gpuSharedTranslationKeys.qualityBudgets)}</h2>
|
|
1513
|
+
<ul id="qualityMetrics" class="plasius-demo__metrics"></ul>
|
|
1514
|
+
</section>
|
|
1515
|
+
<section class="plasius-panel plasius-demo__card">
|
|
1516
|
+
<h2>${t(gpuSharedTranslationKeys.debugTelemetry)}</h2>
|
|
1517
|
+
<ul id="debugMetrics" class="plasius-demo__metrics"></ul>
|
|
1518
|
+
</section>
|
|
1519
|
+
<section class="plasius-panel plasius-demo__card">
|
|
1520
|
+
<h2>${t(gpuSharedTranslationKeys.notes)}</h2>
|
|
1521
|
+
<ul id="sceneNotes" class="plasius-demo__metrics"></ul>
|
|
1522
|
+
</section>
|
|
1523
|
+
</aside>
|
|
1524
|
+
</section>
|
|
1525
|
+
<p class="plasius-demo__footer">
|
|
1526
|
+
This visual example is shared across the GPU packages to keep manual validation fast and consistent.
|
|
1527
|
+
</p>
|
|
1528
|
+
</main>
|
|
1529
|
+
`;
|
|
1530
|
+
return {
|
|
1531
|
+
status: root.querySelector("#demoStatus"),
|
|
1532
|
+
details: root.querySelector("#demoDetails"),
|
|
1533
|
+
canvas: root.querySelector("#demoCanvas"),
|
|
1534
|
+
pauseButton: root.querySelector("#pauseButton"),
|
|
1535
|
+
stressToggle: root.querySelector("#stressToggle"),
|
|
1536
|
+
focusMode: root.querySelector("#focusMode"),
|
|
1537
|
+
sceneMetrics: root.querySelector("#sceneMetrics"),
|
|
1538
|
+
qualityMetrics: root.querySelector("#qualityMetrics"),
|
|
1539
|
+
debugMetrics: root.querySelector("#debugMetrics"),
|
|
1540
|
+
sceneNotes: root.querySelector("#sceneNotes")
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
function buildSceneSnapshot(state, shipModel) {
|
|
1544
|
+
const shipPhysics = Object.freeze(
|
|
1545
|
+
Object.fromEntries(
|
|
1546
|
+
state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
|
|
1547
|
+
)
|
|
1548
|
+
);
|
|
1549
|
+
return Object.freeze({
|
|
1550
|
+
focus: state.focus,
|
|
1551
|
+
frame: state.frame,
|
|
1552
|
+
time: state.time,
|
|
1553
|
+
stress: state.stress,
|
|
1554
|
+
collisions: state.contactCount,
|
|
1555
|
+
collisionCount: state.collisionCount,
|
|
1556
|
+
collisionFlash: state.collisionFlash,
|
|
1557
|
+
sprays: Object.freeze(
|
|
1558
|
+
state.sprays.map(
|
|
1559
|
+
(spray) => Object.freeze({
|
|
1560
|
+
life: spray.life,
|
|
1561
|
+
position: Object.freeze({ ...spray.position }),
|
|
1562
|
+
velocity: Object.freeze({ ...spray.velocity })
|
|
1563
|
+
})
|
|
1564
|
+
)
|
|
1565
|
+
),
|
|
1566
|
+
ships: Object.freeze(
|
|
1567
|
+
state.ships.map(
|
|
1568
|
+
(ship) => Object.freeze({
|
|
1569
|
+
id: ship.id,
|
|
1570
|
+
modelKey: ship.modelKey ?? "brigantine",
|
|
1571
|
+
position: Object.freeze({ ...ship.position }),
|
|
1572
|
+
velocity: Object.freeze({ ...ship.velocity }),
|
|
1573
|
+
rotationY: ship.rotationY,
|
|
1574
|
+
angularVelocity: ship.angularVelocity,
|
|
1575
|
+
tint: Object.freeze({ ...ship.tint })
|
|
1576
|
+
})
|
|
1577
|
+
)
|
|
1578
|
+
),
|
|
1579
|
+
waveImpulses: Object.freeze(
|
|
1580
|
+
state.waveImpulses.map(
|
|
1581
|
+
(impulse) => Object.freeze({
|
|
1582
|
+
x: impulse.x,
|
|
1583
|
+
z: impulse.z,
|
|
1584
|
+
strength: impulse.strength,
|
|
1585
|
+
radius: impulse.radius,
|
|
1586
|
+
life: impulse.life
|
|
1587
|
+
})
|
|
1588
|
+
)
|
|
1589
|
+
),
|
|
1590
|
+
shipPhysics: shipModel?.physics ?? null,
|
|
1591
|
+
shipModels: shipPhysics,
|
|
1592
|
+
physics: Object.freeze({
|
|
1593
|
+
profile: state.physics.profile,
|
|
1594
|
+
plan: state.physics.plan,
|
|
1595
|
+
manifest: state.physics.manifest,
|
|
1596
|
+
snapshot: state.physics.snapshot,
|
|
1597
|
+
shipPhysics
|
|
1598
|
+
})
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
function resolveSceneDescription(state, options, shipModel) {
|
|
1602
|
+
const scene = buildSceneSnapshot(state, shipModel);
|
|
1603
|
+
if (typeof options.describeState !== "function") {
|
|
1604
|
+
return { scene, description: null };
|
|
1605
|
+
}
|
|
1606
|
+
const description = options.describeState(state.packageState, scene) ?? null;
|
|
1607
|
+
return { scene, description };
|
|
1608
|
+
}
|
|
1609
|
+
function updatePackageState(state, options, shipModel, dt) {
|
|
1610
|
+
if (typeof options.updateState !== "function") {
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const scene = buildSceneSnapshot(state, shipModel);
|
|
1614
|
+
const nextState = options.updateState(state.packageState, scene, dt);
|
|
1615
|
+
if (typeof nextState !== "undefined") {
|
|
1616
|
+
state.packageState = nextState;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
function normalizeColorOverride(color, fallback) {
|
|
1620
|
+
if (!color || typeof color !== "object") {
|
|
1621
|
+
return fallback;
|
|
1622
|
+
}
|
|
1623
|
+
return {
|
|
1624
|
+
r: typeof color.r === "number" ? color.r : fallback.r,
|
|
1625
|
+
g: typeof color.g === "number" ? color.g : fallback.g,
|
|
1626
|
+
b: typeof color.b === "number" ? color.b : fallback.b
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
function readVisualNumber(value, fallback) {
|
|
1630
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1631
|
+
}
|
|
1632
|
+
function readPositiveNumber(value, fallback) {
|
|
1633
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1634
|
+
}
|
|
1635
|
+
function isTruthyCaptureValue(value) {
|
|
1636
|
+
return value === "1" || value === "true" || value === "scene" || value === "video";
|
|
1637
|
+
}
|
|
1638
|
+
function resolveCaptureSettings(options) {
|
|
1639
|
+
const explicitCaptureMode = typeof options.captureMode === "boolean" ? options.captureMode : void 0;
|
|
1640
|
+
let captureMode = explicitCaptureMode ?? false;
|
|
1641
|
+
let renderScale = readPositiveNumber(options.renderScale, void 0);
|
|
1642
|
+
try {
|
|
1643
|
+
const params = new URLSearchParams(window.location.search);
|
|
1644
|
+
if (explicitCaptureMode === void 0) {
|
|
1645
|
+
captureMode = isTruthyCaptureValue(params.get("capture")) || params.get("presentation") === "capture";
|
|
1646
|
+
}
|
|
1647
|
+
renderScale = readPositiveNumber(Number(params.get("renderScale")), renderScale);
|
|
1648
|
+
} catch {
|
|
1649
|
+
}
|
|
1650
|
+
return {
|
|
1651
|
+
captureMode,
|
|
1652
|
+
renderScale
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function getCanvasDisplaySize(canvas) {
|
|
1656
|
+
const rect = typeof canvas.getBoundingClientRect === "function" ? canvas.getBoundingClientRect() : null;
|
|
1657
|
+
const width = Math.round(
|
|
1658
|
+
readPositiveNumber(rect?.width, readPositiveNumber(canvas.clientWidth, canvas.width))
|
|
1659
|
+
);
|
|
1660
|
+
const height = Math.round(
|
|
1661
|
+
readPositiveNumber(rect?.height, readPositiveNumber(canvas.clientHeight, canvas.height))
|
|
1662
|
+
);
|
|
1663
|
+
return {
|
|
1664
|
+
width: Math.max(1, width || DEFAULT_CANVAS_WIDTH),
|
|
1665
|
+
height: Math.max(1, height || DEFAULT_CANVAS_HEIGHT)
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function resizeCanvasToDisplaySize(canvas, state) {
|
|
1669
|
+
const { width, height } = getCanvasDisplaySize(canvas);
|
|
1670
|
+
const deviceScale = readPositiveNumber(globalThis.devicePixelRatio, 1);
|
|
1671
|
+
const requestedScale = readPositiveNumber(state.renderScale, deviceScale);
|
|
1672
|
+
const maxScale = state.captureMode ? 2 : 1.5;
|
|
1673
|
+
let scale = clamp(requestedScale, 1, maxScale);
|
|
1674
|
+
const pixelBudget = state.captureMode ? CAPTURE_CANVAS_PIXEL_BUDGET : DEFAULT_CANVAS_WIDTH * DEFAULT_CANVAS_HEIGHT * 1.5;
|
|
1675
|
+
const projectedPixels = width * height * scale * scale;
|
|
1676
|
+
if (projectedPixels > pixelBudget) {
|
|
1677
|
+
scale = Math.sqrt(pixelBudget / Math.max(1, width * height));
|
|
1678
|
+
}
|
|
1679
|
+
const targetWidth = Math.max(1, Math.round(width * scale));
|
|
1680
|
+
const targetHeight = Math.max(1, Math.round(height * scale));
|
|
1681
|
+
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
|
1682
|
+
canvas.width = targetWidth;
|
|
1683
|
+
canvas.height = targetHeight;
|
|
1684
|
+
}
|
|
1685
|
+
state.renderScale = scale;
|
|
1686
|
+
}
|
|
1687
|
+
function resolveClothPresentation(state, meshDetail, clothFeatures) {
|
|
1688
|
+
const clothPlan = clothFeatures.createPlan({
|
|
1689
|
+
garmentId: "shore-flag",
|
|
1690
|
+
kind: state.focus === "cloth" ? "flag" : clothFeatures.garmentKinds[0],
|
|
1691
|
+
profile: state.focus === "cloth" ? "cinematic" : clothFeatures.profileNames[0],
|
|
1692
|
+
supportsRayTracing: true,
|
|
1693
|
+
nearFieldMaxMeters: 18,
|
|
1694
|
+
midFieldMaxMeters: 55,
|
|
1695
|
+
farFieldMaxMeters: 180
|
|
1696
|
+
});
|
|
1697
|
+
const preset = CAMERA_PRESETS[state.focus] ?? CAMERA_PRESETS.integrated;
|
|
1698
|
+
const fallbackEye = state.camera.eye ? state.camera.eye : addVec3(
|
|
1699
|
+
state.camera.target,
|
|
1700
|
+
vec3(
|
|
1701
|
+
Math.sin(state.camera.yaw ?? preset.yaw) * Math.cos(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance),
|
|
1702
|
+
Math.sin(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance),
|
|
1703
|
+
Math.cos(state.camera.yaw ?? preset.yaw) * Math.cos(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance)
|
|
1704
|
+
)
|
|
1705
|
+
);
|
|
1706
|
+
const cameraDistance = lengthVec3(subVec3(state.camera.target, fallbackEye));
|
|
1707
|
+
const band = clothFeatures.selectBand(cameraDistance, clothPlan.thresholds);
|
|
1708
|
+
const representation = clothPlan.representations.find((entry) => entry.band === band) ?? clothPlan.representations[0];
|
|
1709
|
+
return {
|
|
1710
|
+
clothPlan,
|
|
1711
|
+
band,
|
|
1712
|
+
continuity: representation.continuity,
|
|
1713
|
+
representation
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
function getFlagRestPosition(rows, cols, row, column) {
|
|
1717
|
+
const u = cols <= 1 ? 0 : column / (cols - 1);
|
|
1718
|
+
const v = rows <= 1 ? 0 : row / (rows - 1);
|
|
1719
|
+
return vec3(
|
|
1720
|
+
FLAG_LAYOUT.origin.x + u * FLAG_LAYOUT.mastOffsetX,
|
|
1721
|
+
FLAG_LAYOUT.origin.y - FLAG_LAYOUT.height * v - u * u * 0.08,
|
|
1722
|
+
FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * u
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
function buildClothConstraints(rows, cols, restPositions) {
|
|
1726
|
+
const constraints = [];
|
|
1727
|
+
const indexFor = (row, column) => row * cols + column;
|
|
1728
|
+
const pushConstraint = (a, b, stiffness) => {
|
|
1729
|
+
constraints.push(
|
|
1730
|
+
Object.freeze({
|
|
1731
|
+
a,
|
|
1732
|
+
b,
|
|
1733
|
+
restLength: lengthVec3(subVec3(restPositions[a], restPositions[b])),
|
|
1734
|
+
stiffness
|
|
1735
|
+
})
|
|
1736
|
+
);
|
|
1737
|
+
};
|
|
1738
|
+
for (let row = 0; row < rows; row += 1) {
|
|
1739
|
+
for (let column = 0; column < cols; column += 1) {
|
|
1740
|
+
const index = indexFor(row, column);
|
|
1741
|
+
if (column + 1 < cols) {
|
|
1742
|
+
pushConstraint(index, indexFor(row, column + 1), 0.92);
|
|
1743
|
+
}
|
|
1744
|
+
if (row + 1 < rows) {
|
|
1745
|
+
pushConstraint(index, indexFor(row + 1, column), 0.9);
|
|
1746
|
+
}
|
|
1747
|
+
if (column + 1 < cols && row + 1 < rows) {
|
|
1748
|
+
pushConstraint(index, indexFor(row + 1, column + 1), 0.66);
|
|
1749
|
+
}
|
|
1750
|
+
if (column - 1 >= 0 && row + 1 < rows) {
|
|
1751
|
+
pushConstraint(index, indexFor(row + 1, column - 1), 0.66);
|
|
1752
|
+
}
|
|
1753
|
+
if (column + 2 < cols) {
|
|
1754
|
+
pushConstraint(index, indexFor(row, column + 2), 0.22);
|
|
1755
|
+
}
|
|
1756
|
+
if (row + 2 < rows) {
|
|
1757
|
+
pushConstraint(index, indexFor(row + 2, column), 0.18);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return Object.freeze(constraints);
|
|
1762
|
+
}
|
|
1763
|
+
function createShowcaseClothSimulationState(options = {}) {
|
|
1764
|
+
const rows = Math.max(4, options.rows ?? 11);
|
|
1765
|
+
const cols = Math.max(4, options.cols ?? 16);
|
|
1766
|
+
const continuity = options.continuity ?? {
|
|
1767
|
+
broadMotionFloor: 0.72,
|
|
1768
|
+
wrinkleFloor: 0.56
|
|
1769
|
+
};
|
|
1770
|
+
const representation = options.representation ?? {
|
|
1771
|
+
mesh: {
|
|
1772
|
+
solverIterations: 6,
|
|
1773
|
+
wrinkleLayers: 2
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
const restPositions = [];
|
|
1777
|
+
const positions = [];
|
|
1778
|
+
const previousPositions = [];
|
|
1779
|
+
const uvs = [];
|
|
1780
|
+
const phaseOffsets = [];
|
|
1781
|
+
const pinned = [];
|
|
1782
|
+
for (let row = 0; row < rows; row += 1) {
|
|
1783
|
+
for (let column = 0; column < cols; column += 1) {
|
|
1784
|
+
const index = row * cols + column;
|
|
1785
|
+
const u = cols <= 1 ? 0 : column / (cols - 1);
|
|
1786
|
+
const v = rows <= 1 ? 0 : row / (rows - 1);
|
|
1787
|
+
const rest = getFlagRestPosition(rows, cols, row, column);
|
|
1788
|
+
const preload = vec3(
|
|
1789
|
+
u * 0.04,
|
|
1790
|
+
Math.sin(v * Math.PI) * 0.02 * continuity.wrinkleFloor,
|
|
1791
|
+
-u * 0.12
|
|
1792
|
+
);
|
|
1793
|
+
const pinnedPoint = column === 0;
|
|
1794
|
+
restPositions.push(rest);
|
|
1795
|
+
positions.push(pinnedPoint ? vec3(rest.x, rest.y, rest.z) : addVec3(rest, preload));
|
|
1796
|
+
previousPositions.push(
|
|
1797
|
+
pinnedPoint ? vec3(rest.x, rest.y, rest.z) : addVec3(rest, scaleVec3(preload, 0.35))
|
|
1798
|
+
);
|
|
1799
|
+
uvs.push(Object.freeze({ u, v }));
|
|
1800
|
+
phaseOffsets.push(pseudoRandom(index + 17) * Math.PI * 2);
|
|
1801
|
+
pinned.push(pinnedPoint);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return {
|
|
1805
|
+
rows,
|
|
1806
|
+
cols,
|
|
1807
|
+
continuity,
|
|
1808
|
+
representation,
|
|
1809
|
+
restPositions,
|
|
1810
|
+
positions,
|
|
1811
|
+
previousPositions,
|
|
1812
|
+
constraints: buildClothConstraints(rows, cols, restPositions),
|
|
1813
|
+
indices: Object.freeze(
|
|
1814
|
+
Array.from({ length: (rows - 1) * (cols - 1) * 6 }, (_, listIndex) => listIndex).map((_, listIndex, source) => {
|
|
1815
|
+
if (listIndex >= source.length) {
|
|
1816
|
+
return 0;
|
|
1817
|
+
}
|
|
1818
|
+
const quadIndex = Math.floor(listIndex / 6);
|
|
1819
|
+
const quadColumn = quadIndex % (cols - 1);
|
|
1820
|
+
const quadRow = Math.floor(quadIndex / (cols - 1));
|
|
1821
|
+
const base = quadRow * cols + quadColumn;
|
|
1822
|
+
return [base, base + 1, base + cols + 1, base, base + cols + 1, base + cols][listIndex % 6];
|
|
1823
|
+
})
|
|
1824
|
+
),
|
|
1825
|
+
uvs,
|
|
1826
|
+
phaseOffsets,
|
|
1827
|
+
pinned
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
function resetPinnedClothPoints(clothState) {
|
|
1831
|
+
for (let index = 0; index < clothState.positions.length; index += 1) {
|
|
1832
|
+
if (!clothState.pinned[index]) {
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
const anchor = clothState.restPositions[index];
|
|
1836
|
+
clothState.positions[index] = vec3(anchor.x, anchor.y, anchor.z);
|
|
1837
|
+
clothState.previousPositions[index] = vec3(anchor.x, anchor.y, anchor.z);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
function satisfyClothConstraint(clothState, constraint) {
|
|
1841
|
+
const a = clothState.positions[constraint.a];
|
|
1842
|
+
const b = clothState.positions[constraint.b];
|
|
1843
|
+
const delta = subVec3(b, a);
|
|
1844
|
+
const distance = lengthVec3(delta);
|
|
1845
|
+
if (distance <= 1e-4) {
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const correctionScale = (distance - constraint.restLength) / distance * 0.5 * constraint.stiffness;
|
|
1849
|
+
const correction = scaleVec3(delta, correctionScale);
|
|
1850
|
+
if (!clothState.pinned[constraint.a]) {
|
|
1851
|
+
clothState.positions[constraint.a] = addVec3(a, correction);
|
|
1852
|
+
}
|
|
1853
|
+
if (!clothState.pinned[constraint.b]) {
|
|
1854
|
+
clothState.positions[constraint.b] = subVec3(b, correction);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
function advanceShowcaseClothSimulationState(clothState, options = {}) {
|
|
1858
|
+
const dt = clamp(options.dt ?? 1 / 60, 1 / 240, 1 / 18);
|
|
1859
|
+
const time = readVisualNumber(options.time, 0);
|
|
1860
|
+
const flagMotion = readVisualNumber(options.flagMotion, 0.92);
|
|
1861
|
+
const waveInfluence = readVisualNumber(options.waveInfluence, 0);
|
|
1862
|
+
const wrinkleLayers = Math.max(1, clothState.representation.mesh?.wrinkleLayers ?? 2);
|
|
1863
|
+
const solverIterations = clamp(
|
|
1864
|
+
Math.round(clothState.representation.mesh?.solverIterations ?? 6),
|
|
1865
|
+
2,
|
|
1866
|
+
10
|
|
1867
|
+
);
|
|
1868
|
+
for (let index = 0; index < clothState.positions.length; index += 1) {
|
|
1869
|
+
if (clothState.pinned[index]) {
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
const current = clothState.positions[index];
|
|
1873
|
+
const previous = clothState.previousPositions[index];
|
|
1874
|
+
const { u, v } = clothState.uvs[index];
|
|
1875
|
+
const phase = clothState.phaseOffsets[index];
|
|
1876
|
+
const broadMotion = clothState.continuity.broadMotionFloor;
|
|
1877
|
+
const wrinkleMotion = clothState.continuity.wrinkleFloor;
|
|
1878
|
+
const gustPhase = time * 2.1 + phase + u * 4.4 + v * 2.3;
|
|
1879
|
+
const wrinklePhase = time * 5.3 + phase * 0.72 + u * 9.6 + v * 7.1;
|
|
1880
|
+
const windDirection = normalizeVec3(
|
|
1881
|
+
vec3(
|
|
1882
|
+
0.18 + Math.sin(gustPhase) * (0.12 + broadMotion * 0.09),
|
|
1883
|
+
Math.cos(time * 1.4 + phase + v * 4.8) * 0.06 * wrinkleMotion,
|
|
1884
|
+
1 + Math.sin(gustPhase * 0.74) * 0.18
|
|
1885
|
+
)
|
|
1886
|
+
);
|
|
1887
|
+
const windStrength = (1.6 + broadMotion * 1.25 + wrinkleLayers * 0.12) * flagMotion * (0.44 + u * 1.14);
|
|
1888
|
+
const wrinkleForce = vec3(
|
|
1889
|
+
Math.sin(wrinklePhase) * 0.22 * wrinkleMotion * flagMotion,
|
|
1890
|
+
Math.cos(wrinklePhase * 0.7) * 0.08 * wrinkleMotion,
|
|
1891
|
+
Math.cos(wrinklePhase) * 0.14 * broadMotion * flagMotion
|
|
1892
|
+
);
|
|
1893
|
+
const acceleration = addVec3(
|
|
1894
|
+
vec3(0, -0.48 - u * 0.08, 0),
|
|
1895
|
+
addVec3(
|
|
1896
|
+
scaleVec3(windDirection, windStrength),
|
|
1897
|
+
addVec3(
|
|
1898
|
+
wrinkleForce,
|
|
1899
|
+
vec3(waveInfluence * (0.04 + u * 0.08), 0, waveInfluence * 0.16)
|
|
1900
|
+
)
|
|
1901
|
+
)
|
|
1902
|
+
);
|
|
1903
|
+
const inertia = scaleVec3(subVec3(current, previous), 0.987);
|
|
1904
|
+
const next = addVec3(addVec3(current, inertia), scaleVec3(acceleration, dt * dt));
|
|
1905
|
+
clothState.previousPositions[index] = vec3(current.x, current.y, current.z);
|
|
1906
|
+
clothState.positions[index] = next;
|
|
1907
|
+
}
|
|
1908
|
+
resetPinnedClothPoints(clothState);
|
|
1909
|
+
for (let iteration = 0; iteration < solverIterations; iteration += 1) {
|
|
1910
|
+
for (const constraint of clothState.constraints) {
|
|
1911
|
+
satisfyClothConstraint(clothState, constraint);
|
|
1912
|
+
}
|
|
1913
|
+
resetPinnedClothPoints(clothState);
|
|
1914
|
+
}
|
|
1915
|
+
return clothState;
|
|
1916
|
+
}
|
|
1917
|
+
function ensureShowcaseClothState(state, meshDetail, clothPresentation) {
|
|
1918
|
+
if (!state.clothState || state.clothState.rows !== meshDetail.rows || state.clothState.cols !== meshDetail.cols) {
|
|
1919
|
+
state.clothState = createShowcaseClothSimulationState({
|
|
1920
|
+
rows: meshDetail.rows,
|
|
1921
|
+
cols: meshDetail.cols,
|
|
1922
|
+
continuity: clothPresentation.continuity,
|
|
1923
|
+
representation: clothPresentation.representation
|
|
1924
|
+
});
|
|
1925
|
+
} else {
|
|
1926
|
+
state.clothState.continuity = clothPresentation.continuity;
|
|
1927
|
+
state.clothState.representation = clothPresentation.representation;
|
|
1928
|
+
}
|
|
1929
|
+
return state.clothState;
|
|
1930
|
+
}
|
|
1931
|
+
function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
|
|
1932
|
+
const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
|
|
1933
|
+
const defaults = {
|
|
1934
|
+
skyTop: premiumShadows ? "#040c1a" : "#06101f",
|
|
1935
|
+
skyMid: premiumShadows ? "#11203b" : "#152643",
|
|
1936
|
+
skyBottom: premiumShadows ? "#2f4468" : "#364d73",
|
|
1937
|
+
duskGlow: premiumShadows ? "rgba(116, 142, 201, 0.26)" : "rgba(104, 128, 188, 0.22)",
|
|
1938
|
+
seaTop: premiumShadows ? "#102946" : "#153050",
|
|
1939
|
+
seaMid: premiumShadows ? "#0a1d33" : "#0d2138",
|
|
1940
|
+
seaBottom: "#04101d",
|
|
1941
|
+
moonCore: "rgba(241, 246, 255, 0.98)",
|
|
1942
|
+
moonHalo: "rgba(167, 191, 255, 0.24)",
|
|
1943
|
+
moonReflection: "rgba(192, 214, 255, 0.22)",
|
|
1944
|
+
starColor: "rgba(232, 239, 255, 0.82)",
|
|
1945
|
+
ambientMist: "rgba(41, 63, 97, 0.16)",
|
|
1946
|
+
reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
|
|
1947
|
+
shadowAccent: lightingSnapshot.currentLevel.config.shadowStrength,
|
|
1948
|
+
waveAmplitude: 0.94,
|
|
1949
|
+
waveDirection: { x: 0.88, z: 0.28 },
|
|
1950
|
+
wavePhaseSpeed: 0.88,
|
|
1951
|
+
wakeStrength: 0.31,
|
|
1952
|
+
wakeLength: 18,
|
|
1953
|
+
collisionRippleStrength: 0.42,
|
|
1954
|
+
waterNear: { r: 0.08, g: 0.23, b: 0.33 },
|
|
1955
|
+
waterFar: { r: 0.18, g: 0.35, b: 0.49 },
|
|
1956
|
+
harborWall: { r: 0.26, g: 0.24, b: 0.28 },
|
|
1957
|
+
harborDeck: { r: 0.33, g: 0.22, b: 0.16 },
|
|
1958
|
+
harborTower: { r: 0.23, g: 0.24, b: 0.29 },
|
|
1959
|
+
flagColor: { r: 0.66, g: 0.16, b: 0.13 },
|
|
1960
|
+
flagMotion: 0.92,
|
|
1961
|
+
lanternCore: { r: 0.98, g: 0.8, b: 0.48 },
|
|
1962
|
+
lanternGlow: { r: 1, g: 0.56, b: 0.2 },
|
|
1963
|
+
lanternReflectionStrength: 0.42,
|
|
1964
|
+
torchCore: { r: 0.99, g: 0.72, b: 0.36 },
|
|
1965
|
+
torchGlow: { r: 0.98, g: 0.38, b: 0.15 },
|
|
1966
|
+
collisionFlash: "rgba(255, 212, 168, 0.16)"
|
|
1967
|
+
};
|
|
1968
|
+
return {
|
|
1969
|
+
skyTop: typeof customVisuals.skyTop === "string" ? customVisuals.skyTop : defaults.skyTop,
|
|
1970
|
+
skyMid: typeof customVisuals.skyMid === "string" ? customVisuals.skyMid : defaults.skyMid,
|
|
1971
|
+
skyBottom: typeof customVisuals.skyBottom === "string" ? customVisuals.skyBottom : defaults.skyBottom,
|
|
1972
|
+
seaTop: typeof customVisuals.seaTop === "string" ? customVisuals.seaTop : defaults.seaTop,
|
|
1973
|
+
seaMid: typeof customVisuals.seaMid === "string" ? customVisuals.seaMid : defaults.seaMid,
|
|
1974
|
+
seaBottom: typeof customVisuals.seaBottom === "string" ? customVisuals.seaBottom : defaults.seaBottom,
|
|
1975
|
+
duskGlow: typeof customVisuals.duskGlow === "string" ? customVisuals.duskGlow : defaults.duskGlow,
|
|
1976
|
+
moonCore: typeof customVisuals.moonCore === "string" ? customVisuals.moonCore : typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.moonCore,
|
|
1977
|
+
moonHalo: typeof customVisuals.moonHalo === "string" ? customVisuals.moonHalo : defaults.moonHalo,
|
|
1978
|
+
moonReflection: typeof customVisuals.moonReflection === "string" ? customVisuals.moonReflection : defaults.moonReflection,
|
|
1979
|
+
starColor: typeof customVisuals.starColor === "string" ? customVisuals.starColor : defaults.starColor,
|
|
1980
|
+
ambientMist: typeof customVisuals.ambientMist === "string" ? customVisuals.ambientMist : defaults.ambientMist,
|
|
1981
|
+
reflectionStrength: readVisualNumber(
|
|
1982
|
+
customVisuals.reflectionStrength,
|
|
1983
|
+
defaults.reflectionStrength
|
|
1984
|
+
),
|
|
1985
|
+
shadowAccent: readVisualNumber(customVisuals.shadowAccent, defaults.shadowAccent),
|
|
1986
|
+
waveAmplitude: readVisualNumber(customVisuals.waveAmplitude, defaults.waveAmplitude),
|
|
1987
|
+
waveDirection: customVisuals.waveDirection && typeof customVisuals.waveDirection.x === "number" && typeof customVisuals.waveDirection.z === "number" ? { x: customVisuals.waveDirection.x, z: customVisuals.waveDirection.z } : defaults.waveDirection,
|
|
1988
|
+
wavePhaseSpeed: readVisualNumber(customVisuals.wavePhaseSpeed, defaults.wavePhaseSpeed),
|
|
1989
|
+
wakeStrength: readVisualNumber(customVisuals.wakeStrength, defaults.wakeStrength),
|
|
1990
|
+
wakeLength: readVisualNumber(customVisuals.wakeLength, defaults.wakeLength),
|
|
1991
|
+
collisionRippleStrength: readVisualNumber(
|
|
1992
|
+
customVisuals.collisionRippleStrength,
|
|
1993
|
+
defaults.collisionRippleStrength
|
|
1994
|
+
),
|
|
1995
|
+
waterNear: normalizeColorOverride(customVisuals.waterNear, defaults.waterNear),
|
|
1996
|
+
waterFar: normalizeColorOverride(customVisuals.waterFar, defaults.waterFar),
|
|
1997
|
+
harborWall: normalizeColorOverride(customVisuals.harborWall, defaults.harborWall),
|
|
1998
|
+
harborDeck: normalizeColorOverride(customVisuals.harborDeck, defaults.harborDeck),
|
|
1999
|
+
harborTower: normalizeColorOverride(customVisuals.harborTower, defaults.harborTower),
|
|
2000
|
+
flagColor: normalizeColorOverride(customVisuals.flagColor, defaults.flagColor),
|
|
2001
|
+
flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion),
|
|
2002
|
+
lanternCore: normalizeColorOverride(customVisuals.lanternCore, defaults.lanternCore),
|
|
2003
|
+
lanternGlow: normalizeColorOverride(customVisuals.lanternGlow, defaults.lanternGlow),
|
|
2004
|
+
lanternReflectionStrength: readVisualNumber(
|
|
2005
|
+
customVisuals.lanternReflectionStrength,
|
|
2006
|
+
defaults.lanternReflectionStrength
|
|
2007
|
+
),
|
|
2008
|
+
torchCore: normalizeColorOverride(customVisuals.torchCore, defaults.torchCore),
|
|
2009
|
+
torchGlow: normalizeColorOverride(customVisuals.torchGlow, defaults.torchGlow),
|
|
2010
|
+
collisionFlash: typeof customVisuals.collisionFlash === "string" ? customVisuals.collisionFlash : defaults.collisionFlash
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
function buildClothSurface(model, state, meshDetail, visuals, clothFeatures) {
|
|
2014
|
+
const resolvedClothFeatures = normalizeClothFeatureAdapters(clothFeatures);
|
|
2015
|
+
const clothPresentation = resolveClothPresentation(state, meshDetail, resolvedClothFeatures);
|
|
2016
|
+
const clothState = ensureShowcaseClothState(state, meshDetail, clothPresentation);
|
|
2017
|
+
return {
|
|
2018
|
+
clothPlan: clothPresentation.clothPlan,
|
|
2019
|
+
band: clothPresentation.band,
|
|
2020
|
+
representation: clothPresentation.representation,
|
|
2021
|
+
continuity: clothPresentation.continuity,
|
|
2022
|
+
color: visuals.flagColor,
|
|
2023
|
+
positions: clothState.positions.map((point) => vec3(point.x, point.y, point.z)),
|
|
2024
|
+
indices: clothState.indices,
|
|
2025
|
+
grid: { rows: clothState.rows, cols: clothState.cols }
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
function resolveWaveDirection(state) {
|
|
2029
|
+
const direction = state.demoVisuals?.waveDirection;
|
|
2030
|
+
if (direction && typeof direction === "object" && typeof direction.x === "number" && typeof direction.z === "number") {
|
|
2031
|
+
return normalizeVec3(vec3(direction.x, 0, direction.z));
|
|
2032
|
+
}
|
|
2033
|
+
return normalizeVec3(vec3(0.86, 0, 0.34));
|
|
2034
|
+
}
|
|
2035
|
+
function sampleShipWake(state, x, z, time) {
|
|
2036
|
+
const wakeStrength = readVisualNumber(state.demoVisuals?.wakeStrength, 0.24);
|
|
2037
|
+
const wakeLength = readVisualNumber(state.demoVisuals?.wakeLength, 15);
|
|
2038
|
+
let total = 0;
|
|
2039
|
+
for (const ship of state.ships) {
|
|
2040
|
+
const speed = Math.hypot(ship.velocity.x, ship.velocity.z);
|
|
2041
|
+
if (speed <= 0.05) {
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
const direction = normalizeVec3(vec3(ship.velocity.x, 0, ship.velocity.z));
|
|
2045
|
+
const behind = scaleVec3(direction, -1);
|
|
2046
|
+
const lateral = vec3(-direction.z, 0, direction.x);
|
|
2047
|
+
const delta = vec3(x - ship.position.x, 0, z - ship.position.z);
|
|
2048
|
+
const along = dotVec3(delta, behind);
|
|
2049
|
+
if (along < 0 || along > wakeLength) {
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
const cross = Math.abs(dotVec3(delta, lateral));
|
|
2053
|
+
const width = 0.9 + along * 0.2;
|
|
2054
|
+
if (cross > width * 3.2) {
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
const envelope = Math.exp(-along * 0.14) * Math.exp(-(cross * cross / Math.max(0.4, width * width * 2.4)));
|
|
2058
|
+
total += Math.sin(along * 1.6 - time * 4.2) * speed * wakeStrength * envelope;
|
|
2059
|
+
}
|
|
2060
|
+
return total;
|
|
2061
|
+
}
|
|
2062
|
+
function sampleWaveImpulses(state, x, z, time) {
|
|
2063
|
+
const rippleStrength = readVisualNumber(state.demoVisuals?.collisionRippleStrength, 0.34);
|
|
2064
|
+
let total = 0;
|
|
2065
|
+
for (const impulse of state.waveImpulses) {
|
|
2066
|
+
const dx = x - impulse.x;
|
|
2067
|
+
const dz = z - impulse.z;
|
|
2068
|
+
const distance = Math.hypot(dx, dz);
|
|
2069
|
+
const radius = impulse.radius + (1 - impulse.life) * 4.8;
|
|
2070
|
+
if (distance > radius * 2.8) {
|
|
2071
|
+
continue;
|
|
2072
|
+
}
|
|
2073
|
+
const phase = distance * 1.8 - (1 - impulse.life) * 10 - time * 0.4;
|
|
2074
|
+
const envelope = Math.exp(-distance / Math.max(0.1, radius)) * impulse.life;
|
|
2075
|
+
total += Math.sin(phase) * impulse.strength * rippleStrength * envelope * 0.18;
|
|
2076
|
+
}
|
|
2077
|
+
return total;
|
|
2078
|
+
}
|
|
2079
|
+
function sampleWave(state, x, z, time) {
|
|
2080
|
+
const direction = resolveWaveDirection(state);
|
|
2081
|
+
const lateral = vec3(-direction.z, 0, direction.x);
|
|
2082
|
+
const along = x * direction.x + z * direction.z;
|
|
2083
|
+
const cross = x * lateral.x + z * lateral.z;
|
|
2084
|
+
const phaseSpeed = readVisualNumber(state.demoVisuals?.wavePhaseSpeed, 1);
|
|
2085
|
+
const amplitude = readVisualNumber(state.demoVisuals?.waveAmplitude, 1);
|
|
2086
|
+
const base = Math.sin(along * 0.22 - time * 1.12 * phaseSpeed) * 0.42 + Math.cos(along * 0.11 + cross * 0.07 - time * 0.78 * phaseSpeed) * 0.26 + Math.sin(cross * 0.19 - time * 1.34 * phaseSpeed) * 0.16;
|
|
2087
|
+
return base * amplitude + sampleShipWake(state, x, z, time) + sampleWaveImpulses(state, x, z, time);
|
|
2088
|
+
}
|
|
2089
|
+
function resolveFluidBandContinuity(continuity, band) {
|
|
2090
|
+
if (continuity?.bands && continuity.bands[band]) {
|
|
2091
|
+
return continuity.bands[band];
|
|
2092
|
+
}
|
|
2093
|
+
return continuity ?? { amplitudeFloor: 1, frequencyFloor: 1 };
|
|
2094
|
+
}
|
|
2095
|
+
function buildWaterMotionEffects(state) {
|
|
2096
|
+
const wakeTrails = [];
|
|
2097
|
+
const rippleRings = state.waveImpulses.map((impulse) => {
|
|
2098
|
+
const radius = impulse.radius + (1 - impulse.life) * 4.8;
|
|
2099
|
+
return Object.freeze({
|
|
2100
|
+
center: vec3(
|
|
2101
|
+
impulse.x,
|
|
2102
|
+
sampleWave(state, impulse.x, impulse.z, state.time) * 0.24 + 0.06,
|
|
2103
|
+
impulse.z
|
|
2104
|
+
),
|
|
2105
|
+
radius,
|
|
2106
|
+
opacity: clamp(impulse.life * 0.28, 0.08, 0.3)
|
|
2107
|
+
});
|
|
2108
|
+
});
|
|
2109
|
+
for (const ship of state.ships) {
|
|
2110
|
+
const speed = Math.hypot(ship.velocity.x, ship.velocity.z);
|
|
2111
|
+
if (speed <= 0.18) {
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
const direction = normalizeVec3(vec3(ship.velocity.x, 0, ship.velocity.z));
|
|
2115
|
+
const behind = scaleVec3(direction, -1);
|
|
2116
|
+
const lateral = vec3(-direction.z, 0, direction.x);
|
|
2117
|
+
const points = [];
|
|
2118
|
+
for (let sampleIndex = 0; sampleIndex < 6; sampleIndex += 1) {
|
|
2119
|
+
const along = 1 + sampleIndex * 1.45;
|
|
2120
|
+
const lateralOffset = Math.sin(state.time * 1.2 + sampleIndex * 0.8 + readVisualNumber(ship.wanderPhase, 0)) * 0.12;
|
|
2121
|
+
const worldPoint = addVec3(
|
|
2122
|
+
ship.position,
|
|
2123
|
+
addVec3(scaleVec3(behind, along), scaleVec3(lateral, lateralOffset))
|
|
2124
|
+
);
|
|
2125
|
+
points.push(
|
|
2126
|
+
Object.freeze({
|
|
2127
|
+
center: vec3(
|
|
2128
|
+
worldPoint.x,
|
|
2129
|
+
sampleWave(state, worldPoint.x, worldPoint.z, state.time) * 0.24 + 0.04,
|
|
2130
|
+
worldPoint.z
|
|
2131
|
+
),
|
|
2132
|
+
width: 0.34 + sampleIndex * 0.13
|
|
2133
|
+
})
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
wakeTrails.push(
|
|
2137
|
+
Object.freeze({
|
|
2138
|
+
opacity: clamp(0.18 + speed * 0.09, 0.22, 0.46),
|
|
2139
|
+
points: Object.freeze(points)
|
|
2140
|
+
})
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
return Object.freeze({
|
|
2144
|
+
wakeTrails: Object.freeze(wakeTrails),
|
|
2145
|
+
rippleRings: Object.freeze(rippleRings)
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
function buildWaterBands(state, fluidDetail, visuals, fluidFeatures) {
|
|
2149
|
+
const resolvedFluidFeatures = normalizeFluidFeatureAdapters(fluidFeatures);
|
|
2150
|
+
const fluidPlan = resolvedFluidFeatures.createPlan({
|
|
2151
|
+
fluidBodyId: "harbor",
|
|
2152
|
+
kind: state.focus === "fluid" ? "ocean" : resolvedFluidFeatures.bodyKinds[0],
|
|
2153
|
+
profile: state.focus === "fluid" ? "cinematic" : resolvedFluidFeatures.profileNames[0],
|
|
2154
|
+
supportsRayTracing: true,
|
|
2155
|
+
nearFieldMaxMeters: 40,
|
|
2156
|
+
midFieldMaxMeters: 150,
|
|
2157
|
+
farFieldMaxMeters: 600
|
|
2158
|
+
});
|
|
2159
|
+
const bandMeshes = [];
|
|
2160
|
+
const bandExtents = Object.freeze([
|
|
2161
|
+
{ band: "near", width: 20, depth: 18, step: 1, y: 0.2 },
|
|
2162
|
+
{ band: "mid", width: 34, depth: 28, step: 2, y: 0.05 },
|
|
2163
|
+
{ band: "far", width: 54, depth: 42, step: 3.5, y: -0.05 },
|
|
2164
|
+
{ band: "horizon", width: 80, depth: 76, step: 7, y: -0.14 }
|
|
2165
|
+
]);
|
|
2166
|
+
for (const bandSpec of bandExtents) {
|
|
2167
|
+
const representation = fluidPlan.representations.find((entry) => entry.band === bandSpec.band) ?? fluidPlan.representations[0];
|
|
2168
|
+
const continuity = resolvedFluidFeatures.createContinuityEnvelope({ fluidBodyId: "harbor" });
|
|
2169
|
+
const bandContinuity = resolveFluidBandContinuity(continuity, bandSpec.band);
|
|
2170
|
+
const bandResolution = bandSpec.band === "near" ? fluidDetail.nearResolution : bandSpec.band === "mid" ? fluidDetail.midResolution : bandSpec.band === "far" ? 5 : 3;
|
|
2171
|
+
const cols = Math.max(4, bandResolution * 2);
|
|
2172
|
+
const rows = Math.max(4, bandResolution + 2);
|
|
2173
|
+
const positions = [];
|
|
2174
|
+
const indices = [];
|
|
2175
|
+
const originX = -bandSpec.width * 0.5;
|
|
2176
|
+
const originZ = -6;
|
|
2177
|
+
for (let row = 0; row < rows; row += 1) {
|
|
2178
|
+
for (let column = 0; column < cols; column += 1) {
|
|
2179
|
+
const u = column / (cols - 1);
|
|
2180
|
+
const v = row / (rows - 1);
|
|
2181
|
+
const x = originX + bandSpec.width * u;
|
|
2182
|
+
const z = originZ + bandSpec.depth * v;
|
|
2183
|
+
const y = bandSpec.y + sampleWave(state, x, z, state.time) * bandContinuity.amplitudeFloor * (bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
|
|
2184
|
+
positions.push(vec3(x, y, z));
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
for (let row = 0; row < rows - 1; row += 1) {
|
|
2188
|
+
for (let column = 0; column < cols - 1; column += 1) {
|
|
2189
|
+
const a = row * cols + column;
|
|
2190
|
+
const b = a + 1;
|
|
2191
|
+
const c = a + cols + 1;
|
|
2192
|
+
const d = a + cols;
|
|
2193
|
+
indices.push(a, b, c, a, c, d);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
bandMeshes.push({
|
|
2197
|
+
band: bandSpec.band,
|
|
2198
|
+
representation,
|
|
2199
|
+
continuity: bandContinuity,
|
|
2200
|
+
rows,
|
|
2201
|
+
cols,
|
|
2202
|
+
positions,
|
|
2203
|
+
indices,
|
|
2204
|
+
color: bandSpec.band === "near" ? visuals.waterNear : bandSpec.band === "mid" ? {
|
|
2205
|
+
r: mix(visuals.waterNear.r, visuals.waterFar.r, 0.4),
|
|
2206
|
+
g: mix(visuals.waterNear.g, visuals.waterFar.g, 0.4),
|
|
2207
|
+
b: mix(visuals.waterNear.b, visuals.waterFar.b, 0.4)
|
|
2208
|
+
} : bandSpec.band === "far" ? visuals.waterFar : {
|
|
2209
|
+
r: mix(visuals.waterFar.r, 0.76, 0.2),
|
|
2210
|
+
g: mix(visuals.waterFar.g, 0.78, 0.2),
|
|
2211
|
+
b: mix(visuals.waterFar.b, 0.82, 0.2)
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
return { fluidPlan, bandMeshes };
|
|
2216
|
+
}
|
|
2217
|
+
function createSceneState(options, featureAdapters) {
|
|
2218
|
+
const translate = options.translate;
|
|
2219
|
+
const { governor, fluidDetail, clothDetail, lightingDetail } = createPerformanceGovernor(
|
|
2220
|
+
featureAdapters.performance
|
|
2221
|
+
);
|
|
2222
|
+
const physicsProfile = featureAdapters.physics.defaultProfile;
|
|
2223
|
+
const createPhysicsSimulationPlan = assertRequiredFunction(
|
|
2224
|
+
featureAdapters.physics,
|
|
2225
|
+
"physics",
|
|
2226
|
+
"createSimulationPlan"
|
|
2227
|
+
);
|
|
2228
|
+
const getPhysicsWorkerManifest = assertRequiredFunction(
|
|
2229
|
+
featureAdapters.physics,
|
|
2230
|
+
"physics",
|
|
2231
|
+
"getManifest"
|
|
2232
|
+
);
|
|
2233
|
+
const createGpuDebugSession = assertRequiredFunction(
|
|
2234
|
+
featureAdapters.debug,
|
|
2235
|
+
"debug",
|
|
2236
|
+
"createSession"
|
|
2237
|
+
);
|
|
2238
|
+
const physicsPlan = createPhysicsSimulationPlan(physicsProfile);
|
|
2239
|
+
const physicsManifest = getPhysicsWorkerManifest(physicsProfile);
|
|
2240
|
+
const debugSession = createGpuDebugSession({
|
|
2241
|
+
enabled: true,
|
|
2242
|
+
adapter: {
|
|
2243
|
+
label: translate(gpuSharedTranslationKeys.debugAdapterShowcase),
|
|
2244
|
+
memoryCapacityHintBytes: 6 * 1024 * 1024 * 1024,
|
|
2245
|
+
coreCountHint: 12
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
2248
|
+
debugSession.trackAllocation({
|
|
2249
|
+
id: "showcase.color",
|
|
2250
|
+
owner: "renderer",
|
|
2251
|
+
category: "texture",
|
|
2252
|
+
sizeBytes: 1280 * 720 * 4,
|
|
2253
|
+
label: translate(gpuSharedTranslationKeys.debugMainColorBuffer)
|
|
2254
|
+
});
|
|
2255
|
+
debugSession.trackAllocation({
|
|
2256
|
+
id: "showcase.shadow-impression",
|
|
2257
|
+
owner: "lighting",
|
|
2258
|
+
category: "texture",
|
|
2259
|
+
sizeBytes: 12 * 1024 * 1024,
|
|
2260
|
+
label: translate(gpuSharedTranslationKeys.debugShadowImpressionAtlas)
|
|
2261
|
+
});
|
|
2262
|
+
return {
|
|
2263
|
+
translate,
|
|
2264
|
+
focus: options.focus,
|
|
2265
|
+
governor,
|
|
2266
|
+
fluidDetail,
|
|
2267
|
+
clothDetail,
|
|
2268
|
+
lightingDetail,
|
|
2269
|
+
debugSession,
|
|
2270
|
+
showcaseRealisticModelsEnabled: options.realisticModelsEnabled !== false,
|
|
2271
|
+
captureMode: options.captureMode === true,
|
|
2272
|
+
renderScale: readPositiveNumber(options.renderScale, void 0),
|
|
2273
|
+
packageState: void 0,
|
|
2274
|
+
demoDescription: null,
|
|
2275
|
+
demoVisuals: null,
|
|
2276
|
+
time: 0,
|
|
2277
|
+
lastTimeMs: null,
|
|
2278
|
+
paused: false,
|
|
2279
|
+
stress: false,
|
|
2280
|
+
camera: {
|
|
2281
|
+
...CAMERA_PRESETS[options.focus],
|
|
2282
|
+
target: vec3(...CAMERA_PRESETS[options.focus].target)
|
|
2283
|
+
},
|
|
2284
|
+
ships: [
|
|
2285
|
+
{
|
|
2286
|
+
id: "northwind",
|
|
2287
|
+
modelKey: "brigantine",
|
|
2288
|
+
position: vec3(-5.2, 0, 7.2),
|
|
2289
|
+
velocity: vec3(2.35, 0, -1.08),
|
|
2290
|
+
rotationY: 0.58,
|
|
2291
|
+
angularVelocity: 0.09,
|
|
2292
|
+
tint: { r: 0.62, g: 0.39, b: 0.23 },
|
|
2293
|
+
massScale: 1.42,
|
|
2294
|
+
cruiseSpeed: 2.25,
|
|
2295
|
+
throttleResponse: 0.46,
|
|
2296
|
+
rudderResponse: 0.54,
|
|
2297
|
+
wanderPhase: 0.35,
|
|
2298
|
+
lanterns: CUTTER_LANTERNS,
|
|
2299
|
+
lanternStrength: 1.06,
|
|
2300
|
+
collisionRadiusScale: 1.04
|
|
2301
|
+
},
|
|
2302
|
+
{
|
|
2303
|
+
id: "tidecaller",
|
|
2304
|
+
modelKey: "cutter",
|
|
2305
|
+
position: vec3(4.8, 0, 4.4),
|
|
2306
|
+
velocity: vec3(-2.15, 0, 1.74),
|
|
2307
|
+
rotationY: -2.48,
|
|
2308
|
+
angularVelocity: -0.2,
|
|
2309
|
+
tint: { r: 0.58, g: 0.24, b: 0.16 },
|
|
2310
|
+
massScale: 0.84,
|
|
2311
|
+
cruiseSpeed: 2.68,
|
|
2312
|
+
throttleResponse: 0.7,
|
|
2313
|
+
rudderResponse: 0.78,
|
|
2314
|
+
wanderPhase: 1.6,
|
|
2315
|
+
lanterns: SHIP_LANTERNS,
|
|
2316
|
+
lanternStrength: 1.18,
|
|
2317
|
+
collisionRadiusScale: 0.94
|
|
2318
|
+
}
|
|
2319
|
+
],
|
|
2320
|
+
sprays: [],
|
|
2321
|
+
waveImpulses: [],
|
|
2322
|
+
frame: 0,
|
|
2323
|
+
contactCount: 0,
|
|
2324
|
+
collisionCount: 0,
|
|
2325
|
+
collisionFlash: 0,
|
|
2326
|
+
clothState: null,
|
|
2327
|
+
physics: {
|
|
2328
|
+
profile: physicsProfile,
|
|
2329
|
+
plan: physicsPlan,
|
|
2330
|
+
manifest: physicsManifest,
|
|
2331
|
+
snapshot: null
|
|
2332
|
+
},
|
|
2333
|
+
assetCatalog: null,
|
|
2334
|
+
shipModel: null
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
function setListContent(element, values) {
|
|
2338
|
+
element.innerHTML = values.map((value) => `<li>${value}</li>`).join("");
|
|
2339
|
+
}
|
|
2340
|
+
function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength, visuals) {
|
|
2341
|
+
const sky = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.5);
|
|
2342
|
+
sky.addColorStop(0, visuals.skyTop);
|
|
2343
|
+
sky.addColorStop(0.54, visuals.skyMid);
|
|
2344
|
+
sky.addColorStop(1, visuals.skyBottom);
|
|
2345
|
+
ctx.fillStyle = sky;
|
|
2346
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
2347
|
+
for (let index = 0; index < 70; index += 1) {
|
|
2348
|
+
const x = pseudoRandom(index + 13) * canvas.width;
|
|
2349
|
+
const y = pseudoRandom(index * 7 + 5) * canvas.height * 0.42;
|
|
2350
|
+
const twinkle = 0.45 + Math.sin(state.time * 1.4 + index * 0.73) * 0.25;
|
|
2351
|
+
const radius = 0.6 + pseudoRandom(index * 11 + 2) * 1.9;
|
|
2352
|
+
ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${clamp(twinkle, 0.16, 0.92)})`);
|
|
2353
|
+
ctx.beginPath();
|
|
2354
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
2355
|
+
ctx.fill();
|
|
2356
|
+
}
|
|
2357
|
+
const horizonGlow = ctx.createLinearGradient(0, canvas.height * 0.22, 0, canvas.height * 0.62);
|
|
2358
|
+
horizonGlow.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
2359
|
+
horizonGlow.addColorStop(1, visuals.duskGlow);
|
|
2360
|
+
ctx.fillStyle = horizonGlow;
|
|
2361
|
+
ctx.fillRect(0, canvas.height * 0.2, canvas.width, canvas.height * 0.45);
|
|
2362
|
+
const shoreline = ctx.createLinearGradient(0, canvas.height * 0.45, 0, canvas.height);
|
|
2363
|
+
shoreline.addColorStop(0, visuals.seaTop);
|
|
2364
|
+
shoreline.addColorStop(0.52, visuals.seaMid);
|
|
2365
|
+
shoreline.addColorStop(1, visuals.seaBottom);
|
|
2366
|
+
ctx.fillStyle = shoreline;
|
|
2367
|
+
ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
|
|
2368
|
+
const moonX = canvas.width * 0.76 + Math.sin(state.time * 0.045) * 18;
|
|
2369
|
+
const moonY = canvas.height * 0.17 + Math.cos(state.time * 0.05) * 10;
|
|
2370
|
+
const moon = ctx.createRadialGradient(moonX, moonY, 14, moonX, moonY, 126);
|
|
2371
|
+
moon.addColorStop(0, visuals.moonCore);
|
|
2372
|
+
moon.addColorStop(0.46, visuals.moonHalo);
|
|
2373
|
+
moon.addColorStop(1, "rgba(167, 191, 255, 0)");
|
|
2374
|
+
ctx.fillStyle = moon;
|
|
2375
|
+
ctx.beginPath();
|
|
2376
|
+
ctx.arc(moonX, moonY, 94, 0, Math.PI * 2);
|
|
2377
|
+
ctx.fill();
|
|
2378
|
+
const moonCore = ctx.createRadialGradient(moonX, moonY, 4, moonX, moonY, 28);
|
|
2379
|
+
moonCore.addColorStop(0, "rgba(255, 255, 255, 0.98)");
|
|
2380
|
+
moonCore.addColorStop(1, visuals.moonCore);
|
|
2381
|
+
ctx.fillStyle = moonCore;
|
|
2382
|
+
ctx.beginPath();
|
|
2383
|
+
ctx.arc(moonX, moonY, 24, 0, Math.PI * 2);
|
|
2384
|
+
ctx.fill();
|
|
2385
|
+
const track = ctx.createLinearGradient(moonX, canvas.height * 0.44, moonX, canvas.height * 0.98);
|
|
2386
|
+
track.addColorStop(0, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.08 + reflectionStrength * 0.12})`));
|
|
2387
|
+
track.addColorStop(0.42, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.04 + reflectionStrength * 0.18})`));
|
|
2388
|
+
track.addColorStop(1, "rgba(192, 214, 255, 0)");
|
|
2389
|
+
ctx.save();
|
|
2390
|
+
ctx.globalCompositeOperation = "screen";
|
|
2391
|
+
ctx.fillStyle = track;
|
|
2392
|
+
ctx.beginPath();
|
|
2393
|
+
ctx.ellipse(moonX, canvas.height * 0.75, 38 + shadowStrength * 42, canvas.height * 0.24, 0, 0, Math.PI * 2);
|
|
2394
|
+
ctx.fill();
|
|
2395
|
+
ctx.restore();
|
|
2396
|
+
const mist = ctx.createLinearGradient(0, canvas.height * 0.5, 0, canvas.height);
|
|
2397
|
+
mist.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
2398
|
+
mist.addColorStop(1, visuals.ambientMist);
|
|
2399
|
+
ctx.fillStyle = mist;
|
|
2400
|
+
ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
|
|
2401
|
+
if (state.collisionFlash > 0.01) {
|
|
2402
|
+
ctx.fillStyle = visuals.collisionFlash.replace(
|
|
2403
|
+
/[\d.]+\)$/u,
|
|
2404
|
+
`${clamp(state.collisionFlash * 0.22, 0, 0.26)})`
|
|
2405
|
+
);
|
|
2406
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
function resolveLocalLightContribution(triangle, lightSources) {
|
|
2410
|
+
const contribution = { r: 0, g: 0, b: 0 };
|
|
2411
|
+
if (!Array.isArray(lightSources) || triangle.surfaceType === "water") {
|
|
2412
|
+
return contribution;
|
|
2413
|
+
}
|
|
2414
|
+
const normal = normalizeVec3(triangle.normal);
|
|
2415
|
+
for (const source of lightSources.slice(0, 8)) {
|
|
2416
|
+
const delta = subVec3(source.point, triangle.worldCenter);
|
|
2417
|
+
const distance = lengthVec3(delta);
|
|
2418
|
+
const attenuation = (source.glowScale ?? 1) / Math.max(1, 0.68 + distance * distance * 0.2);
|
|
2419
|
+
if (attenuation < 0.012) {
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
const lightDir = normalizeVec3(delta);
|
|
2423
|
+
const facing = clamp(dotVec3(normal, lightDir), 0, 1);
|
|
2424
|
+
const response = attenuation * (0.18 + facing * 0.82);
|
|
2425
|
+
const glowColor = source.glowColor ?? source.coreColor ?? { r: 1, g: 0.72, b: 0.4 };
|
|
2426
|
+
contribution.r += glowColor.r * response * 0.32;
|
|
2427
|
+
contribution.g += glowColor.g * response * 0.26;
|
|
2428
|
+
contribution.b += glowColor.b * response * 0.18;
|
|
2429
|
+
}
|
|
2430
|
+
return contribution;
|
|
2431
|
+
}
|
|
2432
|
+
function drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength, localLights = []) {
|
|
2433
|
+
triangles.sort((left, right) => right.depth - left.depth);
|
|
2434
|
+
for (const triangle of triangles) {
|
|
2435
|
+
const surfaceNormal = normalizeVec3(triangle.normal);
|
|
2436
|
+
const material = triangle.material ?? {
|
|
2437
|
+
roughness: 0.88,
|
|
2438
|
+
metallic: 0.08,
|
|
2439
|
+
emissive: { r: 0, g: 0, b: 0 }
|
|
2440
|
+
};
|
|
2441
|
+
const shaded = shadeColor(
|
|
2442
|
+
triangle.baseColor,
|
|
2443
|
+
surfaceNormal,
|
|
2444
|
+
lightDir,
|
|
2445
|
+
clamp((triangle.worldCenter.y + 3) / 10, 0, 1),
|
|
2446
|
+
triangle.accent
|
|
2447
|
+
);
|
|
2448
|
+
const reflection = reflectionStrength * (triangle.reflection ?? 0);
|
|
2449
|
+
const viewDir = normalizeVec3(subVec3(camera.eye, triangle.worldCenter));
|
|
2450
|
+
const reflectedLight = reflectVec3(scaleVec3(lightDir, -1), surfaceNormal);
|
|
2451
|
+
const gloss = mix(0.78, 0.14, clamp(material.roughness ?? 0.88, 0, 1)) + (material.metallic ?? 0) * 0.18;
|
|
2452
|
+
const specularPower = mix(26, 7, clamp(material.roughness ?? 0.88, 0, 1));
|
|
2453
|
+
const specular = Math.pow(clamp(dotVec3(reflectedLight, viewDir), 0, 1), specularPower) * gloss;
|
|
2454
|
+
const emissive = material.emissive ?? { r: 0, g: 0, b: 0 };
|
|
2455
|
+
const localLight = resolveLocalLightContribution(triangle, localLights);
|
|
2456
|
+
const occlusion = triangle.surfaceType === "water" ? shadowStrength * 0.018 : shadowStrength * 0.04;
|
|
2457
|
+
const detailed = applyMaterialDetail(
|
|
2458
|
+
{
|
|
2459
|
+
r: clamp(
|
|
2460
|
+
shaded.r + reflection * 0.08 + specular * 0.16 + emissive.r * 0.42 + localLight.r - occlusion,
|
|
2461
|
+
0,
|
|
2462
|
+
1
|
|
2463
|
+
),
|
|
2464
|
+
g: clamp(
|
|
2465
|
+
shaded.g + reflection * 0.08 + specular * 0.16 + emissive.g * 0.42 + localLight.g - occlusion,
|
|
2466
|
+
0,
|
|
2467
|
+
1
|
|
2468
|
+
),
|
|
2469
|
+
b: clamp(
|
|
2470
|
+
shaded.b + reflection * 0.16 + specular * 0.22 + emissive.b * 0.46 + localLight.b - occlusion * 0.5,
|
|
2471
|
+
0,
|
|
2472
|
+
1
|
|
2473
|
+
)
|
|
2474
|
+
},
|
|
2475
|
+
material,
|
|
2476
|
+
triangle.worldCenter,
|
|
2477
|
+
surfaceNormal,
|
|
2478
|
+
triangle.surfaceType
|
|
2479
|
+
);
|
|
2480
|
+
const fill = colorToRgba(
|
|
2481
|
+
detailed,
|
|
2482
|
+
triangle.baseColor.a ?? 0.98
|
|
2483
|
+
);
|
|
2484
|
+
ctx.fillStyle = fill;
|
|
2485
|
+
ctx.beginPath();
|
|
2486
|
+
ctx.moveTo(triangle.points[0].x, triangle.points[0].y);
|
|
2487
|
+
ctx.lineTo(triangle.points[1].x, triangle.points[1].y);
|
|
2488
|
+
ctx.lineTo(triangle.points[2].x, triangle.points[2].y);
|
|
2489
|
+
ctx.closePath();
|
|
2490
|
+
ctx.fill();
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, options = {}) {
|
|
2494
|
+
const planeY = options.planeY ?? 0;
|
|
2495
|
+
const projected = worldPoints.map((point) => projectShadowPoint(point, lightDir, planeY)).filter(Boolean).map((point) => projectPoint(point, camera, viewport)).filter(Boolean);
|
|
2496
|
+
if (projected.length < 3) {
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
ctx.save();
|
|
2500
|
+
ctx.globalCompositeOperation = "multiply";
|
|
2501
|
+
ctx.fillStyle = options.color ?? `rgba(12, 24, 36, ${clamp(options.alpha ?? 0.16, 0, 0.5)})`;
|
|
2502
|
+
ctx.shadowColor = options.color ?? "rgba(12, 24, 36, 0.22)";
|
|
2503
|
+
ctx.shadowBlur = options.blur ?? 18;
|
|
2504
|
+
ctx.beginPath();
|
|
2505
|
+
ctx.moveTo(projected[0].x, projected[0].y);
|
|
2506
|
+
for (let index = 1; index < projected.length; index += 1) {
|
|
2507
|
+
ctx.lineTo(projected[index].x, projected[index].y);
|
|
2508
|
+
}
|
|
2509
|
+
ctx.closePath();
|
|
2510
|
+
ctx.fill();
|
|
2511
|
+
ctx.restore();
|
|
2512
|
+
}
|
|
2513
|
+
function pushHarborGeometry(camera, viewport, triangles, state) {
|
|
2514
|
+
if (!state.showcaseRealisticModelsEnabled) {
|
|
2515
|
+
for (const object of LEGACY_HARBOR_LAYOUT) {
|
|
2516
|
+
buildTrianglesFromMesh(
|
|
2517
|
+
{ positions: [object], indices: [0], normals: null, colors: null, material: createLegacyMeshPrimitive({})?.material, bounds: null, name: "legacy-structure" },
|
|
2518
|
+
{
|
|
2519
|
+
position: object.position,
|
|
2520
|
+
rotationY: object.rotationY,
|
|
2521
|
+
scale: object.scale
|
|
2522
|
+
},
|
|
2523
|
+
object.color,
|
|
2524
|
+
camera,
|
|
2525
|
+
viewport,
|
|
2526
|
+
triangles,
|
|
2527
|
+
{
|
|
2528
|
+
accent: object.accent,
|
|
2529
|
+
reflection: 0,
|
|
2530
|
+
surfaceType: "structure"
|
|
2531
|
+
}
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
for (const placement of SHOWCASE_ENVIRONMENT_LAYOUT) {
|
|
2537
|
+
const mesh = state.assetCatalog?.environment?.[placement.assetKey] ?? null;
|
|
2538
|
+
if (!mesh) {
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
buildTrianglesFromMesh(
|
|
2542
|
+
mesh,
|
|
2543
|
+
{
|
|
2544
|
+
position: vec3(placement.position.x, placement.position.y, placement.position.z),
|
|
2545
|
+
rotationY: placement.rotationY,
|
|
2546
|
+
scale: placement.scale
|
|
2547
|
+
},
|
|
2548
|
+
null,
|
|
2549
|
+
camera,
|
|
2550
|
+
viewport,
|
|
2551
|
+
triangles,
|
|
2552
|
+
{
|
|
2553
|
+
accent: placement.accent,
|
|
2554
|
+
reflection: 0,
|
|
2555
|
+
surfaceType: "structure"
|
|
2556
|
+
}
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
function renderShipRigging(ctx, ship, camera, viewport) {
|
|
2561
|
+
const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
|
|
2562
|
+
const layout = ship.modelKey === "cutter" ? {
|
|
2563
|
+
lineColor: "rgba(85, 89, 97, 0.92)",
|
|
2564
|
+
sailColor: "rgba(218, 232, 244, 0.28)",
|
|
2565
|
+
points: [
|
|
2566
|
+
vec3(0, 0.88, -0.32),
|
|
2567
|
+
vec3(0, 2.4, -0.28),
|
|
2568
|
+
vec3(0.1, 1.92, -0.3),
|
|
2569
|
+
vec3(1.18, 1.72, -0.18),
|
|
2570
|
+
vec3(1.04, 1.08, -0.12)
|
|
2571
|
+
],
|
|
2572
|
+
mastPairs: [[0, 1], [2, 3]],
|
|
2573
|
+
sailTriangle: [2, 3, 4]
|
|
2574
|
+
} : {
|
|
2575
|
+
lineColor: "rgba(73, 54, 45, 0.94)",
|
|
2576
|
+
sailColor: "rgba(238, 232, 214, 0.88)",
|
|
2577
|
+
points: [
|
|
2578
|
+
vec3(0, 0.38, -0.4),
|
|
2579
|
+
vec3(0, 3.8, -0.2),
|
|
2580
|
+
vec3(-0.25, 0.32, -1.9),
|
|
2581
|
+
vec3(-0.15, 2.7, -1.75),
|
|
2582
|
+
vec3(0.08, 3.2, -0.2),
|
|
2583
|
+
vec3(0.12, 1.2, -0.5),
|
|
2584
|
+
vec3(2.25, 2.25, 0.15)
|
|
2585
|
+
],
|
|
2586
|
+
mastPairs: [[0, 1], [2, 3]],
|
|
2587
|
+
sailTriangle: [4, 5, 6]
|
|
2588
|
+
};
|
|
2589
|
+
const projected = layout.points.map((point) => transformPoint(point, transform)).map((point) => projectPoint(point, camera, viewport));
|
|
2590
|
+
if (projected.some((value) => value === null)) {
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
ctx.strokeStyle = layout.lineColor;
|
|
2594
|
+
ctx.lineWidth = ship.modelKey === "cutter" ? 2.2 : 3.5;
|
|
2595
|
+
ctx.beginPath();
|
|
2596
|
+
for (const [from, to] of layout.mastPairs) {
|
|
2597
|
+
ctx.moveTo(projected[from].x, projected[from].y);
|
|
2598
|
+
ctx.lineTo(projected[to].x, projected[to].y);
|
|
2599
|
+
}
|
|
2600
|
+
ctx.stroke();
|
|
2601
|
+
const [a, b, c] = layout.sailTriangle;
|
|
2602
|
+
ctx.fillStyle = layout.sailColor;
|
|
2603
|
+
ctx.beginPath();
|
|
2604
|
+
ctx.moveTo(projected[a].x, projected[a].y);
|
|
2605
|
+
ctx.lineTo(projected[b].x, projected[b].y);
|
|
2606
|
+
ctx.lineTo(projected[c].x, projected[c].y);
|
|
2607
|
+
ctx.closePath();
|
|
2608
|
+
ctx.fill();
|
|
2609
|
+
}
|
|
2610
|
+
function renderClothAccent(ctx, cloth, camera, viewport) {
|
|
2611
|
+
const projected = cloth.positions.map((point) => projectPoint(point, camera, viewport));
|
|
2612
|
+
ctx.strokeStyle = "rgba(255, 241, 226, 0.92)";
|
|
2613
|
+
ctx.lineWidth = 1.7;
|
|
2614
|
+
for (let row = 0; row < cloth.grid.rows; row += Math.max(1, Math.floor(cloth.grid.rows / 5))) {
|
|
2615
|
+
ctx.beginPath();
|
|
2616
|
+
let started = false;
|
|
2617
|
+
for (let column = 0; column < cloth.grid.cols; column += 1) {
|
|
2618
|
+
const point = projected[row * cloth.grid.cols + column];
|
|
2619
|
+
if (!point) {
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
if (!started) {
|
|
2623
|
+
ctx.moveTo(point.x, point.y);
|
|
2624
|
+
started = true;
|
|
2625
|
+
} else {
|
|
2626
|
+
ctx.lineTo(point.x, point.y);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
if (started) {
|
|
2630
|
+
ctx.stroke();
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
const borderIndices = [
|
|
2634
|
+
0,
|
|
2635
|
+
cloth.grid.cols - 1,
|
|
2636
|
+
cloth.grid.rows * cloth.grid.cols - 1,
|
|
2637
|
+
(cloth.grid.rows - 1) * cloth.grid.cols
|
|
2638
|
+
];
|
|
2639
|
+
ctx.fillStyle = colorToRgba(cloth.color, 0.95);
|
|
2640
|
+
for (const index of borderIndices) {
|
|
2641
|
+
const point = projected[index];
|
|
2642
|
+
if (!point) {
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
ctx.beginPath();
|
|
2646
|
+
ctx.arc(point.x, point.y, 2.8, 0, Math.PI * 2);
|
|
2647
|
+
ctx.fill();
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
function renderWaterHighlights(ctx, waterBands, camera, viewport) {
|
|
2651
|
+
for (const band of waterBands) {
|
|
2652
|
+
if (band.band === "horizon") {
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
const interval = band.band === "near" ? 2 : 3;
|
|
2656
|
+
const alpha = band.band === "near" ? 0.22 : 0.14;
|
|
2657
|
+
ctx.strokeStyle = `rgba(232, 247, 255, ${alpha})`;
|
|
2658
|
+
ctx.lineWidth = band.band === "near" ? 1.3 : 0.9;
|
|
2659
|
+
for (let row = interval; row < band.rows - 1; row += interval) {
|
|
2660
|
+
ctx.beginPath();
|
|
2661
|
+
let started = false;
|
|
2662
|
+
for (let column = 0; column < band.cols; column += 1) {
|
|
2663
|
+
const point = projectPoint(
|
|
2664
|
+
band.positions[row * band.cols + column],
|
|
2665
|
+
camera,
|
|
2666
|
+
viewport
|
|
2667
|
+
);
|
|
2668
|
+
if (!point) {
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
if (!started) {
|
|
2672
|
+
ctx.moveTo(point.x, point.y);
|
|
2673
|
+
started = true;
|
|
2674
|
+
} else {
|
|
2675
|
+
ctx.lineTo(point.x, point.y);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
if (started) {
|
|
2679
|
+
ctx.stroke();
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
function readPhysicsNumber(physics, key, fallback) {
|
|
2685
|
+
const value = physics?.[key];
|
|
2686
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2687
|
+
}
|
|
2688
|
+
function getShipMass(ship, shipModel) {
|
|
2689
|
+
const baseMass = readPhysicsNumber(shipModel.physics, "mass", 3200);
|
|
2690
|
+
return baseMass * readVisualNumber(ship.massScale, 1);
|
|
2691
|
+
}
|
|
2692
|
+
function getShipHalfExtents(ship, shipModel) {
|
|
2693
|
+
const physicsHalfExtents = Array.isArray(shipModel.physics.halfExtents) ? shipModel.physics.halfExtents : [1.35, 0.95, 3.9];
|
|
2694
|
+
const scale = SHIP_SCALE * readVisualNumber(ship.collisionRadiusScale, 1);
|
|
2695
|
+
return {
|
|
2696
|
+
x: physicsHalfExtents[0] * scale,
|
|
2697
|
+
y: physicsHalfExtents[1] * scale,
|
|
2698
|
+
z: physicsHalfExtents[2] * scale
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
function getShipCollisionRadius(ship, shipModel) {
|
|
2702
|
+
const halfExtents = getShipHalfExtents(ship, shipModel);
|
|
2703
|
+
return Math.max(halfExtents.x * 1.08, halfExtents.z * 0.62);
|
|
2704
|
+
}
|
|
2705
|
+
function getShipInverseMass(ship, shipModel) {
|
|
2706
|
+
return 1 / Math.max(1, getShipMass(ship, shipModel));
|
|
2707
|
+
}
|
|
2708
|
+
function getShipInverseInertia(ship, shipModel) {
|
|
2709
|
+
const radius = getShipCollisionRadius(ship, shipModel);
|
|
2710
|
+
const inertia = getShipMass(ship, shipModel) * radius * radius * 0.72;
|
|
2711
|
+
return 1 / Math.max(1, inertia);
|
|
2712
|
+
}
|
|
2713
|
+
function spawnSpray(state, point, intensity) {
|
|
2714
|
+
const count = state.fluidDetail.getSnapshot().currentLevel.config.splashCount;
|
|
2715
|
+
for (let index = 0; index < count; index += 1) {
|
|
2716
|
+
const angle = index / count * Math.PI * 2;
|
|
2717
|
+
const speed = 0.9 + Math.random() * intensity * 0.45;
|
|
2718
|
+
state.sprays.push({
|
|
2719
|
+
position: vec3(point.x, point.y, point.z),
|
|
2720
|
+
velocity: vec3(Math.cos(angle) * speed * 0.35, 1.1 + Math.random() * 0.8, Math.sin(angle) * speed * 0.25),
|
|
2721
|
+
life: 1.2 + Math.random() * 0.4
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
function resolveShipRoute(ship, state, radius) {
|
|
2726
|
+
if (typeof ship.routeDirection !== "number") {
|
|
2727
|
+
ship.routeDirection = ship.velocity.x >= 0 ? 1 : -1;
|
|
2728
|
+
}
|
|
2729
|
+
if (ship.position.x > HARBOR_BOUNDS.maxX - radius * 1.1) {
|
|
2730
|
+
ship.routeDirection = -1;
|
|
2731
|
+
} else if (ship.position.x < HARBOR_BOUNDS.minX + radius * 1.1) {
|
|
2732
|
+
ship.routeDirection = 1;
|
|
2733
|
+
}
|
|
2734
|
+
const wander = Math.sin(state.time * 0.22 + readVisualNumber(ship.wanderPhase, 0));
|
|
2735
|
+
const crossCurrent = Math.cos(state.time * 0.31 + readVisualNumber(ship.wanderPhase, 0));
|
|
2736
|
+
const laneCenter = ship.id === "northwind" ? 10.2 + wander * 2.1 + crossCurrent * 0.6 : 7 + wander * 3.3 - crossCurrent * 1.1;
|
|
2737
|
+
const targetX = ship.routeDirection > 0 ? HARBOR_BOUNDS.maxX - radius * 1.7 : HARBOR_BOUNDS.minX + radius * 1.7;
|
|
2738
|
+
return vec3(targetX, 0, clamp(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
|
|
2739
|
+
}
|
|
2740
|
+
function updateShipMotion(state, ship, dt, shipModel) {
|
|
2741
|
+
const physics = shipModel.physics;
|
|
2742
|
+
const massScale = Math.max(0.55, readVisualNumber(ship.massScale, 1));
|
|
2743
|
+
const radius = getShipCollisionRadius(ship, shipModel);
|
|
2744
|
+
const waterline = readPhysicsNumber(physics, "waterline", 0.42);
|
|
2745
|
+
const linearDamping = readPhysicsNumber(physics, "linearDamping", 0.04);
|
|
2746
|
+
const angularDamping = readPhysicsNumber(physics, "angularDamping", 0.08);
|
|
2747
|
+
const throttleResponse = readVisualNumber(ship.throttleResponse, 0.58);
|
|
2748
|
+
const rudderResponse = readVisualNumber(ship.rudderResponse, 0.62);
|
|
2749
|
+
const cruiseSpeed = readVisualNumber(ship.cruiseSpeed, 2.4);
|
|
2750
|
+
ship.collisionCooldown = Math.max(0, readVisualNumber(ship.collisionCooldown, 0) - dt);
|
|
2751
|
+
const forward = directionFromYaw(ship.rotationY);
|
|
2752
|
+
const lateral = perpendicularOnWater(forward);
|
|
2753
|
+
const routeTarget = resolveShipRoute(ship, state, radius);
|
|
2754
|
+
const desiredHeading = Math.atan2(routeTarget.x - ship.position.x, routeTarget.z - ship.position.z);
|
|
2755
|
+
const headingError = Math.atan2(
|
|
2756
|
+
Math.sin(desiredHeading - ship.rotationY),
|
|
2757
|
+
Math.cos(desiredHeading - ship.rotationY)
|
|
2758
|
+
);
|
|
2759
|
+
ship.angularVelocity += headingError * rudderResponse * dt * (1.18 / Math.sqrt(massScale)) + Math.sin(state.time * 0.9 + readVisualNumber(ship.wanderPhase, 0)) * dt * 0.04;
|
|
2760
|
+
const waveDirection = resolveWaveDirection(state);
|
|
2761
|
+
const forwardSpeed = dotVec3(ship.velocity, forward);
|
|
2762
|
+
const lateralSpeed = dotVec3(ship.velocity, lateral);
|
|
2763
|
+
const thrust = (cruiseSpeed - forwardSpeed) * throttleResponse;
|
|
2764
|
+
const currentDrift = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.016;
|
|
2765
|
+
const acceleration = addVec3(
|
|
2766
|
+
scaleVec3(forward, thrust),
|
|
2767
|
+
addVec3(
|
|
2768
|
+
scaleVec3(lateral, -lateralSpeed * (1.28 + rudderResponse * 0.4)),
|
|
2769
|
+
scaleVec3(waveDirection, currentDrift / Math.sqrt(massScale))
|
|
2770
|
+
)
|
|
2771
|
+
);
|
|
2772
|
+
ship.velocity = addVec3(ship.velocity, scaleVec3(acceleration, dt));
|
|
2773
|
+
ship.velocity = scaleVec3(
|
|
2774
|
+
ship.velocity,
|
|
2775
|
+
Math.max(0, 1 - linearDamping / Math.pow(massScale, 0.22) * dt)
|
|
2776
|
+
);
|
|
2777
|
+
ship.angularVelocity *= Math.max(
|
|
2778
|
+
0,
|
|
2779
|
+
1 - angularDamping / Math.pow(massScale, 0.15) * dt
|
|
2780
|
+
);
|
|
2781
|
+
ship.rotationY += ship.angularVelocity * dt;
|
|
2782
|
+
ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
|
|
2783
|
+
ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 + waterline;
|
|
2784
|
+
}
|
|
2785
|
+
function resolveBoundaryCollision(ship, state, shipModel) {
|
|
2786
|
+
const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.56;
|
|
2787
|
+
const radius = getShipCollisionRadius(ship, shipModel);
|
|
2788
|
+
const boundaries = [
|
|
2789
|
+
{ axis: "x", min: HARBOR_BOUNDS.minX + radius, max: HARBOR_BOUNDS.maxX - radius, normalMin: vec3(1, 0, 0), normalMax: vec3(-1, 0, 0) },
|
|
2790
|
+
{ axis: "z", min: HARBOR_BOUNDS.minZ + radius, max: HARBOR_BOUNDS.maxZ - radius, normalMin: vec3(0, 0, 1), normalMax: vec3(0, 0, -1) }
|
|
2791
|
+
];
|
|
2792
|
+
for (const boundary of boundaries) {
|
|
2793
|
+
if (ship.position[boundary.axis] < boundary.min) {
|
|
2794
|
+
ship.position[boundary.axis] = boundary.min;
|
|
2795
|
+
const normal = boundary.normalMin;
|
|
2796
|
+
const speedIntoWall = dotVec3(ship.velocity, normal);
|
|
2797
|
+
if (speedIntoWall < 0) {
|
|
2798
|
+
ship.velocity = subVec3(
|
|
2799
|
+
ship.velocity,
|
|
2800
|
+
scaleVec3(normal, (1 + restitution) * speedIntoWall)
|
|
2801
|
+
);
|
|
2802
|
+
const tangent = vec3(-normal.z, 0, normal.x);
|
|
2803
|
+
const tangentSpeed = dotVec3(ship.velocity, tangent);
|
|
2804
|
+
ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
|
|
2805
|
+
ship.angularVelocity += tangentSpeed * 4e-3;
|
|
2806
|
+
}
|
|
2807
|
+
} else if (ship.position[boundary.axis] > boundary.max) {
|
|
2808
|
+
ship.position[boundary.axis] = boundary.max;
|
|
2809
|
+
const normal = boundary.normalMax;
|
|
2810
|
+
const speedIntoWall = dotVec3(ship.velocity, normal);
|
|
2811
|
+
if (speedIntoWall < 0) {
|
|
2812
|
+
ship.velocity = subVec3(
|
|
2813
|
+
ship.velocity,
|
|
2814
|
+
scaleVec3(normal, (1 + restitution) * speedIntoWall)
|
|
2815
|
+
);
|
|
2816
|
+
const tangent = vec3(-normal.z, 0, normal.x);
|
|
2817
|
+
const tangentSpeed = dotVec3(ship.velocity, tangent);
|
|
2818
|
+
ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
|
|
2819
|
+
ship.angularVelocity += tangentSpeed * 4e-3;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
|
|
2825
|
+
const delta = subVec3(b.position, a.position);
|
|
2826
|
+
const radiusA = getShipCollisionRadius(a, shipModelA);
|
|
2827
|
+
const radiusB = getShipCollisionRadius(b, shipModelB);
|
|
2828
|
+
const distance = Math.hypot(delta.x, delta.z);
|
|
2829
|
+
const minDistance = radiusA + radiusB;
|
|
2830
|
+
if (distance >= minDistance) {
|
|
2831
|
+
return false;
|
|
2832
|
+
}
|
|
2833
|
+
const normal = distance > 1e-4 ? normalizeVec3(vec3(delta.x / distance, 0, delta.z / distance)) : normalizeVec3(vec3(Math.cos(state.time * 5.2), 0, Math.sin(state.time * 4.8)));
|
|
2834
|
+
const tangent = vec3(-normal.z, 0, normal.x);
|
|
2835
|
+
const penetration = minDistance - distance;
|
|
2836
|
+
const invMassA = getShipInverseMass(a, shipModelA);
|
|
2837
|
+
const invMassB = getShipInverseMass(b, shipModelB);
|
|
2838
|
+
const invMassSum = invMassA + invMassB;
|
|
2839
|
+
const correction = scaleVec3(normal, penetration / Math.max(1e-4, invMassSum) * 0.72);
|
|
2840
|
+
a.position = subVec3(a.position, scaleVec3(correction, invMassA));
|
|
2841
|
+
b.position = addVec3(b.position, scaleVec3(correction, invMassB));
|
|
2842
|
+
const relativeVelocity = subVec3(b.velocity, a.velocity);
|
|
2843
|
+
const velocityAlongNormal = dotVec3(relativeVelocity, normal);
|
|
2844
|
+
const restitution = (readPhysicsNumber(shipModelA.physics, "restitution", 0.22) + readPhysicsNumber(shipModelB.physics, "restitution", 0.22)) / 2 * 0.88;
|
|
2845
|
+
if (velocityAlongNormal < 0) {
|
|
2846
|
+
const impulseMagnitude = -(1 + restitution) * velocityAlongNormal / Math.max(1e-4, invMassSum);
|
|
2847
|
+
const impulse = scaleVec3(normal, impulseMagnitude);
|
|
2848
|
+
a.velocity = subVec3(a.velocity, scaleVec3(impulse, invMassA));
|
|
2849
|
+
b.velocity = addVec3(b.velocity, scaleVec3(impulse, invMassB));
|
|
2850
|
+
const tangentSpeed = dotVec3(relativeVelocity, tangent);
|
|
2851
|
+
const frictionMagnitude = clamp(
|
|
2852
|
+
-tangentSpeed / Math.max(1e-4, invMassSum),
|
|
2853
|
+
-impulseMagnitude * 0.16,
|
|
2854
|
+
impulseMagnitude * 0.16
|
|
2855
|
+
);
|
|
2856
|
+
const frictionImpulse = scaleVec3(tangent, frictionMagnitude);
|
|
2857
|
+
a.velocity = subVec3(a.velocity, scaleVec3(frictionImpulse, invMassA));
|
|
2858
|
+
b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
|
|
2859
|
+
a.angularVelocity -= tangentSpeed * radiusA * getShipInverseInertia(a, shipModelA) * 0.2 + impulseMagnitude * 24e-5;
|
|
2860
|
+
b.angularVelocity += tangentSpeed * radiusB * getShipInverseInertia(b, shipModelB) * 0.2 + impulseMagnitude * 24e-5;
|
|
2861
|
+
const impactSpeed = Math.abs(velocityAlongNormal);
|
|
2862
|
+
if (impactSpeed > 0.18 && Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0) {
|
|
2863
|
+
const contactPoint = vec3(
|
|
2864
|
+
(a.position.x + b.position.x) * 0.5,
|
|
2865
|
+
(a.position.y + b.position.y) * 0.5 + 0.14,
|
|
2866
|
+
(a.position.z + b.position.z) * 0.5
|
|
2867
|
+
);
|
|
2868
|
+
spawnSpray(state, contactPoint, impactSpeed * 2.4 + penetration * 8);
|
|
2869
|
+
state.waveImpulses.push({
|
|
2870
|
+
x: contactPoint.x,
|
|
2871
|
+
z: contactPoint.z,
|
|
2872
|
+
strength: clamp(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
|
|
2873
|
+
radius: 0.9 + penetration * 1.4,
|
|
2874
|
+
life: 1
|
|
2875
|
+
});
|
|
2876
|
+
state.collisionCount += 1;
|
|
2877
|
+
state.collisionFlash = Math.max(
|
|
2878
|
+
state.collisionFlash,
|
|
2879
|
+
clamp(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
|
|
2880
|
+
);
|
|
2881
|
+
a.collisionCooldown = 0.2;
|
|
2882
|
+
b.collisionCooldown = 0.2;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
state.contactCount += 1;
|
|
2886
|
+
return true;
|
|
2887
|
+
}
|
|
2888
|
+
function updateShips(state, dt, shipModel) {
|
|
2889
|
+
let collided = false;
|
|
2890
|
+
state.contactCount = 0;
|
|
2891
|
+
for (const ship of state.ships) {
|
|
2892
|
+
const activeShipModel = resolveShipModel(state, ship, shipModel);
|
|
2893
|
+
updateShipMotion(state, ship, dt, activeShipModel);
|
|
2894
|
+
resolveBoundaryCollision(ship, state, activeShipModel);
|
|
2895
|
+
}
|
|
2896
|
+
for (let index = 0; index < state.ships.length; index += 1) {
|
|
2897
|
+
for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
|
|
2898
|
+
const shipA = state.ships[index];
|
|
2899
|
+
const shipB = state.ships[otherIndex];
|
|
2900
|
+
const shipModelA = resolveShipModel(state, shipA, shipModel);
|
|
2901
|
+
const shipModelB = resolveShipModel(state, shipB, shipModel);
|
|
2902
|
+
collided = resolveShipCollision(state, shipA, shipB, shipModelA, shipModelB) || collided;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
state.collisionFlash = collided ? Math.max(0.12, state.collisionFlash) : Math.max(0, state.collisionFlash - dt * 1.3);
|
|
2906
|
+
}
|
|
2907
|
+
function updateWaveImpulses(state, dt) {
|
|
2908
|
+
state.waveImpulses = state.waveImpulses.map((impulse) => ({
|
|
2909
|
+
...impulse,
|
|
2910
|
+
life: impulse.life - dt * 0.55
|
|
2911
|
+
})).filter((impulse) => impulse.life > 0);
|
|
2912
|
+
}
|
|
2913
|
+
function updateSpray(state, dt) {
|
|
2914
|
+
state.sprays = state.sprays.map((particle) => {
|
|
2915
|
+
const nextVelocity = vec3(particle.velocity.x, particle.velocity.y - 4.2 * dt, particle.velocity.z);
|
|
2916
|
+
const nextPosition = addVec3(particle.position, scaleVec3(nextVelocity, dt));
|
|
2917
|
+
return {
|
|
2918
|
+
position: nextPosition,
|
|
2919
|
+
velocity: nextVelocity,
|
|
2920
|
+
life: particle.life - dt
|
|
2921
|
+
};
|
|
2922
|
+
}).filter((particle) => particle.life > 0 && particle.position.y > -0.2);
|
|
2923
|
+
}
|
|
2924
|
+
function recordTelemetry(state, frameTimeMs) {
|
|
2925
|
+
const frameId = `showcase-${state.frame}`;
|
|
2926
|
+
const quality = {
|
|
2927
|
+
fluid: state.fluidDetail.getSnapshot(),
|
|
2928
|
+
cloth: state.clothDetail.getSnapshot(),
|
|
2929
|
+
lighting: state.lightingDetail.getSnapshot()
|
|
2930
|
+
};
|
|
2931
|
+
const synthetic = frameTimeMs + state.sprays.length * 0.1 + (state.stress ? 6.5 : 0);
|
|
2932
|
+
const decision = state.governor.recordFrame({ frameTimeMs: synthetic });
|
|
2933
|
+
const queueDepth = Math.min(32, Math.round(6 + state.sprays.length / 2 + (state.stress ? 10 : 0)));
|
|
2934
|
+
const readyLaneDepth = Math.min(
|
|
2935
|
+
16,
|
|
2936
|
+
4 + Math.round(Math.max(0, Math.sin(state.time * 1.7 + 0.8)) * 9)
|
|
2937
|
+
);
|
|
2938
|
+
state.debugSession.recordQueue({
|
|
2939
|
+
owner: "renderer",
|
|
2940
|
+
queueClass: "render",
|
|
2941
|
+
depth: queueDepth,
|
|
2942
|
+
capacity: 32,
|
|
2943
|
+
frameId
|
|
2944
|
+
});
|
|
2945
|
+
state.debugSession.recordReadyLane({
|
|
2946
|
+
owner: "lighting",
|
|
2947
|
+
queueClass: "lighting",
|
|
2948
|
+
laneId: "critical",
|
|
2949
|
+
priority: 920,
|
|
2950
|
+
depth: readyLaneDepth,
|
|
2951
|
+
capacity: 16,
|
|
2952
|
+
frameId
|
|
2953
|
+
});
|
|
2954
|
+
state.debugSession.recordDispatch({
|
|
2955
|
+
owner: "lighting",
|
|
2956
|
+
queueClass: "lighting",
|
|
2957
|
+
jobType: "lighting.integration",
|
|
2958
|
+
frameId,
|
|
2959
|
+
durationMs: quality.lighting.currentLevel.estimatedCostMs ?? 1.2,
|
|
2960
|
+
workgroups: { x: quality.fluid.currentLevel.config.nearResolution, y: 1, z: 1 },
|
|
2961
|
+
workgroupSize: { x: 8, y: 8, z: 1 }
|
|
2962
|
+
});
|
|
2963
|
+
state.debugSession.recordDependencyUnlock({
|
|
2964
|
+
owner: "scene",
|
|
2965
|
+
queueClass: "render",
|
|
2966
|
+
sourceJobType: "physics.resolve",
|
|
2967
|
+
unlockedJobType: "lighting.integration",
|
|
2968
|
+
priority: 920,
|
|
2969
|
+
unlockCount: 2 + Math.round(Math.max(0, Math.sin(state.time * 1.1)) * 4),
|
|
2970
|
+
frameId
|
|
2971
|
+
});
|
|
2972
|
+
state.debugSession.recordPipelinePhase({
|
|
2973
|
+
owner: "scene",
|
|
2974
|
+
pipeline: "scene-preparation",
|
|
2975
|
+
stage: "stable-visual-snapshot",
|
|
2976
|
+
frameId,
|
|
2977
|
+
durationMs: synthetic * 0.38,
|
|
2978
|
+
snapshotAgeMs: Math.max(0, synthetic - 8)
|
|
2979
|
+
});
|
|
2980
|
+
state.debugSession.recordFrame({
|
|
2981
|
+
frameId,
|
|
2982
|
+
frameTimeMs: synthetic,
|
|
2983
|
+
targetFrameTimeMs: 16.67,
|
|
2984
|
+
gpuBusyMs: synthetic * 0.56,
|
|
2985
|
+
dropped: synthetic > 18
|
|
2986
|
+
});
|
|
2987
|
+
return decision;
|
|
2988
|
+
}
|
|
2989
|
+
function renderSprays(ctx, sprays, camera, viewport) {
|
|
2990
|
+
for (const spray of sprays) {
|
|
2991
|
+
const projected = projectPoint(spray.position, camera, viewport);
|
|
2992
|
+
if (!projected) {
|
|
2993
|
+
continue;
|
|
2994
|
+
}
|
|
2995
|
+
const radius = clamp(1 / projected.depth * 260, 1.5, 7.5);
|
|
2996
|
+
ctx.fillStyle = `rgba(225, 243, 250, ${clamp(spray.life / 1.6, 0, 0.9)})`;
|
|
2997
|
+
ctx.beginPath();
|
|
2998
|
+
ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
|
|
2999
|
+
ctx.fill();
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
function renderFlagPole(ctx, camera, viewport) {
|
|
3003
|
+
const base = projectPoint(vec3(-3.5, 0.7, 2.4), camera, viewport);
|
|
3004
|
+
const top = projectPoint(vec3(-3.5, 6.3, 2.4), camera, viewport);
|
|
3005
|
+
if (!base || !top) {
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
ctx.strokeStyle = "rgba(77, 52, 41, 0.95)";
|
|
3009
|
+
ctx.lineWidth = 6;
|
|
3010
|
+
ctx.beginPath();
|
|
3011
|
+
ctx.moveTo(base.x, base.y);
|
|
3012
|
+
ctx.lineTo(top.x, top.y);
|
|
3013
|
+
ctx.stroke();
|
|
3014
|
+
}
|
|
3015
|
+
function renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength) {
|
|
3016
|
+
const bounds = shipModel.bounds;
|
|
3017
|
+
const keelY = (shipModel.physics.waterline ?? 0.42) - 0.28;
|
|
3018
|
+
const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
|
|
3019
|
+
const hullCorners = [
|
|
3020
|
+
vec3(bounds.min[0], keelY, bounds.min[2]),
|
|
3021
|
+
vec3(bounds.max[0], keelY, bounds.min[2]),
|
|
3022
|
+
vec3(bounds.max[0], keelY, bounds.max[2]),
|
|
3023
|
+
vec3(bounds.min[0], keelY, bounds.max[2])
|
|
3024
|
+
].map((point) => transformPoint(point, transform));
|
|
3025
|
+
renderProjectedShadow(ctx, hullCorners, camera, viewport, lightDir, {
|
|
3026
|
+
planeY: sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 - 0.03,
|
|
3027
|
+
alpha: 0.08 + shadowStrength * 0.2,
|
|
3028
|
+
blur: 14 + shadowStrength * 24
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength) {
|
|
3032
|
+
const clothPoints = [
|
|
3033
|
+
cloth.positions[0],
|
|
3034
|
+
cloth.positions[cloth.grid.cols - 1],
|
|
3035
|
+
cloth.positions[cloth.positions.length - 1],
|
|
3036
|
+
cloth.positions[cloth.positions.length - cloth.grid.cols]
|
|
3037
|
+
];
|
|
3038
|
+
renderProjectedShadow(ctx, clothPoints, camera, viewport, lightDir, {
|
|
3039
|
+
planeY: 0.56,
|
|
3040
|
+
alpha: 0.05 + shadowStrength * 0.16,
|
|
3041
|
+
blur: 12 + shadowStrength * 20
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
function collectSceneLightSources(state, visuals) {
|
|
3045
|
+
const directLights = [];
|
|
3046
|
+
const reflectionLights = [];
|
|
3047
|
+
const pushLight = (point, glowScale, reflectionStrength, coreColor, glowColor) => {
|
|
3048
|
+
directLights.push(
|
|
3049
|
+
Object.freeze({
|
|
3050
|
+
pass: "direct-glow",
|
|
3051
|
+
point,
|
|
3052
|
+
coreColor,
|
|
3053
|
+
glowColor,
|
|
3054
|
+
glowScale
|
|
3055
|
+
})
|
|
3056
|
+
);
|
|
3057
|
+
if (reflectionStrength > 0) {
|
|
3058
|
+
reflectionLights.push(
|
|
3059
|
+
Object.freeze({
|
|
3060
|
+
pass: "water-reflection",
|
|
3061
|
+
point,
|
|
3062
|
+
coreColor,
|
|
3063
|
+
glowColor,
|
|
3064
|
+
glowScale,
|
|
3065
|
+
reflectionStrength
|
|
3066
|
+
})
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
};
|
|
3070
|
+
for (const torch of HARBOR_TORCHES) {
|
|
3071
|
+
pushLight(
|
|
3072
|
+
vec3(torch.x, torch.y, torch.z),
|
|
3073
|
+
torch.glow,
|
|
3074
|
+
visuals.lanternReflectionStrength * 0.55,
|
|
3075
|
+
visuals.torchCore,
|
|
3076
|
+
visuals.torchGlow
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
for (const ship of state.ships) {
|
|
3080
|
+
const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
|
|
3081
|
+
const strength = readVisualNumber(ship.lanternStrength, 1);
|
|
3082
|
+
for (const lantern of lanterns) {
|
|
3083
|
+
const point = transformPoint(
|
|
3084
|
+
vec3(lantern.x, lantern.y, lantern.z),
|
|
3085
|
+
{ position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
|
|
3086
|
+
);
|
|
3087
|
+
pushLight(
|
|
3088
|
+
point,
|
|
3089
|
+
lantern.glow * strength,
|
|
3090
|
+
visuals.lanternReflectionStrength,
|
|
3091
|
+
visuals.lanternCore,
|
|
3092
|
+
visuals.lanternGlow
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
return Object.freeze({
|
|
3097
|
+
directLights: Object.freeze(directLights),
|
|
3098
|
+
reflectionLights: Object.freeze(reflectionLights)
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
function renderDirectLightGlow(ctx, source, camera, viewport) {
|
|
3102
|
+
const projected = projectPoint(source.point, camera, viewport);
|
|
3103
|
+
if (!projected) {
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
|
|
3107
|
+
const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
|
|
3108
|
+
halo.addColorStop(0, colorToRgba(source.coreColor, 0.98));
|
|
3109
|
+
halo.addColorStop(0.5, colorToRgba(source.glowColor, 0.42));
|
|
3110
|
+
halo.addColorStop(1, colorToRgba(source.glowColor, 0));
|
|
3111
|
+
ctx.save();
|
|
3112
|
+
ctx.globalCompositeOperation = "screen";
|
|
3113
|
+
ctx.fillStyle = halo;
|
|
3114
|
+
ctx.beginPath();
|
|
3115
|
+
ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
|
|
3116
|
+
ctx.fill();
|
|
3117
|
+
ctx.restore();
|
|
3118
|
+
ctx.fillStyle = colorToRgba(source.coreColor, 0.98);
|
|
3119
|
+
ctx.beginPath();
|
|
3120
|
+
ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
|
|
3121
|
+
ctx.fill();
|
|
3122
|
+
}
|
|
3123
|
+
function renderWaterLightReflection(ctx, source, state, camera, viewport) {
|
|
3124
|
+
const projected = projectPoint(source.point, camera, viewport);
|
|
3125
|
+
if (!projected) {
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
|
|
3129
|
+
const waterline = sampleWave(state, source.point.x, source.point.z, state.time) * 0.22;
|
|
3130
|
+
const reflectedPoint = vec3(
|
|
3131
|
+
source.point.x,
|
|
3132
|
+
waterline - (source.point.y - waterline) * 0.58,
|
|
3133
|
+
source.point.z + 0.08
|
|
3134
|
+
);
|
|
3135
|
+
const reflected = projectPoint(reflectedPoint, camera, viewport);
|
|
3136
|
+
if (!reflected) {
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
const reflectionRadius = radius * 0.72;
|
|
3140
|
+
const glow = ctx.createRadialGradient(
|
|
3141
|
+
reflected.x,
|
|
3142
|
+
reflected.y,
|
|
3143
|
+
reflectionRadius * 0.1,
|
|
3144
|
+
reflected.x,
|
|
3145
|
+
reflected.y,
|
|
3146
|
+
reflectionRadius
|
|
3147
|
+
);
|
|
3148
|
+
glow.addColorStop(0, colorToRgba(source.coreColor, source.reflectionStrength * 0.34));
|
|
3149
|
+
glow.addColorStop(1, colorToRgba(source.glowColor, 0));
|
|
3150
|
+
ctx.save();
|
|
3151
|
+
ctx.globalCompositeOperation = "screen";
|
|
3152
|
+
ctx.fillStyle = glow;
|
|
3153
|
+
ctx.beginPath();
|
|
3154
|
+
ctx.ellipse(
|
|
3155
|
+
reflected.x,
|
|
3156
|
+
reflected.y,
|
|
3157
|
+
reflectionRadius * 0.34,
|
|
3158
|
+
reflectionRadius,
|
|
3159
|
+
0,
|
|
3160
|
+
0,
|
|
3161
|
+
Math.PI * 2
|
|
3162
|
+
);
|
|
3163
|
+
ctx.fill();
|
|
3164
|
+
ctx.restore();
|
|
3165
|
+
}
|
|
3166
|
+
function renderLighthouseBeam(ctx, state, camera, viewport, visuals) {
|
|
3167
|
+
const lighthousePlacement = SHOWCASE_ENVIRONMENT_LAYOUT.find(
|
|
3168
|
+
(placement) => placement.assetKey === "lighthouse"
|
|
3169
|
+
);
|
|
3170
|
+
if (!lighthousePlacement || !state.showcaseRealisticModelsEnabled) {
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
const source = transformPoint(
|
|
3174
|
+
vec3(0, 11.34, 0),
|
|
3175
|
+
{
|
|
3176
|
+
position: vec3(
|
|
3177
|
+
lighthousePlacement.position.x,
|
|
3178
|
+
lighthousePlacement.position.y,
|
|
3179
|
+
lighthousePlacement.position.z
|
|
3180
|
+
),
|
|
3181
|
+
rotationY: lighthousePlacement.rotationY,
|
|
3182
|
+
scale: lighthousePlacement.scale
|
|
3183
|
+
}
|
|
3184
|
+
);
|
|
3185
|
+
const sweep = state.time * 0.22 + 0.8;
|
|
3186
|
+
const direction = normalizeVec3(vec3(Math.sin(sweep), -0.07, Math.cos(sweep)));
|
|
3187
|
+
const spread = perpendicularOnWater(direction);
|
|
3188
|
+
const farCenter = addVec3(source, scaleVec3(direction, 34));
|
|
3189
|
+
const left = addVec3(farCenter, scaleVec3(spread, 7.4));
|
|
3190
|
+
const right = addVec3(farCenter, scaleVec3(spread, -7.4));
|
|
3191
|
+
const projectedSource = projectPoint(source, camera, viewport);
|
|
3192
|
+
const projectedLeft = projectPoint(left, camera, viewport);
|
|
3193
|
+
const projectedRight = projectPoint(right, camera, viewport);
|
|
3194
|
+
if (!projectedSource || !projectedLeft || !projectedRight) {
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
const pulse = 0.72 + Math.sin(state.time * 1.7) * 0.08;
|
|
3198
|
+
ctx.save();
|
|
3199
|
+
ctx.globalCompositeOperation = "screen";
|
|
3200
|
+
ctx.fillStyle = colorToRgba(visuals.torchCore, 0.055 * pulse);
|
|
3201
|
+
ctx.beginPath();
|
|
3202
|
+
ctx.moveTo(projectedSource.x, projectedSource.y);
|
|
3203
|
+
ctx.lineTo(projectedLeft.x, projectedLeft.y);
|
|
3204
|
+
ctx.lineTo(projectedRight.x, projectedRight.y);
|
|
3205
|
+
ctx.closePath();
|
|
3206
|
+
ctx.fill();
|
|
3207
|
+
const beamLength = Math.hypot(
|
|
3208
|
+
projectedLeft.x - projectedSource.x,
|
|
3209
|
+
projectedLeft.y - projectedSource.y
|
|
3210
|
+
);
|
|
3211
|
+
const core = ctx.createRadialGradient(
|
|
3212
|
+
projectedSource.x,
|
|
3213
|
+
projectedSource.y,
|
|
3214
|
+
2,
|
|
3215
|
+
projectedSource.x,
|
|
3216
|
+
projectedSource.y,
|
|
3217
|
+
clamp(beamLength * 0.22, 18, 80)
|
|
3218
|
+
);
|
|
3219
|
+
core.addColorStop(0, colorToRgba(visuals.torchCore, 0.58));
|
|
3220
|
+
core.addColorStop(0.5, colorToRgba(visuals.torchGlow, 0.18));
|
|
3221
|
+
core.addColorStop(1, colorToRgba(visuals.torchGlow, 0));
|
|
3222
|
+
ctx.fillStyle = core;
|
|
3223
|
+
ctx.beginPath();
|
|
3224
|
+
ctx.arc(projectedSource.x, projectedSource.y, clamp(beamLength * 0.18, 14, 64), 0, Math.PI * 2);
|
|
3225
|
+
ctx.fill();
|
|
3226
|
+
ctx.restore();
|
|
3227
|
+
}
|
|
3228
|
+
function renderAtmosphericGrade(ctx, canvas, state, visuals) {
|
|
3229
|
+
const vignette = ctx.createRadialGradient(
|
|
3230
|
+
canvas.width * 0.5,
|
|
3231
|
+
canvas.height * 0.48,
|
|
3232
|
+
canvas.width * 0.2,
|
|
3233
|
+
canvas.width * 0.5,
|
|
3234
|
+
canvas.height * 0.5,
|
|
3235
|
+
canvas.width * 0.72
|
|
3236
|
+
);
|
|
3237
|
+
vignette.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
3238
|
+
vignette.addColorStop(0.68, "rgba(0, 0, 0, 0.08)");
|
|
3239
|
+
vignette.addColorStop(1, "rgba(0, 0, 0, 0.32)");
|
|
3240
|
+
ctx.fillStyle = vignette;
|
|
3241
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
3242
|
+
const seaHaze = ctx.createLinearGradient(0, canvas.height * 0.34, 0, canvas.height);
|
|
3243
|
+
seaHaze.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
3244
|
+
seaHaze.addColorStop(0.5, visuals.ambientMist);
|
|
3245
|
+
seaHaze.addColorStop(1, "rgba(3, 8, 16, 0.18)");
|
|
3246
|
+
ctx.fillStyle = seaHaze;
|
|
3247
|
+
ctx.fillRect(0, canvas.height * 0.34, canvas.width, canvas.height * 0.66);
|
|
3248
|
+
if (state.captureMode) {
|
|
3249
|
+
ctx.save();
|
|
3250
|
+
ctx.globalCompositeOperation = "screen";
|
|
3251
|
+
for (let index = 0; index < 70; index += 1) {
|
|
3252
|
+
const x = pseudoRandom(index * 19 + 3) * canvas.width;
|
|
3253
|
+
const y = pseudoRandom(index * 23 + 7) * canvas.height;
|
|
3254
|
+
const alpha = 8e-3 + pseudoRandom(index * 31 + 11) * 0.012;
|
|
3255
|
+
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
|
|
3256
|
+
ctx.fillRect(x, y, 1.1, 1.1);
|
|
3257
|
+
}
|
|
3258
|
+
ctx.restore();
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
function renderWaterMotionEffects(ctx, effects, camera, viewport) {
|
|
3262
|
+
ctx.save();
|
|
3263
|
+
ctx.globalCompositeOperation = "screen";
|
|
3264
|
+
for (const wake of effects.wakeTrails) {
|
|
3265
|
+
const projected = wake.points.map((point) => ({
|
|
3266
|
+
projected: projectPoint(point.center, camera, viewport),
|
|
3267
|
+
width: point.width
|
|
3268
|
+
})).filter((entry) => entry.projected);
|
|
3269
|
+
if (projected.length < 2) {
|
|
3270
|
+
continue;
|
|
3271
|
+
}
|
|
3272
|
+
const averageDepth = projected.reduce((total, entry) => total + entry.projected.depth, 0) / projected.length;
|
|
3273
|
+
const averageWidth = projected.reduce((total, entry) => total + entry.width, 0) / projected.length;
|
|
3274
|
+
const baseWidth = clamp(averageWidth / Math.max(0.25, averageDepth) * 180, 1.6, 5.4);
|
|
3275
|
+
ctx.strokeStyle = `rgba(146, 194, 236, ${wake.opacity * 0.52})`;
|
|
3276
|
+
ctx.lineWidth = baseWidth * 1.9;
|
|
3277
|
+
ctx.lineCap = "round";
|
|
3278
|
+
ctx.lineJoin = "round";
|
|
3279
|
+
ctx.beginPath();
|
|
3280
|
+
ctx.moveTo(projected[0].projected.x, projected[0].projected.y);
|
|
3281
|
+
for (let index = 1; index < projected.length; index += 1) {
|
|
3282
|
+
ctx.lineTo(projected[index].projected.x, projected[index].projected.y);
|
|
3283
|
+
}
|
|
3284
|
+
ctx.stroke();
|
|
3285
|
+
ctx.strokeStyle = `rgba(234, 247, 255, ${wake.opacity})`;
|
|
3286
|
+
ctx.lineWidth = baseWidth;
|
|
3287
|
+
ctx.lineCap = "round";
|
|
3288
|
+
ctx.lineJoin = "round";
|
|
3289
|
+
ctx.beginPath();
|
|
3290
|
+
ctx.moveTo(projected[0].projected.x, projected[0].projected.y);
|
|
3291
|
+
for (let index = 1; index < projected.length; index += 1) {
|
|
3292
|
+
ctx.lineTo(projected[index].projected.x, projected[index].projected.y);
|
|
3293
|
+
}
|
|
3294
|
+
ctx.stroke();
|
|
3295
|
+
for (const entry of projected.slice(1, 5)) {
|
|
3296
|
+
ctx.fillStyle = `rgba(239, 248, 255, ${wake.opacity * 0.76})`;
|
|
3297
|
+
ctx.beginPath();
|
|
3298
|
+
ctx.ellipse(
|
|
3299
|
+
entry.projected.x,
|
|
3300
|
+
entry.projected.y,
|
|
3301
|
+
baseWidth * 0.72,
|
|
3302
|
+
baseWidth * 0.44,
|
|
3303
|
+
0,
|
|
3304
|
+
0,
|
|
3305
|
+
Math.PI * 2
|
|
3306
|
+
);
|
|
3307
|
+
ctx.fill();
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
for (const ring of effects.rippleRings) {
|
|
3311
|
+
const center = projectPoint(ring.center, camera, viewport);
|
|
3312
|
+
const xAxis = projectPoint(addVec3(ring.center, vec3(ring.radius, 0, 0)), camera, viewport);
|
|
3313
|
+
const zAxis = projectPoint(addVec3(ring.center, vec3(0, 0, ring.radius)), camera, viewport);
|
|
3314
|
+
if (!center || !xAxis || !zAxis) {
|
|
3315
|
+
continue;
|
|
3316
|
+
}
|
|
3317
|
+
const radiusX = Math.hypot(xAxis.x - center.x, xAxis.y - center.y);
|
|
3318
|
+
const radiusY = Math.hypot(zAxis.x - center.x, zAxis.y - center.y);
|
|
3319
|
+
ctx.strokeStyle = `rgba(216, 235, 255, ${ring.opacity})`;
|
|
3320
|
+
ctx.lineWidth = clamp((radiusX + radiusY) * 0.02, 1, 3.1);
|
|
3321
|
+
ctx.beginPath();
|
|
3322
|
+
ctx.ellipse(center.x, center.y, radiusX, radiusY, 0, 0, Math.PI * 2);
|
|
3323
|
+
ctx.stroke();
|
|
3324
|
+
}
|
|
3325
|
+
ctx.restore();
|
|
3326
|
+
}
|
|
3327
|
+
function renderScene(ctx, canvas, state, shipModel, dom, lightingFeatures, fluidFeatures, clothFeatures) {
|
|
3328
|
+
const viewport = { width: canvas.width, height: canvas.height };
|
|
3329
|
+
const camera = buildCamera(state, canvas);
|
|
3330
|
+
state.camera.eye = camera.eye;
|
|
3331
|
+
const lightingPlan = lightingFeatures.createBandPlan({
|
|
3332
|
+
profile: state.focus === "lighting" ? lightingFeatures.defaultProfile : lightingFeatures.getProfile(lightingFeatures.defaultProfile).name,
|
|
3333
|
+
importance: state.focus === "lighting" ? "critical" : "high"
|
|
3334
|
+
});
|
|
3335
|
+
const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
|
|
3336
|
+
const lightDir = normalizeVec3(vec3(-0.22, 0.94, -0.31));
|
|
3337
|
+
const lightingSnapshot = state.lightingDetail.getSnapshot();
|
|
3338
|
+
const visuals = resolveVisualConfig(
|
|
3339
|
+
nearLighting,
|
|
3340
|
+
lightingSnapshot,
|
|
3341
|
+
state.demoDescription?.visuals
|
|
3342
|
+
);
|
|
3343
|
+
state.demoVisuals = visuals;
|
|
3344
|
+
const reflectionStrength = visuals.reflectionStrength;
|
|
3345
|
+
const shadowStrength = visuals.shadowAccent;
|
|
3346
|
+
drawSkyAndShore(
|
|
3347
|
+
ctx,
|
|
3348
|
+
canvas,
|
|
3349
|
+
state,
|
|
3350
|
+
nearLighting,
|
|
3351
|
+
reflectionStrength,
|
|
3352
|
+
shadowStrength,
|
|
3353
|
+
visuals
|
|
3354
|
+
);
|
|
3355
|
+
const waterTriangles = [];
|
|
3356
|
+
const sceneTriangles = [];
|
|
3357
|
+
const water = buildWaterBands(
|
|
3358
|
+
state,
|
|
3359
|
+
state.fluidDetail.getSnapshot().currentLevel.config,
|
|
3360
|
+
visuals,
|
|
3361
|
+
fluidFeatures
|
|
3362
|
+
);
|
|
3363
|
+
for (const bandMesh of water.bandMeshes) {
|
|
3364
|
+
const bandAccent = bandMesh.band === "near" ? 0.06 : bandMesh.band === "mid" ? 0.04 : 0;
|
|
3365
|
+
for (let index = 0; index < bandMesh.indices.length; index += 3) {
|
|
3366
|
+
const a = bandMesh.positions[bandMesh.indices[index]];
|
|
3367
|
+
const b = bandMesh.positions[bandMesh.indices[index + 1]];
|
|
3368
|
+
const c = bandMesh.positions[bandMesh.indices[index + 2]];
|
|
3369
|
+
const normal = normalizeVec3(crossVec3(subVec3(b, a), subVec3(c, a)));
|
|
3370
|
+
const projected = [projectPoint(a, camera, viewport), projectPoint(b, camera, viewport), projectPoint(c, camera, viewport)];
|
|
3371
|
+
if (projected.some((value) => value === null)) {
|
|
3372
|
+
continue;
|
|
3373
|
+
}
|
|
3374
|
+
waterTriangles.push({
|
|
3375
|
+
points: projected,
|
|
3376
|
+
depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
|
|
3377
|
+
worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
|
|
3378
|
+
normal,
|
|
3379
|
+
baseColor: bandMesh.color,
|
|
3380
|
+
accent: bandAccent,
|
|
3381
|
+
material: {
|
|
3382
|
+
name: "water-surface",
|
|
3383
|
+
color: bandMesh.color,
|
|
3384
|
+
roughness: 0.2,
|
|
3385
|
+
metallic: 0,
|
|
3386
|
+
emissive: { r: 0, g: 0, b: 0 }
|
|
3387
|
+
},
|
|
3388
|
+
reflection: 1,
|
|
3389
|
+
surfaceType: "water"
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
const waterMotionEffects = buildWaterMotionEffects(state);
|
|
3394
|
+
const lightSources = collectSceneLightSources(state, visuals);
|
|
3395
|
+
pushHarborGeometry(camera, viewport, sceneTriangles, state);
|
|
3396
|
+
const cloth = buildClothSurface(
|
|
3397
|
+
state,
|
|
3398
|
+
state,
|
|
3399
|
+
state.clothDetail.getSnapshot().currentLevel.config,
|
|
3400
|
+
visuals,
|
|
3401
|
+
clothFeatures
|
|
3402
|
+
);
|
|
3403
|
+
for (let index = 0; index < cloth.indices.length; index += 3) {
|
|
3404
|
+
const a = cloth.positions[cloth.indices[index]];
|
|
3405
|
+
const b = cloth.positions[cloth.indices[index + 1]];
|
|
3406
|
+
const c = cloth.positions[cloth.indices[index + 2]];
|
|
3407
|
+
const normal = normalizeVec3(crossVec3(subVec3(b, a), subVec3(c, a)));
|
|
3408
|
+
const projected = [projectPoint(a, camera, viewport), projectPoint(b, camera, viewport), projectPoint(c, camera, viewport)];
|
|
3409
|
+
if (projected.some((value) => value === null)) {
|
|
3410
|
+
continue;
|
|
3411
|
+
}
|
|
3412
|
+
sceneTriangles.push({
|
|
3413
|
+
points: projected,
|
|
3414
|
+
depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
|
|
3415
|
+
worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
|
|
3416
|
+
normal,
|
|
3417
|
+
baseColor: cloth.color,
|
|
3418
|
+
accent: cloth.band === "near" ? 0.1 : 0.04,
|
|
3419
|
+
material: {
|
|
3420
|
+
name: "flag-cloth",
|
|
3421
|
+
color: cloth.color,
|
|
3422
|
+
roughness: 0.94,
|
|
3423
|
+
metallic: 0,
|
|
3424
|
+
emissive: { r: 0, g: 0, b: 0 }
|
|
3425
|
+
},
|
|
3426
|
+
reflection: 0,
|
|
3427
|
+
surfaceType: "cloth"
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
for (const ship of state.ships) {
|
|
3431
|
+
const activeShipModel = resolveShipModel(state, ship, shipModel);
|
|
3432
|
+
buildTrianglesFromMesh(
|
|
3433
|
+
activeShipModel,
|
|
3434
|
+
{ position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
|
|
3435
|
+
ship.tint,
|
|
3436
|
+
camera,
|
|
3437
|
+
viewport,
|
|
3438
|
+
sceneTriangles,
|
|
3439
|
+
{
|
|
3440
|
+
accent: nearLighting.rtParticipation.directShadows === "premium" ? 0.08 : 0.02,
|
|
3441
|
+
reflection: 0,
|
|
3442
|
+
surfaceType: "ship"
|
|
3443
|
+
}
|
|
3444
|
+
);
|
|
3445
|
+
}
|
|
3446
|
+
drawTriangles(ctx, waterTriangles, lightDir, reflectionStrength, camera, shadowStrength);
|
|
3447
|
+
for (const ship of state.ships) {
|
|
3448
|
+
renderShipShadow(
|
|
3449
|
+
ctx,
|
|
3450
|
+
resolveShipModel(state, ship, shipModel),
|
|
3451
|
+
ship,
|
|
3452
|
+
state,
|
|
3453
|
+
camera,
|
|
3454
|
+
viewport,
|
|
3455
|
+
lightDir,
|
|
3456
|
+
shadowStrength
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
|
|
3460
|
+
for (const source of lightSources.reflectionLights) {
|
|
3461
|
+
renderWaterLightReflection(ctx, source, state, camera, viewport);
|
|
3462
|
+
}
|
|
3463
|
+
renderWaterMotionEffects(ctx, waterMotionEffects, camera, viewport);
|
|
3464
|
+
renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
|
|
3465
|
+
drawTriangles(
|
|
3466
|
+
ctx,
|
|
3467
|
+
sceneTriangles,
|
|
3468
|
+
lightDir,
|
|
3469
|
+
reflectionStrength,
|
|
3470
|
+
camera,
|
|
3471
|
+
shadowStrength,
|
|
3472
|
+
lightSources.directLights
|
|
3473
|
+
);
|
|
3474
|
+
renderFlagPole(ctx, camera, viewport);
|
|
3475
|
+
renderClothAccent(ctx, cloth, camera, viewport);
|
|
3476
|
+
renderLighthouseBeam(ctx, state, camera, viewport, visuals);
|
|
3477
|
+
for (const source of lightSources.directLights) {
|
|
3478
|
+
renderDirectLightGlow(ctx, source, camera, viewport);
|
|
3479
|
+
}
|
|
3480
|
+
for (const ship of state.ships) {
|
|
3481
|
+
renderShipRigging(ctx, ship, camera, viewport);
|
|
3482
|
+
}
|
|
3483
|
+
renderSprays(ctx, state.sprays, camera, viewport);
|
|
3484
|
+
renderAtmosphericGrade(ctx, canvas, state, visuals);
|
|
3485
|
+
const debugSnapshot = state.debugSession.getSnapshot();
|
|
3486
|
+
const quality = {
|
|
3487
|
+
fluid: state.fluidDetail.getSnapshot(),
|
|
3488
|
+
cloth: state.clothDetail.getSnapshot(),
|
|
3489
|
+
lighting: lightingSnapshot
|
|
3490
|
+
};
|
|
3491
|
+
const sceneMetrics = [
|
|
3492
|
+
`focus: ${state.focus}`,
|
|
3493
|
+
`ships: ${state.ships.length} active GLTF hulls across ${new Set(state.ships.map((ship) => ship.modelKey)).size} model families`,
|
|
3494
|
+
`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`,
|
|
3495
|
+
`physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
|
|
3496
|
+
`physics contacts: ${state.contactCount}`,
|
|
3497
|
+
`mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, resolveShipModel(state, ship, shipModel)) / 1e3).toFixed(1)}t`).join(" \xB7 ")}`,
|
|
3498
|
+
`cloth band: ${cloth.band} -> ${cloth.representation.output}`,
|
|
3499
|
+
`fluid near band: ${water.bandMeshes[0].representation.output}`,
|
|
3500
|
+
`lighting profile: ${lightingPlan.profile} (${lightingFeatures.distanceBands.length} bands)`
|
|
3501
|
+
];
|
|
3502
|
+
const qualityMetrics = [
|
|
3503
|
+
`fluid detail: ${quality.fluid.currentLevel.id} (${quality.fluid.currentLevel.config.nearResolution} near cells)`,
|
|
3504
|
+
`cloth detail: ${quality.cloth.currentLevel.id} (${quality.cloth.currentLevel.config.cols}x${quality.cloth.currentLevel.config.rows})`,
|
|
3505
|
+
`lighting detail: ${quality.lighting.currentLevel.id}`,
|
|
3506
|
+
`near shadows: ${nearLighting.primaryShadowSource}`,
|
|
3507
|
+
`near reflections: ${nearLighting.rtParticipation.reflections}`,
|
|
3508
|
+
`governor pressure: ${state.lastDecision.pressureLevel}`,
|
|
3509
|
+
`frame avg: ${state.lastDecision.metrics.averageFrameTimeMs.toFixed(2)} ms`
|
|
3510
|
+
];
|
|
3511
|
+
const debugMetrics = [
|
|
3512
|
+
`queue samples: ${debugSnapshot.queues.sampleCount}`,
|
|
3513
|
+
`dispatch avg: ${(debugSnapshot.dispatch.averageDurationMs ?? 0).toFixed(2)} ms`,
|
|
3514
|
+
`ready lane peak: ${debugSnapshot.dag.peakReadyLaneDepth.toFixed(1)}`,
|
|
3515
|
+
`pipeline samples: ${debugSnapshot.pipeline.sampleCount}`,
|
|
3516
|
+
`tracked memory: ${(debugSnapshot.memory.totalTrackedBytes / (1024 * 1024)).toFixed(1)} MB`
|
|
3517
|
+
];
|
|
3518
|
+
const sceneNotes = state.focus === "physics" ? PHYSICS_SCENE_NOTE_KEYS.map((key) => state.translate(key)) : SCENE_NOTE_KEYS.map((key) => state.translate(key));
|
|
3519
|
+
const custom = state.demoDescription ?? null;
|
|
3520
|
+
setListContent(
|
|
3521
|
+
dom.sceneMetrics,
|
|
3522
|
+
Array.isArray(custom?.sceneMetrics) ? custom.sceneMetrics : sceneMetrics
|
|
3523
|
+
);
|
|
3524
|
+
setListContent(
|
|
3525
|
+
dom.qualityMetrics,
|
|
3526
|
+
Array.isArray(custom?.qualityMetrics) ? custom.qualityMetrics : qualityMetrics
|
|
3527
|
+
);
|
|
3528
|
+
setListContent(
|
|
3529
|
+
dom.debugMetrics,
|
|
3530
|
+
Array.isArray(custom?.debugMetrics) ? custom.debugMetrics : debugMetrics
|
|
3531
|
+
);
|
|
3532
|
+
setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
|
|
3533
|
+
dom.status.textContent = typeof custom?.status === "string" ? custom.status : state.translate(gpuSharedTranslationKeys.statusLive, {
|
|
3534
|
+
fps: state.lastDecision.metrics.fps.toFixed(1)
|
|
3535
|
+
});
|
|
3536
|
+
dom.details.textContent = typeof custom?.details === "string" ? custom.details : state.focus === "physics" ? state.translate(gpuSharedTranslationKeys.detailsPhysics, {
|
|
3537
|
+
snapshotStageId: state.physics.plan.snapshotStageId
|
|
3538
|
+
}) : state.showcaseRealisticModelsEnabled ? state.translate(gpuSharedTranslationKeys.detailsRealistic, {
|
|
3539
|
+
pressureLevel: state.lastDecision.pressureLevel
|
|
3540
|
+
}) : state.translate(gpuSharedTranslationKeys.detailsLegacy, {
|
|
3541
|
+
pressureLevel: state.lastDecision.pressureLevel
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
function updateSceneState(state, dt, shipModel, featureAdapters) {
|
|
3545
|
+
updateShips(state, dt, shipModel);
|
|
3546
|
+
updateWaveImpulses(state, dt);
|
|
3547
|
+
updateSpray(state, dt);
|
|
3548
|
+
const clothPresentation = resolveClothPresentation(
|
|
3549
|
+
state,
|
|
3550
|
+
state.clothDetail.getSnapshot().currentLevel.config,
|
|
3551
|
+
featureAdapters.cloth
|
|
3552
|
+
);
|
|
3553
|
+
const clothState = ensureShowcaseClothState(
|
|
3554
|
+
state,
|
|
3555
|
+
state.clothDetail.getSnapshot().currentLevel.config,
|
|
3556
|
+
clothPresentation
|
|
3557
|
+
);
|
|
3558
|
+
advanceShowcaseClothSimulationState(clothState, {
|
|
3559
|
+
dt,
|
|
3560
|
+
time: state.time,
|
|
3561
|
+
flagMotion: readVisualNumber(state.demoVisuals?.flagMotion, 0.92),
|
|
3562
|
+
waveInfluence: sampleWave(state, FLAG_LAYOUT.origin.x + FLAG_LAYOUT.width * 0.55, FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * 0.48, state.time)
|
|
3563
|
+
});
|
|
3564
|
+
updatePhysicsSnapshot(state, shipModel, featureAdapters.physics);
|
|
3565
|
+
}
|
|
3566
|
+
function syncTextState(state, shipModel, featureAdapters) {
|
|
3567
|
+
const snapshot = {
|
|
3568
|
+
coordinateSystem: "right-handed world; +x right, +y up, +z forward from the shore",
|
|
3569
|
+
focus: state.focus,
|
|
3570
|
+
stress: state.stress,
|
|
3571
|
+
ships: state.ships.map((ship) => ({
|
|
3572
|
+
id: ship.id,
|
|
3573
|
+
modelKey: ship.modelKey ?? "brigantine",
|
|
3574
|
+
x: Number(ship.position.x.toFixed(2)),
|
|
3575
|
+
y: Number(ship.position.y.toFixed(2)),
|
|
3576
|
+
z: Number(ship.position.z.toFixed(2)),
|
|
3577
|
+
vx: Number(ship.velocity.x.toFixed(2)),
|
|
3578
|
+
vz: Number(ship.velocity.z.toFixed(2)),
|
|
3579
|
+
massKg: Math.round(getShipMass(ship, resolveShipModel(state, ship, shipModel))),
|
|
3580
|
+
lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0
|
|
3581
|
+
})),
|
|
3582
|
+
shipPhysics: Object.fromEntries(
|
|
3583
|
+
state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
|
|
3584
|
+
),
|
|
3585
|
+
sprays: state.sprays.length,
|
|
3586
|
+
waveImpulses: state.waveImpulses.length,
|
|
3587
|
+
pressure: state.lastDecision?.pressureLevel ?? "stable",
|
|
3588
|
+
physics: {
|
|
3589
|
+
profile: state.physics.profile,
|
|
3590
|
+
snapshotStageId: state.physics.plan.snapshotStageId,
|
|
3591
|
+
workerJobCount: state.physics.manifest.jobs.length,
|
|
3592
|
+
snapshot: state.physics.snapshot
|
|
3593
|
+
},
|
|
3594
|
+
package: state.demoDescription?.textState ?? null
|
|
3595
|
+
};
|
|
3596
|
+
window.render_game_to_text = () => JSON.stringify(snapshot);
|
|
3597
|
+
window.advanceTime = (ms) => {
|
|
3598
|
+
const step = Math.max(1, Math.round(ms / (1e3 / 60)));
|
|
3599
|
+
for (let index = 0; index < step; index += 1) {
|
|
3600
|
+
state.frame += 1;
|
|
3601
|
+
state.time += 1 / 60;
|
|
3602
|
+
updateSceneState(state, 1 / 60, shipModel, featureAdapters);
|
|
3603
|
+
state.lastDecision = recordTelemetry(state, 16.67 + (state.stress ? 6.5 : 0));
|
|
3604
|
+
}
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
async function mountGpuShowcase(options = {}, featureFlags = null) {
|
|
3608
|
+
const featureAdapters = await resolveShowcaseFeatureAdapters(options);
|
|
3609
|
+
injectStyles();
|
|
3610
|
+
const root = options.root ?? document.body;
|
|
3611
|
+
root.classList?.add?.(ROOT_CLASS);
|
|
3612
|
+
const captureSettings = resolveCaptureSettings(options);
|
|
3613
|
+
if (captureSettings.captureMode) {
|
|
3614
|
+
root.classList?.add?.(CAPTURE_CLASS);
|
|
3615
|
+
}
|
|
3616
|
+
const previousMarkup = root.innerHTML;
|
|
3617
|
+
const previousRenderGameToText = window.render_game_to_text;
|
|
3618
|
+
const previousAdvanceTime = window.advanceTime;
|
|
3619
|
+
const focus = options.focus ?? new URLSearchParams(window.location.search).get("focus") ?? "integrated";
|
|
3620
|
+
const translate = createGpuSharedTranslator(options.translate);
|
|
3621
|
+
const dom = buildDemoDom(root, {
|
|
3622
|
+
packageName: options.packageName ?? "@plasius/gpu-demo-viewer",
|
|
3623
|
+
title: options.title ?? translate(gpuSharedTranslationKeys.showcaseTitle),
|
|
3624
|
+
subtitle: options.subtitle ?? translate(gpuSharedTranslationKeys.showcaseSubtitle),
|
|
3625
|
+
translate
|
|
3626
|
+
});
|
|
3627
|
+
dom.focusMode.value = focus;
|
|
3628
|
+
const state = createSceneState(
|
|
3629
|
+
{
|
|
3630
|
+
focus,
|
|
3631
|
+
translate,
|
|
3632
|
+
realisticModelsEnabled: isFeatureEnabled(featureFlags, GPU_SHOWCASE_REALISTIC_MODELS_FEATURE, true),
|
|
3633
|
+
captureMode: captureSettings.captureMode,
|
|
3634
|
+
renderScale: captureSettings.renderScale
|
|
3635
|
+
},
|
|
3636
|
+
featureAdapters
|
|
3637
|
+
);
|
|
3638
|
+
const assetCatalog = await (state.showcaseRealisticModelsEnabled ? loadShowcaseAssetCatalog() : createLegacyShowcaseAssetCatalog());
|
|
3639
|
+
const shipModel = assetCatalog.ships[assetCatalog.primaryShipKey];
|
|
3640
|
+
state.assetCatalog = assetCatalog;
|
|
3641
|
+
state.shipModel = shipModel;
|
|
3642
|
+
state.packageState = typeof options.createState === "function" ? options.createState() : void 0;
|
|
3643
|
+
updatePhysicsSnapshot(state, shipModel, featureAdapters.physics);
|
|
3644
|
+
state.lastDecision = recordTelemetry(state, 16.4);
|
|
3645
|
+
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
3646
|
+
syncTextState(state, shipModel, featureAdapters);
|
|
3647
|
+
const ctx = dom.canvas.getContext("2d");
|
|
3648
|
+
if (!ctx) {
|
|
3649
|
+
throw new Error("2D canvas context is required for the shared showcase.");
|
|
3650
|
+
}
|
|
3651
|
+
ctx.imageSmoothingEnabled = true;
|
|
3652
|
+
ctx.imageSmoothingQuality = "high";
|
|
3653
|
+
let animationFrameId = 0;
|
|
3654
|
+
let destroyed = false;
|
|
3655
|
+
const renderFrame = (nowMs) => {
|
|
3656
|
+
if (destroyed) {
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
if (!state.paused) {
|
|
3660
|
+
if (state.lastTimeMs == null) {
|
|
3661
|
+
state.lastTimeMs = nowMs;
|
|
3662
|
+
}
|
|
3663
|
+
const dt = Math.min(0.033, (nowMs - state.lastTimeMs) / 1e3);
|
|
3664
|
+
state.lastTimeMs = nowMs;
|
|
3665
|
+
state.time += dt;
|
|
3666
|
+
state.frame += 1;
|
|
3667
|
+
updateSceneState(state, dt, shipModel, featureAdapters);
|
|
3668
|
+
updatePackageState(state, options, shipModel, dt);
|
|
3669
|
+
const syntheticFrame = 14.2 + state.sprays.length * 0.1 + (state.stress ? 6.4 : 0);
|
|
3670
|
+
state.lastDecision = recordTelemetry(state, syntheticFrame);
|
|
3671
|
+
}
|
|
3672
|
+
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
3673
|
+
resizeCanvasToDisplaySize(dom.canvas, state);
|
|
3674
|
+
renderScene(
|
|
3675
|
+
ctx,
|
|
3676
|
+
dom.canvas,
|
|
3677
|
+
state,
|
|
3678
|
+
shipModel,
|
|
3679
|
+
dom,
|
|
3680
|
+
featureAdapters.lighting,
|
|
3681
|
+
featureAdapters.fluid,
|
|
3682
|
+
featureAdapters.cloth
|
|
3683
|
+
);
|
|
3684
|
+
syncTextState(state, shipModel, featureAdapters);
|
|
3685
|
+
animationFrameId = requestAnimationFrame(renderFrame);
|
|
3686
|
+
};
|
|
3687
|
+
const handlePauseClick = () => {
|
|
3688
|
+
state.paused = !state.paused;
|
|
3689
|
+
dom.pauseButton.textContent = state.paused ? state.translate(gpuSharedTranslationKeys.resume) : state.translate(gpuSharedTranslationKeys.pause);
|
|
3690
|
+
};
|
|
3691
|
+
const handleStressChange = () => {
|
|
3692
|
+
state.stress = dom.stressToggle.checked;
|
|
3693
|
+
};
|
|
3694
|
+
const handleFocusChange = () => {
|
|
3695
|
+
state.focus = dom.focusMode.value;
|
|
3696
|
+
Object.assign(state.camera, {
|
|
3697
|
+
...CAMERA_PRESETS[state.focus],
|
|
3698
|
+
target: vec3(...CAMERA_PRESETS[state.focus].target)
|
|
3699
|
+
});
|
|
3700
|
+
};
|
|
3701
|
+
dom.pauseButton.addEventListener("click", handlePauseClick);
|
|
3702
|
+
dom.stressToggle.addEventListener("change", handleStressChange);
|
|
3703
|
+
dom.focusMode.addEventListener("change", handleFocusChange);
|
|
3704
|
+
animationFrameId = requestAnimationFrame(renderFrame);
|
|
3705
|
+
const destroy = () => {
|
|
3706
|
+
if (destroyed) {
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3709
|
+
destroyed = true;
|
|
3710
|
+
if (animationFrameId) {
|
|
3711
|
+
cancelAnimationFrame(animationFrameId);
|
|
3712
|
+
}
|
|
3713
|
+
dom.pauseButton.removeEventListener("click", handlePauseClick);
|
|
3714
|
+
dom.stressToggle.removeEventListener("change", handleStressChange);
|
|
3715
|
+
dom.focusMode.removeEventListener("change", handleFocusChange);
|
|
3716
|
+
try {
|
|
3717
|
+
if (typeof options.destroyState === "function") {
|
|
3718
|
+
options.destroyState(state.packageState);
|
|
3719
|
+
}
|
|
3720
|
+
} finally {
|
|
3721
|
+
state.packageState = void 0;
|
|
3722
|
+
}
|
|
3723
|
+
root.classList?.remove?.(ROOT_CLASS);
|
|
3724
|
+
root.classList?.remove?.(CAPTURE_CLASS);
|
|
3725
|
+
root.innerHTML = previousMarkup;
|
|
3726
|
+
if (typeof previousRenderGameToText === "function") {
|
|
3727
|
+
window.render_game_to_text = previousRenderGameToText;
|
|
3728
|
+
} else {
|
|
3729
|
+
delete window.render_game_to_text;
|
|
3730
|
+
}
|
|
3731
|
+
if (typeof previousAdvanceTime === "function") {
|
|
3732
|
+
window.advanceTime = previousAdvanceTime;
|
|
3733
|
+
} else {
|
|
3734
|
+
delete window.advanceTime;
|
|
3735
|
+
}
|
|
3736
|
+
};
|
|
3737
|
+
return {
|
|
3738
|
+
state,
|
|
3739
|
+
shipModel,
|
|
3740
|
+
canvas: dom.canvas,
|
|
3741
|
+
destroy
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
function updatePhysicsSnapshot(state, shipModel, physicsFeatures) {
|
|
3745
|
+
const createPhysicsWorldSnapshot = assertRequiredFunction(
|
|
3746
|
+
physicsFeatures,
|
|
3747
|
+
"physics",
|
|
3748
|
+
"createWorldSnapshot"
|
|
3749
|
+
);
|
|
3750
|
+
const rigidBodyShapes = Object.fromEntries(
|
|
3751
|
+
state.ships.map((ship) => [
|
|
3752
|
+
ship.id,
|
|
3753
|
+
resolveShipModel(state, ship, shipModel)?.physics?.shape ?? "box"
|
|
3754
|
+
])
|
|
3755
|
+
);
|
|
3756
|
+
state.physics.snapshot = createPhysicsWorldSnapshot({
|
|
3757
|
+
frameId: `showcase-${state.frame}`,
|
|
3758
|
+
tick: state.frame,
|
|
3759
|
+
simulationTimeMs: Number((state.time * 1e3).toFixed(2)),
|
|
3760
|
+
profile: state.physics.profile,
|
|
3761
|
+
authoritativeTransformRevision: state.frame,
|
|
3762
|
+
secondarySimulationRevision: state.frame,
|
|
3763
|
+
animationInputRevision: state.frame,
|
|
3764
|
+
bodyCount: state.ships.length + 2,
|
|
3765
|
+
dynamicBodyCount: state.ships.length,
|
|
3766
|
+
contactCount: state.contactCount,
|
|
3767
|
+
metadata: {
|
|
3768
|
+
collisionCount: state.collisionCount,
|
|
3769
|
+
contactCount: state.contactCount,
|
|
3770
|
+
snapshotStageId: state.physics.plan.snapshotStageId,
|
|
3771
|
+
rigidBodyShape: shipModel.physics.shape ?? "box",
|
|
3772
|
+
rigidBodyShapes
|
|
3773
|
+
}
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
export {
|
|
3777
|
+
advanceShowcaseClothSimulationState as __testOnlyAdvanceShowcaseClothSimulationState,
|
|
3778
|
+
buildWaterBands as __testOnlyBuildWaterBands,
|
|
3779
|
+
buildWaterMotionEffects as __testOnlyBuildWaterMotionEffects,
|
|
3780
|
+
collectSceneLightSources as __testOnlyCollectSceneLightSources,
|
|
3781
|
+
createShowcaseClothSimulationState as __testOnlyCreateShowcaseClothSimulationState,
|
|
3782
|
+
mountGpuShowcase,
|
|
3783
|
+
showcaseFocusModes
|
|
3784
|
+
};
|
|
3785
|
+
//# sourceMappingURL=showcase-runtime-SNCUFSSC.js.map
|