@plasius/gpu-shared 0.1.10 → 0.1.13
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 +51 -1
- package/README.md +59 -2
- 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-2FIFSBB4.js +74 -0
- package/dist/chunk-2FIFSBB4.js.map +1 -0
- package/dist/chunk-DABW627O.js +113 -0
- package/dist/chunk-DABW627O.js.map +1 -0
- package/dist/chunk-DQX4DXBR.js +369 -0
- package/dist/chunk-DQX4DXBR.js.map +1 -0
- package/dist/chunk-NCPJWLX3.js +17 -0
- package/dist/chunk-NCPJWLX3.js.map +1 -0
- package/dist/gltf-loader-WAM23F37.js +9 -0
- package/dist/index.cjs +1265 -281
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +19 -6
- package/dist/index.js.map +1 -1
- package/dist/showcase-inline-assets-B7U7VX5H.js +7 -0
- package/dist/{showcase-runtime-55OVDYWT.js → showcase-runtime-PN7N3FZY.js} +818 -239
- package/dist/showcase-runtime-PN7N3FZY.js.map +1 -0
- package/package.json +15 -1
- package/src/asset-url.js +62 -11
- package/src/feature-flags.js +1 -0
- package/src/gltf-loader.js +322 -32
- package/src/i18n.js +71 -0
- package/src/index.d.ts +115 -1
- package/src/index.js +9 -1
- package/src/showcase-inline-assets.js +3 -0
- package/src/showcase-runtime.js +924 -190
- 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-55OVDYWT.js.map +0 -1
- /package/dist/{chunk-DGUM43GV.js.map → gltf-loader-WAM23F37.js.map} +0 -0
- /package/dist/{gltf-loader-LKALCZAV.js.map → showcase-inline-assets-B7U7VX5H.js.map} +0 -0
package/src/showcase-runtime.js
CHANGED
|
@@ -32,12 +32,18 @@ import {
|
|
|
32
32
|
|
|
33
33
|
import { resolveShowcaseAssetUrl } from "./asset-url.js";
|
|
34
34
|
import { loadGltfModel } from "./gltf-loader.js";
|
|
35
|
+
import { GPU_SHOWCASE_REALISTIC_MODELS_FEATURE } from "./feature-flags.js";
|
|
36
|
+
import {
|
|
37
|
+
createGpuSharedTranslator,
|
|
38
|
+
gpuSharedTranslationKeys,
|
|
39
|
+
} from "./i18n.js";
|
|
35
40
|
|
|
36
41
|
const STYLE_ID = "plasius-shared-3d-showcase-style";
|
|
37
42
|
const ROOT_CLASS = "plasius-showcase-root";
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
43
|
+
const CAPTURE_CLASS = "plasius-showcase-root--capture";
|
|
44
|
+
const DEFAULT_CANVAS_WIDTH = 1280;
|
|
45
|
+
const DEFAULT_CANVAS_HEIGHT = 720;
|
|
46
|
+
const CAPTURE_CANVAS_PIXEL_BUDGET = 1920 * 1080;
|
|
41
47
|
const SHIP_SCALE = 1.1;
|
|
42
48
|
const HARBOR_BOUNDS = Object.freeze({
|
|
43
49
|
minX: -11.2,
|
|
@@ -56,33 +62,69 @@ const CAMERA_PRESETS = Object.freeze({
|
|
|
56
62
|
});
|
|
57
63
|
export const showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
|
|
58
64
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
const FOCUS_MODE_TRANSLATION_KEYS = Object.freeze({
|
|
66
|
+
integrated: gpuSharedTranslationKeys.focusIntegrated,
|
|
67
|
+
lighting: gpuSharedTranslationKeys.focusLighting,
|
|
68
|
+
cloth: gpuSharedTranslationKeys.focusCloth,
|
|
69
|
+
fluid: gpuSharedTranslationKeys.focusFluid,
|
|
70
|
+
physics: gpuSharedTranslationKeys.focusPhysics,
|
|
71
|
+
performance: gpuSharedTranslationKeys.focusPerformance,
|
|
72
|
+
debug: gpuSharedTranslationKeys.focusDebug,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const SCENE_NOTE_KEYS = Object.freeze([
|
|
76
|
+
gpuSharedTranslationKeys.noteAssetLoading,
|
|
77
|
+
gpuSharedTranslationKeys.noteMoonlight,
|
|
78
|
+
gpuSharedTranslationKeys.noteContinuity,
|
|
79
|
+
gpuSharedTranslationKeys.notePerformance,
|
|
64
80
|
]);
|
|
65
81
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-0.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
const PHYSICS_SCENE_NOTE_KEYS = Object.freeze([
|
|
83
|
+
gpuSharedTranslationKeys.notePhysicsSnapshots,
|
|
84
|
+
gpuSharedTranslationKeys.notePhysicsCollisions,
|
|
85
|
+
gpuSharedTranslationKeys.notePhysicsLighting,
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const LEGACY_HARBOR_LAYOUT = Object.freeze([
|
|
89
|
+
Object.freeze({
|
|
90
|
+
position: Object.freeze({ x: -8.2, y: 1.1, z: -0.9 }),
|
|
91
|
+
rotationY: -0.16,
|
|
92
|
+
scale: 5.4,
|
|
93
|
+
color: { r: 0.32, g: 0.27, b: 0.23, a: 1 },
|
|
94
|
+
accent: 0.06,
|
|
95
|
+
}),
|
|
96
|
+
Object.freeze({
|
|
97
|
+
position: Object.freeze({ x: -5.7, y: 0.45, z: 1.4 }),
|
|
98
|
+
rotationY: -0.08,
|
|
99
|
+
scale: { x: 6.8, y: 0.3, z: 2.1 },
|
|
100
|
+
color: { r: 0.31, g: 0.31, b: 0.34, a: 1 },
|
|
101
|
+
accent: 0.04,
|
|
102
|
+
}),
|
|
103
|
+
Object.freeze({
|
|
104
|
+
position: Object.freeze({ x: -10.4, y: 0.28, z: 0.8 }),
|
|
105
|
+
rotationY: 0.22,
|
|
106
|
+
scale: { x: 1.2, y: 0.9, z: 1.2 },
|
|
107
|
+
color: { r: 0.31, g: 0.35, b: 0.39, a: 1 },
|
|
108
|
+
accent: 0.02,
|
|
109
|
+
}),
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const SHOWCASE_ENVIRONMENT_LAYOUT = Object.freeze([
|
|
113
|
+
Object.freeze({
|
|
114
|
+
assetKey: "harbor-dock",
|
|
115
|
+
position: Object.freeze({ x: -4.6, y: 0.16, z: 0.7 }),
|
|
116
|
+
rotationY: -0.08,
|
|
117
|
+
scale: 0.84,
|
|
118
|
+
accent: 0.04,
|
|
119
|
+
}),
|
|
120
|
+
Object.freeze({
|
|
121
|
+
assetKey: "lighthouse",
|
|
122
|
+
position: Object.freeze({ x: -9.8, y: 0, z: -0.58 }),
|
|
123
|
+
rotationY: 0.12,
|
|
124
|
+
scale: 0.56,
|
|
125
|
+
accent: 0.08,
|
|
126
|
+
}),
|
|
127
|
+
]);
|
|
86
128
|
|
|
87
129
|
const SHIP_LANTERNS = Object.freeze([
|
|
88
130
|
Object.freeze({ x: 0.94, y: 1.54, z: 2.52, glow: 1 }),
|
|
@@ -90,6 +132,10 @@ const SHIP_LANTERNS = Object.freeze([
|
|
|
90
132
|
Object.freeze({ x: 0.62, y: 1.42, z: -2.18, glow: 0.88 }),
|
|
91
133
|
Object.freeze({ x: -0.58, y: 1.46, z: -2.04, glow: 0.84 }),
|
|
92
134
|
]);
|
|
135
|
+
const CUTTER_LANTERNS = Object.freeze([
|
|
136
|
+
Object.freeze({ x: 0.42, y: 1.04, z: 1.18, glow: 0.94 }),
|
|
137
|
+
Object.freeze({ x: -0.42, y: 1.04, z: 1.12, glow: 0.88 }),
|
|
138
|
+
]);
|
|
93
139
|
|
|
94
140
|
const HARBOR_TORCHES = Object.freeze([
|
|
95
141
|
Object.freeze({ x: -5.2, y: 1.25, z: 1.36, glow: 1.1 }),
|
|
@@ -128,6 +174,11 @@ function injectStyles() {
|
|
|
128
174
|
radial-gradient(circle at 82% 18%, rgba(240, 188, 103, 0.08), transparent 18%),
|
|
129
175
|
linear-gradient(180deg, #04101d 0%, #0b1930 42%, #081321 100%);
|
|
130
176
|
}
|
|
177
|
+
.${ROOT_CLASS}.${CAPTURE_CLASS} {
|
|
178
|
+
min-height: 100vh;
|
|
179
|
+
overflow: hidden;
|
|
180
|
+
background: #030710;
|
|
181
|
+
}
|
|
131
182
|
.${ROOT_CLASS},
|
|
132
183
|
.${ROOT_CLASS} * {
|
|
133
184
|
box-sizing: border-box;
|
|
@@ -207,6 +258,40 @@ function injectStyles() {
|
|
|
207
258
|
border: 1px solid rgba(159, 185, 223, 0.12);
|
|
208
259
|
background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
|
|
209
260
|
}
|
|
261
|
+
.${CAPTURE_CLASS} .plasius-demo {
|
|
262
|
+
width: 100vw;
|
|
263
|
+
height: 100vh;
|
|
264
|
+
padding: 0;
|
|
265
|
+
display: block;
|
|
266
|
+
}
|
|
267
|
+
.${CAPTURE_CLASS} .plasius-demo__hero,
|
|
268
|
+
.${CAPTURE_CLASS} .plasius-demo__toolbar,
|
|
269
|
+
.${CAPTURE_CLASS} .plasius-demo__legend,
|
|
270
|
+
.${CAPTURE_CLASS} .plasius-demo__sidebar,
|
|
271
|
+
.${CAPTURE_CLASS} .plasius-demo__footer {
|
|
272
|
+
display: none;
|
|
273
|
+
}
|
|
274
|
+
.${CAPTURE_CLASS} .plasius-demo__layout {
|
|
275
|
+
display: block;
|
|
276
|
+
height: 100%;
|
|
277
|
+
}
|
|
278
|
+
.${CAPTURE_CLASS} .plasius-demo__canvas-panel {
|
|
279
|
+
height: 100%;
|
|
280
|
+
padding: 0;
|
|
281
|
+
border: 0;
|
|
282
|
+
border-radius: 0;
|
|
283
|
+
background: transparent;
|
|
284
|
+
box-shadow: none;
|
|
285
|
+
backdrop-filter: none;
|
|
286
|
+
}
|
|
287
|
+
.${CAPTURE_CLASS} .plasius-demo__canvas {
|
|
288
|
+
width: 100%;
|
|
289
|
+
height: 100%;
|
|
290
|
+
aspect-ratio: auto;
|
|
291
|
+
border: 0;
|
|
292
|
+
border-radius: 0;
|
|
293
|
+
background: #030710;
|
|
294
|
+
}
|
|
210
295
|
.plasius-demo__toolbar {
|
|
211
296
|
position: absolute;
|
|
212
297
|
top: 26px;
|
|
@@ -381,6 +466,15 @@ function transformPoint(point, transform) {
|
|
|
381
466
|
return addVec3(rotated, transform.position);
|
|
382
467
|
}
|
|
383
468
|
|
|
469
|
+
function transformDirection(direction, transform) {
|
|
470
|
+
const scale =
|
|
471
|
+
typeof transform.scale === "number"
|
|
472
|
+
? { x: transform.scale, y: transform.scale, z: transform.scale }
|
|
473
|
+
: transform.scale;
|
|
474
|
+
const scaled = vec3(direction.x * scale.x, direction.y * scale.y, direction.z * scale.z);
|
|
475
|
+
return normalizeVec3(rotateY(scaled, transform.rotationY));
|
|
476
|
+
}
|
|
477
|
+
|
|
384
478
|
function projectPoint(point, camera, viewport) {
|
|
385
479
|
const relative = subVec3(point, camera.eye);
|
|
386
480
|
const viewX = dotVec3(relative, camera.right);
|
|
@@ -406,6 +500,92 @@ function colorToRgba(color, alpha = 1) {
|
|
|
406
500
|
return `rgba(${r}, ${g}, ${b}, ${clamp(alpha, 0, 1)})`;
|
|
407
501
|
}
|
|
408
502
|
|
|
503
|
+
function mixColor(a, b, t) {
|
|
504
|
+
return {
|
|
505
|
+
r: mix(a.r, b.r, t),
|
|
506
|
+
g: mix(a.g, b.g, t),
|
|
507
|
+
b: mix(a.b, b.b, t),
|
|
508
|
+
a: mix(a.a ?? 1, b.a ?? 1, t),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function multiplyColor(a, b) {
|
|
513
|
+
return {
|
|
514
|
+
r: a.r * b.r,
|
|
515
|
+
g: a.g * b.g,
|
|
516
|
+
b: a.b * b.b,
|
|
517
|
+
a: (a.a ?? 1) * (b.a ?? 1),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function createLegacyMeshPrimitive(mesh) {
|
|
522
|
+
return Object.freeze({
|
|
523
|
+
name: mesh.name ?? "legacy-mesh",
|
|
524
|
+
positions: mesh.positions,
|
|
525
|
+
indices: mesh.indices,
|
|
526
|
+
normals: null,
|
|
527
|
+
colors: null,
|
|
528
|
+
material: Object.freeze({
|
|
529
|
+
name: "legacy-material",
|
|
530
|
+
color: mesh.color ?? { r: 0.56, g: 0.33, b: 0.22, a: 1 },
|
|
531
|
+
roughness: 0.88,
|
|
532
|
+
metallic: 0.08,
|
|
533
|
+
emissive: Object.freeze({ r: 0, g: 0, b: 0 }),
|
|
534
|
+
}),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function isFeatureEnabled(featureFlags, featureName, fallback = true) {
|
|
539
|
+
const directValue =
|
|
540
|
+
typeof featureFlags?.[featureName] === "boolean"
|
|
541
|
+
? featureFlags[featureName]
|
|
542
|
+
: featureFlags?.flags?.[featureName];
|
|
543
|
+
if (typeof directValue === "boolean") {
|
|
544
|
+
return directValue;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const enabledValue =
|
|
548
|
+
typeof featureFlags?.enabled?.[featureName] === "boolean"
|
|
549
|
+
? featureFlags.enabled[featureName]
|
|
550
|
+
: undefined;
|
|
551
|
+
if (typeof enabledValue === "boolean") {
|
|
552
|
+
return enabledValue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return fallback;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function getMeshPrimitives(mesh) {
|
|
559
|
+
return Array.isArray(mesh?.primitives) && mesh.primitives.length > 0
|
|
560
|
+
? mesh.primitives
|
|
561
|
+
: [createLegacyMeshPrimitive(mesh)];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function tintPrimitiveColor(material, colorOverride) {
|
|
565
|
+
if (!colorOverride) {
|
|
566
|
+
return material.color;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const name = String(material.name ?? "").toLowerCase();
|
|
570
|
+
if (name.includes("sail") || name.includes("glass") || name.includes("roof")) {
|
|
571
|
+
return material.color;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const tintAmount = name.includes("hull")
|
|
575
|
+
? 0.54
|
|
576
|
+
: name.includes("trim")
|
|
577
|
+
? 0.22
|
|
578
|
+
: name.includes("deck")
|
|
579
|
+
? 0.12
|
|
580
|
+
: 0;
|
|
581
|
+
|
|
582
|
+
if (tintAmount <= 0) {
|
|
583
|
+
return material.color;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return mixColor(material.color, multiplyColor(material.color, colorOverride), tintAmount);
|
|
587
|
+
}
|
|
588
|
+
|
|
409
589
|
function projectShadowPoint(point, lightDir, planeY) {
|
|
410
590
|
const shadowDir = scaleVec3(lightDir, -1);
|
|
411
591
|
if (Math.abs(shadowDir.y) < 0.0001) {
|
|
@@ -430,6 +610,64 @@ function shadeColor(base, normal, lightDir, heightBias = 0, accent = 0) {
|
|
|
430
610
|
};
|
|
431
611
|
}
|
|
432
612
|
|
|
613
|
+
function getMaterialSeed(materialName) {
|
|
614
|
+
let seed = 0;
|
|
615
|
+
for (let index = 0; index < materialName.length; index += 1) {
|
|
616
|
+
seed += materialName.charCodeAt(index) * (index + 1);
|
|
617
|
+
}
|
|
618
|
+
return seed;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function getMaterialDetailStrength(material, surfaceType) {
|
|
622
|
+
const name = String(material?.name ?? "").toLowerCase();
|
|
623
|
+
if (surfaceType === "water" || name.includes("glass")) {
|
|
624
|
+
return 0.018;
|
|
625
|
+
}
|
|
626
|
+
if (name.includes("wood") || name.includes("timber") || name.includes("plank")) {
|
|
627
|
+
return 0.13;
|
|
628
|
+
}
|
|
629
|
+
if (name.includes("stone") || name.includes("concrete") || name.includes("plaster")) {
|
|
630
|
+
return 0.1;
|
|
631
|
+
}
|
|
632
|
+
if (name.includes("roof") || name.includes("crate")) {
|
|
633
|
+
return 0.09;
|
|
634
|
+
}
|
|
635
|
+
if (name.includes("paint")) {
|
|
636
|
+
return 0.045;
|
|
637
|
+
}
|
|
638
|
+
if (name.includes("metal")) {
|
|
639
|
+
return 0.035;
|
|
640
|
+
}
|
|
641
|
+
return 0.04;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function applyMaterialDetail(color, material, worldCenter, normal, surfaceType) {
|
|
645
|
+
const materialName = String(material?.name ?? surfaceType ?? "material");
|
|
646
|
+
const detailStrength = getMaterialDetailStrength(material, surfaceType);
|
|
647
|
+
const sample =
|
|
648
|
+
worldCenter.x * 3.17 +
|
|
649
|
+
worldCenter.y * 5.29 +
|
|
650
|
+
worldCenter.z * 7.83 +
|
|
651
|
+
getMaterialSeed(materialName) * 0.013;
|
|
652
|
+
const grain = (pseudoRandom(sample) - 0.5) * detailStrength;
|
|
653
|
+
const lowerSurface = smoothstep(7.5, -0.8, worldCenter.y);
|
|
654
|
+
const verticalSurface = 1 - clamp(Math.abs(normal.y), 0, 1);
|
|
655
|
+
const materialLowerWear =
|
|
656
|
+
/stone|concrete|plaster|paint|wood|timber|plank|crate/.test(materialName.toLowerCase())
|
|
657
|
+
? lowerSurface * verticalSurface * 0.055
|
|
658
|
+
: 0;
|
|
659
|
+
const wetlineWear =
|
|
660
|
+
surfaceType === "ship" && worldCenter.y < 0.72
|
|
661
|
+
? smoothstep(0.72, -0.1, worldCenter.y) * 0.05
|
|
662
|
+
: 0;
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
r: clamp(color.r * (1 + grain) - materialLowerWear - wetlineWear, 0, 1),
|
|
666
|
+
g: clamp(color.g * (1 + grain * 0.82) - materialLowerWear * 0.9 - wetlineWear, 0, 1),
|
|
667
|
+
b: clamp(color.b * (1 + grain * 0.62) - materialLowerWear * 0.68 - wetlineWear * 0.75, 0, 1),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
433
671
|
function buildCamera(state, canvas) {
|
|
434
672
|
const preset = CAMERA_PRESETS[state.focus] ?? CAMERA_PRESETS.integrated;
|
|
435
673
|
const yaw = state.camera.yaw ?? preset.yaw;
|
|
@@ -455,49 +693,153 @@ function buildCamera(state, canvas) {
|
|
|
455
693
|
};
|
|
456
694
|
}
|
|
457
695
|
|
|
458
|
-
function buildTrianglesFromMesh(
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
696
|
+
function buildTrianglesFromMesh(
|
|
697
|
+
mesh,
|
|
698
|
+
transform,
|
|
699
|
+
colorOverride,
|
|
700
|
+
camera,
|
|
701
|
+
viewport,
|
|
702
|
+
triangles,
|
|
703
|
+
options = {}
|
|
704
|
+
) {
|
|
705
|
+
const primitives = getMeshPrimitives(mesh);
|
|
706
|
+
for (const primitive of primitives) {
|
|
707
|
+
const resolvedColor = tintPrimitiveColor(primitive.material, colorOverride);
|
|
708
|
+
for (let index = 0; index < primitive.indices.length; index += 3) {
|
|
709
|
+
const aIndex = primitive.indices[index] * 3;
|
|
710
|
+
const bIndex = primitive.indices[index + 1] * 3;
|
|
711
|
+
const cIndex = primitive.indices[index + 2] * 3;
|
|
712
|
+
|
|
713
|
+
const a = transformPoint(
|
|
714
|
+
vec3(
|
|
715
|
+
primitive.positions[aIndex],
|
|
716
|
+
primitive.positions[aIndex + 1],
|
|
717
|
+
primitive.positions[aIndex + 2]
|
|
718
|
+
),
|
|
719
|
+
transform
|
|
720
|
+
);
|
|
721
|
+
const b = transformPoint(
|
|
722
|
+
vec3(
|
|
723
|
+
primitive.positions[bIndex],
|
|
724
|
+
primitive.positions[bIndex + 1],
|
|
725
|
+
primitive.positions[bIndex + 2]
|
|
726
|
+
),
|
|
727
|
+
transform
|
|
728
|
+
);
|
|
729
|
+
const c = transformPoint(
|
|
730
|
+
vec3(
|
|
731
|
+
primitive.positions[cIndex],
|
|
732
|
+
primitive.positions[cIndex + 1],
|
|
733
|
+
primitive.positions[cIndex + 2]
|
|
734
|
+
),
|
|
735
|
+
transform
|
|
736
|
+
);
|
|
463
737
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
738
|
+
const ab = subVec3(b, a);
|
|
739
|
+
const ac = subVec3(c, a);
|
|
740
|
+
const faceNormal = normalizeVec3(crossVec3(ab, ac));
|
|
741
|
+
let normal = faceNormal;
|
|
742
|
+
if (Array.isArray(primitive.normals)) {
|
|
743
|
+
const aNormal = transformDirection(
|
|
744
|
+
vec3(
|
|
745
|
+
primitive.normals[aIndex],
|
|
746
|
+
primitive.normals[aIndex + 1],
|
|
747
|
+
primitive.normals[aIndex + 2]
|
|
748
|
+
),
|
|
749
|
+
transform
|
|
750
|
+
);
|
|
751
|
+
const bNormal = transformDirection(
|
|
752
|
+
vec3(
|
|
753
|
+
primitive.normals[bIndex],
|
|
754
|
+
primitive.normals[bIndex + 1],
|
|
755
|
+
primitive.normals[bIndex + 2]
|
|
756
|
+
),
|
|
757
|
+
transform
|
|
758
|
+
);
|
|
759
|
+
const cNormal = transformDirection(
|
|
760
|
+
vec3(
|
|
761
|
+
primitive.normals[cIndex],
|
|
762
|
+
primitive.normals[cIndex + 1],
|
|
763
|
+
primitive.normals[cIndex + 2]
|
|
764
|
+
),
|
|
765
|
+
transform
|
|
766
|
+
);
|
|
767
|
+
normal = normalizeVec3(
|
|
768
|
+
scaleVec3(addVec3(addVec3(aNormal, bNormal), cNormal), 1 / 3)
|
|
769
|
+
);
|
|
770
|
+
}
|
|
476
771
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (dotVec3(normal, viewDir) <= 0) {
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
772
|
+
const viewDir = normalizeVec3(subVec3(camera.eye, a));
|
|
773
|
+
if (dotVec3(faceNormal, viewDir) <= 0) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
484
776
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
777
|
+
const projected = [
|
|
778
|
+
projectPoint(a, camera, viewport),
|
|
779
|
+
projectPoint(b, camera, viewport),
|
|
780
|
+
projectPoint(c, camera, viewport),
|
|
781
|
+
];
|
|
782
|
+
if (projected.some((value) => value === null)) {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
489
785
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
786
|
+
triangles.push({
|
|
787
|
+
points: projected,
|
|
788
|
+
depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
|
|
789
|
+
worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
|
|
790
|
+
normal,
|
|
791
|
+
baseColor: resolvedColor,
|
|
792
|
+
accent: options.accent ?? 0,
|
|
793
|
+
material: primitive.material,
|
|
794
|
+
reflection: options.reflection ?? 0,
|
|
795
|
+
surfaceType: options.surfaceType ?? "solid",
|
|
796
|
+
});
|
|
797
|
+
}
|
|
498
798
|
}
|
|
499
799
|
}
|
|
500
800
|
|
|
801
|
+
async function loadShowcaseAssetCatalog() {
|
|
802
|
+
const [brigantine, cutter, lighthouse, harborDock] = await Promise.all([
|
|
803
|
+
loadGltfModel(resolveShowcaseAssetUrl("brigantine")),
|
|
804
|
+
loadGltfModel(resolveShowcaseAssetUrl("cutter")),
|
|
805
|
+
loadGltfModel(resolveShowcaseAssetUrl("lighthouse")),
|
|
806
|
+
loadGltfModel(resolveShowcaseAssetUrl("harbor-dock")),
|
|
807
|
+
]);
|
|
808
|
+
|
|
809
|
+
return Object.freeze({
|
|
810
|
+
primaryShipKey: "brigantine",
|
|
811
|
+
ships: Object.freeze({
|
|
812
|
+
brigantine,
|
|
813
|
+
cutter,
|
|
814
|
+
}),
|
|
815
|
+
environment: Object.freeze({
|
|
816
|
+
lighthouse,
|
|
817
|
+
"harbor-dock": harborDock,
|
|
818
|
+
}),
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function createLegacyShowcaseAssetCatalog() {
|
|
823
|
+
const brigantine = loadGltfModel(resolveShowcaseAssetUrl("brigantine"));
|
|
824
|
+
return Promise.resolve(brigantine).then((primary) =>
|
|
825
|
+
Object.freeze({
|
|
826
|
+
primaryShipKey: "brigantine",
|
|
827
|
+
ships: Object.freeze({
|
|
828
|
+
brigantine: primary,
|
|
829
|
+
}),
|
|
830
|
+
environment: Object.freeze({}),
|
|
831
|
+
})
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function resolveShipModel(state, ship, fallbackModel = null) {
|
|
836
|
+
return (
|
|
837
|
+
state.assetCatalog?.ships?.[ship.modelKey ?? state.assetCatalog?.primaryShipKey ?? "brigantine"] ??
|
|
838
|
+
fallbackModel ??
|
|
839
|
+
state.shipModel
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
501
843
|
function createPerformanceGovernor() {
|
|
502
844
|
const fluidDetail = createQualityLadderAdapter({
|
|
503
845
|
id: "fluid-detail",
|
|
@@ -554,6 +896,7 @@ function createPerformanceGovernor() {
|
|
|
554
896
|
}
|
|
555
897
|
|
|
556
898
|
function buildDemoDom(root, options) {
|
|
899
|
+
const t = options.translate;
|
|
557
900
|
root.innerHTML = `
|
|
558
901
|
<main class="plasius-demo">
|
|
559
902
|
<section class="plasius-demo__hero">
|
|
@@ -563,56 +906,55 @@ function buildDemoDom(root, options) {
|
|
|
563
906
|
<p class="plasius-demo__lead">${options.subtitle}</p>
|
|
564
907
|
</section>
|
|
565
908
|
<section class="plasius-panel plasius-demo__status">
|
|
566
|
-
<p id="demoStatus" class="plasius-demo__status-badge"
|
|
909
|
+
<p id="demoStatus" class="plasius-demo__status-badge">${t(gpuSharedTranslationKeys.statusBooting)}</p>
|
|
567
910
|
<p id="demoDetails" class="plasius-demo__status-text">
|
|
568
|
-
|
|
911
|
+
${t(gpuSharedTranslationKeys.detailsBooting)}
|
|
569
912
|
</p>
|
|
570
913
|
</section>
|
|
571
914
|
</section>
|
|
572
915
|
<section class="plasius-demo__layout">
|
|
573
916
|
<section class="plasius-panel plasius-demo__canvas-panel">
|
|
574
|
-
<canvas id="demoCanvas" class="plasius-demo__canvas" width="
|
|
917
|
+
<canvas id="demoCanvas" class="plasius-demo__canvas" width="${DEFAULT_CANVAS_WIDTH}" height="${DEFAULT_CANVAS_HEIGHT}"></canvas>
|
|
575
918
|
<div class="plasius-demo__toolbar">
|
|
576
|
-
<button id="pauseButton" type="button"
|
|
919
|
+
<button id="pauseButton" type="button">${t(gpuSharedTranslationKeys.pause)}</button>
|
|
577
920
|
<label class="plasius-toggle">
|
|
578
921
|
<input id="stressToggle" type="checkbox" />
|
|
579
|
-
|
|
922
|
+
${t(gpuSharedTranslationKeys.stressMode)}
|
|
580
923
|
</label>
|
|
581
924
|
<label class="plasius-toggle">
|
|
582
|
-
|
|
925
|
+
${t(gpuSharedTranslationKeys.focus)}
|
|
583
926
|
<select id="focusMode">
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
<option value="debug">debug</option>
|
|
927
|
+
${showcaseFocusModes
|
|
928
|
+
.map(
|
|
929
|
+
(mode) =>
|
|
930
|
+
`<option value="${mode}">${t(FOCUS_MODE_TRANSLATION_KEYS[mode])}</option>`
|
|
931
|
+
)
|
|
932
|
+
.join("")}
|
|
591
933
|
</select>
|
|
592
934
|
</label>
|
|
593
935
|
</div>
|
|
594
936
|
<div class="plasius-demo__legend">
|
|
595
|
-
<strong
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
937
|
+
<strong>${t(gpuSharedTranslationKeys.legendTitle)}</strong>
|
|
938
|
+
${t(gpuSharedTranslationKeys.legendShipMetadata)}<br />
|
|
939
|
+
${t(gpuSharedTranslationKeys.legendLighting)}<br />
|
|
940
|
+
${t(gpuSharedTranslationKeys.legendCollisions)}
|
|
599
941
|
</div>
|
|
600
942
|
</section>
|
|
601
943
|
<aside class="plasius-demo__sidebar">
|
|
602
944
|
<section class="plasius-panel plasius-demo__card">
|
|
603
|
-
<h2
|
|
945
|
+
<h2>${t(gpuSharedTranslationKeys.sceneState)}</h2>
|
|
604
946
|
<ul id="sceneMetrics" class="plasius-demo__metrics"></ul>
|
|
605
947
|
</section>
|
|
606
948
|
<section class="plasius-panel plasius-demo__card">
|
|
607
|
-
<h2
|
|
949
|
+
<h2>${t(gpuSharedTranslationKeys.qualityBudgets)}</h2>
|
|
608
950
|
<ul id="qualityMetrics" class="plasius-demo__metrics"></ul>
|
|
609
951
|
</section>
|
|
610
952
|
<section class="plasius-panel plasius-demo__card">
|
|
611
|
-
<h2
|
|
953
|
+
<h2>${t(gpuSharedTranslationKeys.debugTelemetry)}</h2>
|
|
612
954
|
<ul id="debugMetrics" class="plasius-demo__metrics"></ul>
|
|
613
955
|
</section>
|
|
614
956
|
<section class="plasius-panel plasius-demo__card">
|
|
615
|
-
<h2
|
|
957
|
+
<h2>${t(gpuSharedTranslationKeys.notes)}</h2>
|
|
616
958
|
<ul id="sceneNotes" class="plasius-demo__metrics"></ul>
|
|
617
959
|
</section>
|
|
618
960
|
</aside>
|
|
@@ -638,6 +980,12 @@ function buildDemoDom(root, options) {
|
|
|
638
980
|
}
|
|
639
981
|
|
|
640
982
|
function buildSceneSnapshot(state, shipModel) {
|
|
983
|
+
const shipPhysics = Object.freeze(
|
|
984
|
+
Object.fromEntries(
|
|
985
|
+
state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
|
|
986
|
+
)
|
|
987
|
+
);
|
|
988
|
+
|
|
641
989
|
return Object.freeze({
|
|
642
990
|
focus: state.focus,
|
|
643
991
|
frame: state.frame,
|
|
@@ -659,6 +1007,7 @@ function buildSceneSnapshot(state, shipModel) {
|
|
|
659
1007
|
state.ships.map((ship) =>
|
|
660
1008
|
Object.freeze({
|
|
661
1009
|
id: ship.id,
|
|
1010
|
+
modelKey: ship.modelKey ?? "brigantine",
|
|
662
1011
|
position: Object.freeze({ ...ship.position }),
|
|
663
1012
|
velocity: Object.freeze({ ...ship.velocity }),
|
|
664
1013
|
rotationY: ship.rotationY,
|
|
@@ -679,12 +1028,13 @@ function buildSceneSnapshot(state, shipModel) {
|
|
|
679
1028
|
)
|
|
680
1029
|
),
|
|
681
1030
|
shipPhysics: shipModel?.physics ?? null,
|
|
1031
|
+
shipModels: shipPhysics,
|
|
682
1032
|
physics: Object.freeze({
|
|
683
1033
|
profile: state.physics.profile,
|
|
684
1034
|
plan: state.physics.plan,
|
|
685
1035
|
manifest: state.physics.manifest,
|
|
686
1036
|
snapshot: state.physics.snapshot,
|
|
687
|
-
shipPhysics
|
|
1037
|
+
shipPhysics,
|
|
688
1038
|
}),
|
|
689
1039
|
});
|
|
690
1040
|
}
|
|
@@ -727,6 +1077,83 @@ function readVisualNumber(value, fallback) {
|
|
|
727
1077
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
728
1078
|
}
|
|
729
1079
|
|
|
1080
|
+
function readPositiveNumber(value, fallback) {
|
|
1081
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
1082
|
+
? value
|
|
1083
|
+
: fallback;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function isTruthyCaptureValue(value) {
|
|
1087
|
+
return value === "1" || value === "true" || value === "scene" || value === "video";
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function resolveCaptureSettings(options) {
|
|
1091
|
+
const explicitCaptureMode =
|
|
1092
|
+
typeof options.captureMode === "boolean" ? options.captureMode : undefined;
|
|
1093
|
+
let captureMode = explicitCaptureMode ?? false;
|
|
1094
|
+
let renderScale = readPositiveNumber(options.renderScale, undefined);
|
|
1095
|
+
|
|
1096
|
+
try {
|
|
1097
|
+
const params = new URLSearchParams(window.location.search);
|
|
1098
|
+
if (explicitCaptureMode === undefined) {
|
|
1099
|
+
captureMode =
|
|
1100
|
+
isTruthyCaptureValue(params.get("capture")) ||
|
|
1101
|
+
params.get("presentation") === "capture";
|
|
1102
|
+
}
|
|
1103
|
+
renderScale = readPositiveNumber(Number(params.get("renderScale")), renderScale);
|
|
1104
|
+
} catch {
|
|
1105
|
+
// Query-string capture controls are optional and only available in browsers.
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return {
|
|
1109
|
+
captureMode,
|
|
1110
|
+
renderScale,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function getCanvasDisplaySize(canvas) {
|
|
1115
|
+
const rect =
|
|
1116
|
+
typeof canvas.getBoundingClientRect === "function"
|
|
1117
|
+
? canvas.getBoundingClientRect()
|
|
1118
|
+
: null;
|
|
1119
|
+
const width = Math.round(
|
|
1120
|
+
readPositiveNumber(rect?.width, readPositiveNumber(canvas.clientWidth, canvas.width))
|
|
1121
|
+
);
|
|
1122
|
+
const height = Math.round(
|
|
1123
|
+
readPositiveNumber(rect?.height, readPositiveNumber(canvas.clientHeight, canvas.height))
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
return {
|
|
1127
|
+
width: Math.max(1, width || DEFAULT_CANVAS_WIDTH),
|
|
1128
|
+
height: Math.max(1, height || DEFAULT_CANVAS_HEIGHT),
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function resizeCanvasToDisplaySize(canvas, state) {
|
|
1133
|
+
const { width, height } = getCanvasDisplaySize(canvas);
|
|
1134
|
+
const deviceScale = readPositiveNumber(globalThis.devicePixelRatio, 1);
|
|
1135
|
+
const requestedScale = readPositiveNumber(state.renderScale, deviceScale);
|
|
1136
|
+
const maxScale = state.captureMode ? 2 : 1.5;
|
|
1137
|
+
let scale = clamp(requestedScale, 1, maxScale);
|
|
1138
|
+
const pixelBudget = state.captureMode
|
|
1139
|
+
? CAPTURE_CANVAS_PIXEL_BUDGET
|
|
1140
|
+
: DEFAULT_CANVAS_WIDTH * DEFAULT_CANVAS_HEIGHT * 1.5;
|
|
1141
|
+
const projectedPixels = width * height * scale * scale;
|
|
1142
|
+
|
|
1143
|
+
if (projectedPixels > pixelBudget) {
|
|
1144
|
+
scale = Math.sqrt(pixelBudget / Math.max(1, width * height));
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const targetWidth = Math.max(1, Math.round(width * scale));
|
|
1148
|
+
const targetHeight = Math.max(1, Math.round(height * scale));
|
|
1149
|
+
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
|
1150
|
+
canvas.width = targetWidth;
|
|
1151
|
+
canvas.height = targetHeight;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
state.renderScale = scale;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
730
1157
|
function resolveClothPresentation(state, meshDetail) {
|
|
731
1158
|
const clothPlan = createClothRepresentationPlan({
|
|
732
1159
|
garmentId: "shore-flag",
|
|
@@ -1215,6 +1642,14 @@ function sampleWave(state, x, z, time) {
|
|
|
1215
1642
|
);
|
|
1216
1643
|
}
|
|
1217
1644
|
|
|
1645
|
+
function resolveFluidBandContinuity(continuity, band) {
|
|
1646
|
+
if (continuity?.bands && continuity.bands[band]) {
|
|
1647
|
+
return continuity.bands[band];
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
return continuity ?? { amplitudeFloor: 1, frequencyFloor: 1 };
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1218
1653
|
function buildWaterMotionEffects(state) {
|
|
1219
1654
|
const wakeTrails = [];
|
|
1220
1655
|
const rippleRings = state.waveImpulses.map((impulse) => {
|
|
@@ -1297,6 +1732,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
|
|
|
1297
1732
|
fluidPlan.representations.find((entry) => entry.band === bandSpec.band) ??
|
|
1298
1733
|
fluidPlan.representations[0];
|
|
1299
1734
|
const continuity = createFluidContinuityEnvelope({ fluidBodyId: "harbor" });
|
|
1735
|
+
const bandContinuity = resolveFluidBandContinuity(continuity, bandSpec.band);
|
|
1300
1736
|
const bandResolution =
|
|
1301
1737
|
bandSpec.band === "near"
|
|
1302
1738
|
? fluidDetail.nearResolution
|
|
@@ -1320,7 +1756,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
|
|
|
1320
1756
|
const y =
|
|
1321
1757
|
bandSpec.y +
|
|
1322
1758
|
sampleWave(state, x, z, state.time) *
|
|
1323
|
-
|
|
1759
|
+
bandContinuity.amplitudeFloor *
|
|
1324
1760
|
(bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
|
|
1325
1761
|
positions.push(vec3(x, y, z));
|
|
1326
1762
|
}
|
|
@@ -1338,7 +1774,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
|
|
|
1338
1774
|
bandMeshes.push({
|
|
1339
1775
|
band: bandSpec.band,
|
|
1340
1776
|
representation,
|
|
1341
|
-
continuity,
|
|
1777
|
+
continuity: bandContinuity,
|
|
1342
1778
|
rows,
|
|
1343
1779
|
cols,
|
|
1344
1780
|
positions,
|
|
@@ -1366,6 +1802,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
|
|
|
1366
1802
|
}
|
|
1367
1803
|
|
|
1368
1804
|
function createSceneState(options) {
|
|
1805
|
+
const translate = options.translate;
|
|
1369
1806
|
const { governor, fluidDetail, clothDetail, lightingDetail } = createPerformanceGovernor();
|
|
1370
1807
|
const physicsProfile = defaultPhysicsWorkerProfile;
|
|
1371
1808
|
const physicsPlan = createPhysicsSimulationPlan(physicsProfile);
|
|
@@ -1373,7 +1810,7 @@ function createSceneState(options) {
|
|
|
1373
1810
|
const debugSession = createGpuDebugSession({
|
|
1374
1811
|
enabled: true,
|
|
1375
1812
|
adapter: {
|
|
1376
|
-
label:
|
|
1813
|
+
label: translate(gpuSharedTranslationKeys.debugAdapterShowcase),
|
|
1377
1814
|
memoryCapacityHintBytes: 6 * 1024 * 1024 * 1024,
|
|
1378
1815
|
coreCountHint: 12,
|
|
1379
1816
|
},
|
|
@@ -1383,23 +1820,27 @@ function createSceneState(options) {
|
|
|
1383
1820
|
owner: "renderer",
|
|
1384
1821
|
category: "texture",
|
|
1385
1822
|
sizeBytes: 1280 * 720 * 4,
|
|
1386
|
-
label:
|
|
1823
|
+
label: translate(gpuSharedTranslationKeys.debugMainColorBuffer),
|
|
1387
1824
|
});
|
|
1388
1825
|
debugSession.trackAllocation({
|
|
1389
1826
|
id: "showcase.shadow-impression",
|
|
1390
1827
|
owner: "lighting",
|
|
1391
1828
|
category: "texture",
|
|
1392
1829
|
sizeBytes: 12 * 1024 * 1024,
|
|
1393
|
-
label:
|
|
1830
|
+
label: translate(gpuSharedTranslationKeys.debugShadowImpressionAtlas),
|
|
1394
1831
|
});
|
|
1395
1832
|
|
|
1396
1833
|
return {
|
|
1834
|
+
translate,
|
|
1397
1835
|
focus: options.focus,
|
|
1398
1836
|
governor,
|
|
1399
1837
|
fluidDetail,
|
|
1400
1838
|
clothDetail,
|
|
1401
1839
|
lightingDetail,
|
|
1402
1840
|
debugSession,
|
|
1841
|
+
showcaseRealisticModelsEnabled: options.realisticModelsEnabled !== false,
|
|
1842
|
+
captureMode: options.captureMode === true,
|
|
1843
|
+
renderScale: readPositiveNumber(options.renderScale, undefined),
|
|
1403
1844
|
packageState: undefined,
|
|
1404
1845
|
demoDescription: null,
|
|
1405
1846
|
demoVisuals: null,
|
|
@@ -1414,6 +1855,7 @@ function createSceneState(options) {
|
|
|
1414
1855
|
ships: [
|
|
1415
1856
|
{
|
|
1416
1857
|
id: "northwind",
|
|
1858
|
+
modelKey: "brigantine",
|
|
1417
1859
|
position: vec3(-5.2, 0, 7.2),
|
|
1418
1860
|
velocity: vec3(2.35, 0, -1.08),
|
|
1419
1861
|
rotationY: 0.58,
|
|
@@ -1424,17 +1866,18 @@ function createSceneState(options) {
|
|
|
1424
1866
|
throttleResponse: 0.46,
|
|
1425
1867
|
rudderResponse: 0.54,
|
|
1426
1868
|
wanderPhase: 0.35,
|
|
1427
|
-
lanterns:
|
|
1869
|
+
lanterns: CUTTER_LANTERNS,
|
|
1428
1870
|
lanternStrength: 1.06,
|
|
1429
1871
|
collisionRadiusScale: 1.04,
|
|
1430
1872
|
},
|
|
1431
1873
|
{
|
|
1432
1874
|
id: "tidecaller",
|
|
1875
|
+
modelKey: "cutter",
|
|
1433
1876
|
position: vec3(4.8, 0, 4.4),
|
|
1434
1877
|
velocity: vec3(-2.15, 0, 1.74),
|
|
1435
1878
|
rotationY: -2.48,
|
|
1436
1879
|
angularVelocity: -0.2,
|
|
1437
|
-
tint: { r: 0.
|
|
1880
|
+
tint: { r: 0.58, g: 0.24, b: 0.16 },
|
|
1438
1881
|
massScale: 0.84,
|
|
1439
1882
|
cruiseSpeed: 2.68,
|
|
1440
1883
|
throttleResponse: 0.7,
|
|
@@ -1458,6 +1901,7 @@ function createSceneState(options) {
|
|
|
1458
1901
|
manifest: physicsManifest,
|
|
1459
1902
|
snapshot: null,
|
|
1460
1903
|
},
|
|
1904
|
+
assetCatalog: null,
|
|
1461
1905
|
shipModel: null,
|
|
1462
1906
|
};
|
|
1463
1907
|
}
|
|
@@ -1544,10 +1988,51 @@ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, s
|
|
|
1544
1988
|
}
|
|
1545
1989
|
}
|
|
1546
1990
|
|
|
1547
|
-
function
|
|
1991
|
+
function resolveLocalLightContribution(triangle, lightSources) {
|
|
1992
|
+
const contribution = { r: 0, g: 0, b: 0 };
|
|
1993
|
+
if (!Array.isArray(lightSources) || triangle.surfaceType === "water") {
|
|
1994
|
+
return contribution;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
const normal = normalizeVec3(triangle.normal);
|
|
1998
|
+
for (const source of lightSources.slice(0, 8)) {
|
|
1999
|
+
const delta = subVec3(source.point, triangle.worldCenter);
|
|
2000
|
+
const distance = lengthVec3(delta);
|
|
2001
|
+
const attenuation =
|
|
2002
|
+
(source.glowScale ?? 1) / Math.max(1, 0.68 + distance * distance * 0.2);
|
|
2003
|
+
if (attenuation < 0.012) {
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
const lightDir = normalizeVec3(delta);
|
|
2008
|
+
const facing = clamp(dotVec3(normal, lightDir), 0, 1);
|
|
2009
|
+
const response = attenuation * (0.18 + facing * 0.82);
|
|
2010
|
+
const glowColor = source.glowColor ?? source.coreColor ?? { r: 1, g: 0.72, b: 0.4 };
|
|
2011
|
+
contribution.r += glowColor.r * response * 0.32;
|
|
2012
|
+
contribution.g += glowColor.g * response * 0.26;
|
|
2013
|
+
contribution.b += glowColor.b * response * 0.18;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
return contribution;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
function drawTriangles(
|
|
2020
|
+
ctx,
|
|
2021
|
+
triangles,
|
|
2022
|
+
lightDir,
|
|
2023
|
+
reflectionStrength,
|
|
2024
|
+
camera,
|
|
2025
|
+
shadowStrength,
|
|
2026
|
+
localLights = []
|
|
2027
|
+
) {
|
|
1548
2028
|
triangles.sort((left, right) => right.depth - left.depth);
|
|
1549
2029
|
for (const triangle of triangles) {
|
|
1550
2030
|
const surfaceNormal = normalizeVec3(triangle.normal);
|
|
2031
|
+
const material = triangle.material ?? {
|
|
2032
|
+
roughness: 0.88,
|
|
2033
|
+
metallic: 0.08,
|
|
2034
|
+
emissive: { r: 0, g: 0, b: 0 },
|
|
2035
|
+
};
|
|
1551
2036
|
const shaded = shadeColor(
|
|
1552
2037
|
triangle.baseColor,
|
|
1553
2038
|
surfaceNormal,
|
|
@@ -1555,19 +2040,42 @@ function drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, sha
|
|
|
1555
2040
|
clamp((triangle.worldCenter.y + 3) / 10, 0, 1),
|
|
1556
2041
|
triangle.accent
|
|
1557
2042
|
);
|
|
1558
|
-
const reflection =
|
|
2043
|
+
const reflection = reflectionStrength * (triangle.reflection ?? 0);
|
|
1559
2044
|
const viewDir = normalizeVec3(subVec3(camera.eye, triangle.worldCenter));
|
|
1560
2045
|
const reflectedLight = reflectVec3(scaleVec3(lightDir, -1), surfaceNormal);
|
|
1561
|
-
const gloss =
|
|
1562
|
-
const
|
|
1563
|
-
const
|
|
1564
|
-
|
|
2046
|
+
const gloss = mix(0.78, 0.14, clamp(material.roughness ?? 0.88, 0, 1)) + (material.metallic ?? 0) * 0.18;
|
|
2047
|
+
const specularPower = mix(26, 7, clamp(material.roughness ?? 0.88, 0, 1));
|
|
2048
|
+
const specular =
|
|
2049
|
+
Math.pow(clamp(dotVec3(reflectedLight, viewDir), 0, 1), specularPower) * gloss;
|
|
2050
|
+
const emissive = material.emissive ?? { r: 0, g: 0, b: 0 };
|
|
2051
|
+
const localLight = resolveLocalLightContribution(triangle, localLights);
|
|
2052
|
+
const occlusion = triangle.surfaceType === "water" ? shadowStrength * 0.018 : shadowStrength * 0.04;
|
|
2053
|
+
const detailed = applyMaterialDetail(
|
|
1565
2054
|
{
|
|
1566
|
-
r: clamp(
|
|
1567
|
-
|
|
1568
|
-
|
|
2055
|
+
r: clamp(
|
|
2056
|
+
shaded.r + reflection * 0.08 + specular * 0.16 + emissive.r * 0.42 + localLight.r - occlusion,
|
|
2057
|
+
0,
|
|
2058
|
+
1
|
|
2059
|
+
),
|
|
2060
|
+
g: clamp(
|
|
2061
|
+
shaded.g + reflection * 0.08 + specular * 0.16 + emissive.g * 0.42 + localLight.g - occlusion,
|
|
2062
|
+
0,
|
|
2063
|
+
1
|
|
2064
|
+
),
|
|
2065
|
+
b: clamp(
|
|
2066
|
+
shaded.b + reflection * 0.16 + specular * 0.22 + emissive.b * 0.46 + localLight.b - occlusion * 0.5,
|
|
2067
|
+
0,
|
|
2068
|
+
1
|
|
2069
|
+
),
|
|
1569
2070
|
},
|
|
1570
|
-
|
|
2071
|
+
material,
|
|
2072
|
+
triangle.worldCenter,
|
|
2073
|
+
surfaceNormal,
|
|
2074
|
+
triangle.surfaceType
|
|
2075
|
+
);
|
|
2076
|
+
const fill = colorToRgba(
|
|
2077
|
+
detailed,
|
|
2078
|
+
triangle.baseColor.a ?? 0.98
|
|
1571
2079
|
);
|
|
1572
2080
|
ctx.fillStyle = fill;
|
|
1573
2081
|
ctx.beginPath();
|
|
@@ -1606,78 +2114,111 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
|
|
|
1606
2114
|
ctx.restore();
|
|
1607
2115
|
}
|
|
1608
2116
|
|
|
1609
|
-
function pushHarborGeometry(camera, viewport, triangles,
|
|
1610
|
-
|
|
1611
|
-
{
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
2117
|
+
function pushHarborGeometry(camera, viewport, triangles, state) {
|
|
2118
|
+
if (!state.showcaseRealisticModelsEnabled) {
|
|
2119
|
+
for (const object of LEGACY_HARBOR_LAYOUT) {
|
|
2120
|
+
buildTrianglesFromMesh(
|
|
2121
|
+
{ positions: [object], indices: [0], normals: null, colors: null, material: createLegacyMeshPrimitive({})?.material, bounds: null, name: "legacy-structure" },
|
|
2122
|
+
{
|
|
2123
|
+
position: object.position,
|
|
2124
|
+
rotationY: object.rotationY,
|
|
2125
|
+
scale: object.scale,
|
|
2126
|
+
},
|
|
2127
|
+
object.color,
|
|
2128
|
+
camera,
|
|
2129
|
+
viewport,
|
|
2130
|
+
triangles,
|
|
2131
|
+
{
|
|
2132
|
+
accent: object.accent,
|
|
2133
|
+
reflection: 0,
|
|
2134
|
+
surfaceType: "structure",
|
|
2135
|
+
}
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
for (const placement of SHOWCASE_ENVIRONMENT_LAYOUT) {
|
|
2143
|
+
const mesh = state.assetCatalog?.environment?.[placement.assetKey] ?? null;
|
|
2144
|
+
if (!mesh) {
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
1633
2147
|
|
|
1634
|
-
for (const object of harborObjects) {
|
|
1635
2148
|
buildTrianglesFromMesh(
|
|
1636
|
-
|
|
2149
|
+
mesh,
|
|
1637
2150
|
{
|
|
1638
|
-
position:
|
|
1639
|
-
rotationY:
|
|
1640
|
-
scale:
|
|
2151
|
+
position: vec3(placement.position.x, placement.position.y, placement.position.z),
|
|
2152
|
+
rotationY: placement.rotationY,
|
|
2153
|
+
scale: placement.scale,
|
|
1641
2154
|
},
|
|
1642
|
-
|
|
2155
|
+
null,
|
|
1643
2156
|
camera,
|
|
1644
2157
|
viewport,
|
|
1645
2158
|
triangles,
|
|
1646
|
-
|
|
2159
|
+
{
|
|
2160
|
+
accent: placement.accent,
|
|
2161
|
+
reflection: 0,
|
|
2162
|
+
surfaceType: "structure",
|
|
2163
|
+
}
|
|
1647
2164
|
);
|
|
1648
2165
|
}
|
|
1649
2166
|
}
|
|
1650
2167
|
|
|
1651
2168
|
function renderShipRigging(ctx, ship, camera, viewport) {
|
|
1652
2169
|
const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
2170
|
+
const layout =
|
|
2171
|
+
ship.modelKey === "cutter"
|
|
2172
|
+
? {
|
|
2173
|
+
lineColor: "rgba(85, 89, 97, 0.92)",
|
|
2174
|
+
sailColor: "rgba(218, 232, 244, 0.28)",
|
|
2175
|
+
points: [
|
|
2176
|
+
vec3(0, 0.88, -0.32),
|
|
2177
|
+
vec3(0, 2.4, -0.28),
|
|
2178
|
+
vec3(0.1, 1.92, -0.3),
|
|
2179
|
+
vec3(1.18, 1.72, -0.18),
|
|
2180
|
+
vec3(1.04, 1.08, -0.12),
|
|
2181
|
+
],
|
|
2182
|
+
mastPairs: [[0, 1], [2, 3]],
|
|
2183
|
+
sailTriangle: [2, 3, 4],
|
|
2184
|
+
}
|
|
2185
|
+
: {
|
|
2186
|
+
lineColor: "rgba(73, 54, 45, 0.94)",
|
|
2187
|
+
sailColor: "rgba(238, 232, 214, 0.88)",
|
|
2188
|
+
points: [
|
|
2189
|
+
vec3(0, 0.38, -0.4),
|
|
2190
|
+
vec3(0, 3.8, -0.2),
|
|
2191
|
+
vec3(-0.25, 0.32, -1.9),
|
|
2192
|
+
vec3(-0.15, 2.7, -1.75),
|
|
2193
|
+
vec3(0.08, 3.2, -0.2),
|
|
2194
|
+
vec3(0.12, 1.2, -0.5),
|
|
2195
|
+
vec3(2.25, 2.25, 0.15),
|
|
2196
|
+
],
|
|
2197
|
+
mastPairs: [[0, 1], [2, 3]],
|
|
2198
|
+
sailTriangle: [4, 5, 6],
|
|
2199
|
+
};
|
|
2200
|
+
const projected = layout.points
|
|
2201
|
+
.map((point) => transformPoint(point, transform))
|
|
2202
|
+
.map((point) => projectPoint(point, camera, viewport));
|
|
1663
2203
|
if (projected.some((value) => value === null)) {
|
|
1664
2204
|
return;
|
|
1665
2205
|
}
|
|
1666
2206
|
|
|
1667
|
-
ctx.strokeStyle =
|
|
1668
|
-
ctx.lineWidth = 3.5;
|
|
2207
|
+
ctx.strokeStyle = layout.lineColor;
|
|
2208
|
+
ctx.lineWidth = ship.modelKey === "cutter" ? 2.2 : 3.5;
|
|
1669
2209
|
ctx.beginPath();
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
2210
|
+
for (const [from, to] of layout.mastPairs) {
|
|
2211
|
+
ctx.moveTo(projected[from].x, projected[from].y);
|
|
2212
|
+
ctx.lineTo(projected[to].x, projected[to].y);
|
|
2213
|
+
}
|
|
1674
2214
|
ctx.stroke();
|
|
1675
2215
|
|
|
1676
|
-
|
|
2216
|
+
const [a, b, c] = layout.sailTriangle;
|
|
2217
|
+
ctx.fillStyle = layout.sailColor;
|
|
1677
2218
|
ctx.beginPath();
|
|
1678
|
-
ctx.moveTo(projected[
|
|
1679
|
-
ctx.lineTo(projected[
|
|
1680
|
-
ctx.lineTo(projected[
|
|
2219
|
+
ctx.moveTo(projected[a].x, projected[a].y);
|
|
2220
|
+
ctx.lineTo(projected[b].x, projected[b].y);
|
|
2221
|
+
ctx.lineTo(projected[c].x, projected[c].y);
|
|
1681
2222
|
ctx.closePath();
|
|
1682
2223
|
ctx.fill();
|
|
1683
2224
|
}
|
|
@@ -1932,10 +2473,10 @@ function resolveBoundaryCollision(ship, state, shipModel) {
|
|
|
1932
2473
|
}
|
|
1933
2474
|
}
|
|
1934
2475
|
|
|
1935
|
-
function resolveShipCollision(state, a, b,
|
|
2476
|
+
function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
|
|
1936
2477
|
const delta = subVec3(b.position, a.position);
|
|
1937
|
-
const radiusA = getShipCollisionRadius(a,
|
|
1938
|
-
const radiusB = getShipCollisionRadius(b,
|
|
2478
|
+
const radiusA = getShipCollisionRadius(a, shipModelA);
|
|
2479
|
+
const radiusB = getShipCollisionRadius(b, shipModelB);
|
|
1939
2480
|
const distance = Math.hypot(delta.x, delta.z);
|
|
1940
2481
|
const minDistance = radiusA + radiusB;
|
|
1941
2482
|
if (distance >= minDistance) {
|
|
@@ -1948,8 +2489,8 @@ function resolveShipCollision(state, a, b, shipModel) {
|
|
|
1948
2489
|
: normalizeVec3(vec3(Math.cos(state.time * 5.2), 0, Math.sin(state.time * 4.8)));
|
|
1949
2490
|
const tangent = vec3(-normal.z, 0, normal.x);
|
|
1950
2491
|
const penetration = minDistance - distance;
|
|
1951
|
-
const invMassA = getShipInverseMass(a,
|
|
1952
|
-
const invMassB = getShipInverseMass(b,
|
|
2492
|
+
const invMassA = getShipInverseMass(a, shipModelA);
|
|
2493
|
+
const invMassB = getShipInverseMass(b, shipModelB);
|
|
1953
2494
|
const invMassSum = invMassA + invMassB;
|
|
1954
2495
|
const correction = scaleVec3(normal, (penetration / Math.max(0.0001, invMassSum)) * 0.72);
|
|
1955
2496
|
a.position = subVec3(a.position, scaleVec3(correction, invMassA));
|
|
@@ -1957,7 +2498,11 @@ function resolveShipCollision(state, a, b, shipModel) {
|
|
|
1957
2498
|
|
|
1958
2499
|
const relativeVelocity = subVec3(b.velocity, a.velocity);
|
|
1959
2500
|
const velocityAlongNormal = dotVec3(relativeVelocity, normal);
|
|
1960
|
-
const restitution =
|
|
2501
|
+
const restitution =
|
|
2502
|
+
((readPhysicsNumber(shipModelA.physics, "restitution", 0.22) +
|
|
2503
|
+
readPhysicsNumber(shipModelB.physics, "restitution", 0.22)) /
|
|
2504
|
+
2) *
|
|
2505
|
+
0.88;
|
|
1961
2506
|
if (velocityAlongNormal < 0) {
|
|
1962
2507
|
const impulseMagnitude =
|
|
1963
2508
|
(-(1 + restitution) * velocityAlongNormal) / Math.max(0.0001, invMassSum);
|
|
@@ -1976,10 +2521,10 @@ function resolveShipCollision(state, a, b, shipModel) {
|
|
|
1976
2521
|
b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
|
|
1977
2522
|
|
|
1978
2523
|
a.angularVelocity -=
|
|
1979
|
-
tangentSpeed * radiusA * getShipInverseInertia(a,
|
|
2524
|
+
tangentSpeed * radiusA * getShipInverseInertia(a, shipModelA) * 0.2 +
|
|
1980
2525
|
impulseMagnitude * 0.00024;
|
|
1981
2526
|
b.angularVelocity +=
|
|
1982
|
-
tangentSpeed * radiusB * getShipInverseInertia(b,
|
|
2527
|
+
tangentSpeed * radiusB * getShipInverseInertia(b, shipModelB) * 0.2 +
|
|
1983
2528
|
impulseMagnitude * 0.00024;
|
|
1984
2529
|
|
|
1985
2530
|
const impactSpeed = Math.abs(velocityAlongNormal);
|
|
@@ -2019,14 +2564,19 @@ function updateShips(state, dt, shipModel) {
|
|
|
2019
2564
|
state.contactCount = 0;
|
|
2020
2565
|
|
|
2021
2566
|
for (const ship of state.ships) {
|
|
2022
|
-
|
|
2023
|
-
|
|
2567
|
+
const activeShipModel = resolveShipModel(state, ship, shipModel);
|
|
2568
|
+
updateShipMotion(state, ship, dt, activeShipModel);
|
|
2569
|
+
resolveBoundaryCollision(ship, state, activeShipModel);
|
|
2024
2570
|
}
|
|
2025
2571
|
|
|
2026
2572
|
for (let index = 0; index < state.ships.length; index += 1) {
|
|
2027
2573
|
for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
|
|
2574
|
+
const shipA = state.ships[index];
|
|
2575
|
+
const shipB = state.ships[otherIndex];
|
|
2576
|
+
const shipModelA = resolveShipModel(state, shipA, shipModel);
|
|
2577
|
+
const shipModelB = resolveShipModel(state, shipB, shipModel);
|
|
2028
2578
|
collided =
|
|
2029
|
-
resolveShipCollision(state,
|
|
2579
|
+
resolveShipCollision(state, shipA, shipB, shipModelA, shipModelB) ||
|
|
2030
2580
|
collided;
|
|
2031
2581
|
}
|
|
2032
2582
|
}
|
|
@@ -2318,6 +2868,108 @@ function renderWaterLightReflection(ctx, source, state, camera, viewport) {
|
|
|
2318
2868
|
ctx.restore();
|
|
2319
2869
|
}
|
|
2320
2870
|
|
|
2871
|
+
function renderLighthouseBeam(ctx, state, camera, viewport, visuals) {
|
|
2872
|
+
const lighthousePlacement = SHOWCASE_ENVIRONMENT_LAYOUT.find(
|
|
2873
|
+
(placement) => placement.assetKey === "lighthouse"
|
|
2874
|
+
);
|
|
2875
|
+
if (!lighthousePlacement || !state.showcaseRealisticModelsEnabled) {
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
const source = transformPoint(
|
|
2880
|
+
vec3(0, 11.34, 0),
|
|
2881
|
+
{
|
|
2882
|
+
position: vec3(
|
|
2883
|
+
lighthousePlacement.position.x,
|
|
2884
|
+
lighthousePlacement.position.y,
|
|
2885
|
+
lighthousePlacement.position.z
|
|
2886
|
+
),
|
|
2887
|
+
rotationY: lighthousePlacement.rotationY,
|
|
2888
|
+
scale: lighthousePlacement.scale,
|
|
2889
|
+
}
|
|
2890
|
+
);
|
|
2891
|
+
const sweep = state.time * 0.22 + 0.8;
|
|
2892
|
+
const direction = normalizeVec3(vec3(Math.sin(sweep), -0.07, Math.cos(sweep)));
|
|
2893
|
+
const spread = perpendicularOnWater(direction);
|
|
2894
|
+
const farCenter = addVec3(source, scaleVec3(direction, 34));
|
|
2895
|
+
const left = addVec3(farCenter, scaleVec3(spread, 7.4));
|
|
2896
|
+
const right = addVec3(farCenter, scaleVec3(spread, -7.4));
|
|
2897
|
+
const projectedSource = projectPoint(source, camera, viewport);
|
|
2898
|
+
const projectedLeft = projectPoint(left, camera, viewport);
|
|
2899
|
+
const projectedRight = projectPoint(right, camera, viewport);
|
|
2900
|
+
if (!projectedSource || !projectedLeft || !projectedRight) {
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
const pulse = 0.72 + Math.sin(state.time * 1.7) * 0.08;
|
|
2905
|
+
ctx.save();
|
|
2906
|
+
ctx.globalCompositeOperation = "screen";
|
|
2907
|
+
ctx.fillStyle = colorToRgba(visuals.torchCore, 0.055 * pulse);
|
|
2908
|
+
ctx.beginPath();
|
|
2909
|
+
ctx.moveTo(projectedSource.x, projectedSource.y);
|
|
2910
|
+
ctx.lineTo(projectedLeft.x, projectedLeft.y);
|
|
2911
|
+
ctx.lineTo(projectedRight.x, projectedRight.y);
|
|
2912
|
+
ctx.closePath();
|
|
2913
|
+
ctx.fill();
|
|
2914
|
+
|
|
2915
|
+
const beamLength = Math.hypot(
|
|
2916
|
+
projectedLeft.x - projectedSource.x,
|
|
2917
|
+
projectedLeft.y - projectedSource.y
|
|
2918
|
+
);
|
|
2919
|
+
const core = ctx.createRadialGradient(
|
|
2920
|
+
projectedSource.x,
|
|
2921
|
+
projectedSource.y,
|
|
2922
|
+
2,
|
|
2923
|
+
projectedSource.x,
|
|
2924
|
+
projectedSource.y,
|
|
2925
|
+
clamp(beamLength * 0.22, 18, 80)
|
|
2926
|
+
);
|
|
2927
|
+
core.addColorStop(0, colorToRgba(visuals.torchCore, 0.58));
|
|
2928
|
+
core.addColorStop(0.5, colorToRgba(visuals.torchGlow, 0.18));
|
|
2929
|
+
core.addColorStop(1, colorToRgba(visuals.torchGlow, 0));
|
|
2930
|
+
ctx.fillStyle = core;
|
|
2931
|
+
ctx.beginPath();
|
|
2932
|
+
ctx.arc(projectedSource.x, projectedSource.y, clamp(beamLength * 0.18, 14, 64), 0, Math.PI * 2);
|
|
2933
|
+
ctx.fill();
|
|
2934
|
+
ctx.restore();
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
function renderAtmosphericGrade(ctx, canvas, state, visuals) {
|
|
2938
|
+
const vignette = ctx.createRadialGradient(
|
|
2939
|
+
canvas.width * 0.5,
|
|
2940
|
+
canvas.height * 0.48,
|
|
2941
|
+
canvas.width * 0.2,
|
|
2942
|
+
canvas.width * 0.5,
|
|
2943
|
+
canvas.height * 0.5,
|
|
2944
|
+
canvas.width * 0.72
|
|
2945
|
+
);
|
|
2946
|
+
vignette.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
2947
|
+
vignette.addColorStop(0.68, "rgba(0, 0, 0, 0.08)");
|
|
2948
|
+
vignette.addColorStop(1, "rgba(0, 0, 0, 0.32)");
|
|
2949
|
+
ctx.fillStyle = vignette;
|
|
2950
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
2951
|
+
|
|
2952
|
+
const seaHaze = ctx.createLinearGradient(0, canvas.height * 0.34, 0, canvas.height);
|
|
2953
|
+
seaHaze.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
2954
|
+
seaHaze.addColorStop(0.5, visuals.ambientMist);
|
|
2955
|
+
seaHaze.addColorStop(1, "rgba(3, 8, 16, 0.18)");
|
|
2956
|
+
ctx.fillStyle = seaHaze;
|
|
2957
|
+
ctx.fillRect(0, canvas.height * 0.34, canvas.width, canvas.height * 0.66);
|
|
2958
|
+
|
|
2959
|
+
if (state.captureMode) {
|
|
2960
|
+
ctx.save();
|
|
2961
|
+
ctx.globalCompositeOperation = "screen";
|
|
2962
|
+
for (let index = 0; index < 70; index += 1) {
|
|
2963
|
+
const x = pseudoRandom(index * 19 + 3) * canvas.width;
|
|
2964
|
+
const y = pseudoRandom(index * 23 + 7) * canvas.height;
|
|
2965
|
+
const alpha = 0.008 + pseudoRandom(index * 31 + 11) * 0.012;
|
|
2966
|
+
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
|
|
2967
|
+
ctx.fillRect(x, y, 1.1, 1.1);
|
|
2968
|
+
}
|
|
2969
|
+
ctx.restore();
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2321
2973
|
function renderWaterMotionEffects(ctx, effects, camera, viewport) {
|
|
2322
2974
|
ctx.save();
|
|
2323
2975
|
ctx.globalCompositeOperation = "screen";
|
|
@@ -2450,6 +3102,15 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2450
3102
|
normal,
|
|
2451
3103
|
baseColor: bandMesh.color,
|
|
2452
3104
|
accent: bandAccent,
|
|
3105
|
+
material: {
|
|
3106
|
+
name: "water-surface",
|
|
3107
|
+
color: bandMesh.color,
|
|
3108
|
+
roughness: 0.2,
|
|
3109
|
+
metallic: 0,
|
|
3110
|
+
emissive: { r: 0, g: 0, b: 0 },
|
|
3111
|
+
},
|
|
3112
|
+
reflection: 1,
|
|
3113
|
+
surfaceType: "water",
|
|
2453
3114
|
});
|
|
2454
3115
|
}
|
|
2455
3116
|
}
|
|
@@ -2457,7 +3118,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2457
3118
|
const waterMotionEffects = buildWaterMotionEffects(state);
|
|
2458
3119
|
const lightSources = collectSceneLightSources(state, visuals);
|
|
2459
3120
|
|
|
2460
|
-
pushHarborGeometry(camera, viewport, sceneTriangles,
|
|
3121
|
+
pushHarborGeometry(camera, viewport, sceneTriangles, state);
|
|
2461
3122
|
const cloth = buildClothSurface(
|
|
2462
3123
|
state,
|
|
2463
3124
|
state,
|
|
@@ -2480,24 +3141,47 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2480
3141
|
normal,
|
|
2481
3142
|
baseColor: cloth.color,
|
|
2482
3143
|
accent: cloth.band === "near" ? 0.1 : 0.04,
|
|
3144
|
+
material: {
|
|
3145
|
+
name: "flag-cloth",
|
|
3146
|
+
color: cloth.color,
|
|
3147
|
+
roughness: 0.94,
|
|
3148
|
+
metallic: 0,
|
|
3149
|
+
emissive: { r: 0, g: 0, b: 0 },
|
|
3150
|
+
},
|
|
3151
|
+
reflection: 0,
|
|
3152
|
+
surfaceType: "cloth",
|
|
2483
3153
|
});
|
|
2484
3154
|
}
|
|
2485
3155
|
|
|
2486
3156
|
for (const ship of state.ships) {
|
|
3157
|
+
const activeShipModel = resolveShipModel(state, ship, shipModel);
|
|
2487
3158
|
buildTrianglesFromMesh(
|
|
2488
|
-
|
|
3159
|
+
activeShipModel,
|
|
2489
3160
|
{ position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
|
|
2490
3161
|
ship.tint,
|
|
2491
3162
|
camera,
|
|
2492
3163
|
viewport,
|
|
2493
3164
|
sceneTriangles,
|
|
2494
|
-
|
|
3165
|
+
{
|
|
3166
|
+
accent: nearLighting.rtParticipation.directShadows === "premium" ? 0.08 : 0.02,
|
|
3167
|
+
reflection: 0,
|
|
3168
|
+
surfaceType: "ship",
|
|
3169
|
+
}
|
|
2495
3170
|
);
|
|
2496
3171
|
}
|
|
2497
3172
|
|
|
2498
3173
|
drawTriangles(ctx, waterTriangles, lightDir, reflectionStrength, camera, shadowStrength);
|
|
2499
3174
|
for (const ship of state.ships) {
|
|
2500
|
-
renderShipShadow(
|
|
3175
|
+
renderShipShadow(
|
|
3176
|
+
ctx,
|
|
3177
|
+
resolveShipModel(state, ship, shipModel),
|
|
3178
|
+
ship,
|
|
3179
|
+
state,
|
|
3180
|
+
camera,
|
|
3181
|
+
viewport,
|
|
3182
|
+
lightDir,
|
|
3183
|
+
shadowStrength
|
|
3184
|
+
);
|
|
2501
3185
|
}
|
|
2502
3186
|
renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
|
|
2503
3187
|
for (const source of lightSources.reflectionLights) {
|
|
@@ -2505,9 +3189,18 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2505
3189
|
}
|
|
2506
3190
|
renderWaterMotionEffects(ctx, waterMotionEffects, camera, viewport);
|
|
2507
3191
|
renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
|
|
2508
|
-
drawTriangles(
|
|
3192
|
+
drawTriangles(
|
|
3193
|
+
ctx,
|
|
3194
|
+
sceneTriangles,
|
|
3195
|
+
lightDir,
|
|
3196
|
+
reflectionStrength,
|
|
3197
|
+
camera,
|
|
3198
|
+
shadowStrength,
|
|
3199
|
+
lightSources.directLights
|
|
3200
|
+
);
|
|
2509
3201
|
renderFlagPole(ctx, camera, viewport);
|
|
2510
3202
|
renderClothAccent(ctx, cloth, camera, viewport);
|
|
3203
|
+
renderLighthouseBeam(ctx, state, camera, viewport, visuals);
|
|
2511
3204
|
for (const source of lightSources.directLights) {
|
|
2512
3205
|
renderDirectLightGlow(ctx, source, camera, viewport);
|
|
2513
3206
|
}
|
|
@@ -2515,6 +3208,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2515
3208
|
renderShipRigging(ctx, ship, camera, viewport);
|
|
2516
3209
|
}
|
|
2517
3210
|
renderSprays(ctx, state.sprays, camera, viewport);
|
|
3211
|
+
renderAtmosphericGrade(ctx, canvas, state, visuals);
|
|
2518
3212
|
|
|
2519
3213
|
const debugSnapshot = state.debugSession.getSnapshot();
|
|
2520
3214
|
const quality = {
|
|
@@ -2525,11 +3219,11 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2525
3219
|
|
|
2526
3220
|
const sceneMetrics = [
|
|
2527
3221
|
`focus: ${state.focus}`,
|
|
2528
|
-
`ships: ${state.ships.length} active GLTF hulls`,
|
|
2529
|
-
`moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.
|
|
3222
|
+
`ships: ${state.ships.length} active GLTF hulls across ${new Set(state.ships.map((ship) => ship.modelKey)).size} model families`,
|
|
3223
|
+
`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`,
|
|
2530
3224
|
`physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
|
|
2531
3225
|
`physics contacts: ${state.contactCount}`,
|
|
2532
|
-
`mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, shipModel) / 1000).toFixed(1)}t`).join(" · ")}`,
|
|
3226
|
+
`mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, resolveShipModel(state, ship, shipModel)) / 1000).toFixed(1)}t`).join(" · ")}`,
|
|
2533
3227
|
`cloth band: ${cloth.band} -> ${cloth.representation.output}`,
|
|
2534
3228
|
`fluid near band: ${water.bandMeshes[0].representation.output}`,
|
|
2535
3229
|
`lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`,
|
|
@@ -2552,12 +3246,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2552
3246
|
];
|
|
2553
3247
|
const sceneNotes =
|
|
2554
3248
|
state.focus === "physics"
|
|
2555
|
-
?
|
|
2556
|
-
|
|
2557
|
-
"The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
|
|
2558
|
-
"Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water.",
|
|
2559
|
-
]
|
|
2560
|
-
: SCENE_NOTES;
|
|
3249
|
+
? PHYSICS_SCENE_NOTE_KEYS.map((key) => state.translate(key))
|
|
3250
|
+
: SCENE_NOTE_KEYS.map((key) => state.translate(key));
|
|
2561
3251
|
const custom = state.demoDescription ?? null;
|
|
2562
3252
|
|
|
2563
3253
|
setListContent(
|
|
@@ -2577,13 +3267,23 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2577
3267
|
dom.status.textContent =
|
|
2578
3268
|
typeof custom?.status === "string"
|
|
2579
3269
|
? custom.status
|
|
2580
|
-
:
|
|
3270
|
+
: state.translate(gpuSharedTranslationKeys.statusLive, {
|
|
3271
|
+
fps: state.lastDecision.metrics.fps.toFixed(1),
|
|
3272
|
+
});
|
|
2581
3273
|
dom.details.textContent =
|
|
2582
3274
|
typeof custom?.details === "string"
|
|
2583
3275
|
? custom.details
|
|
2584
3276
|
: state.focus === "physics"
|
|
2585
|
-
?
|
|
2586
|
-
|
|
3277
|
+
? state.translate(gpuSharedTranslationKeys.detailsPhysics, {
|
|
3278
|
+
snapshotStageId: state.physics.plan.snapshotStageId,
|
|
3279
|
+
})
|
|
3280
|
+
: state.showcaseRealisticModelsEnabled
|
|
3281
|
+
? state.translate(gpuSharedTranslationKeys.detailsRealistic, {
|
|
3282
|
+
pressureLevel: state.lastDecision.pressureLevel,
|
|
3283
|
+
})
|
|
3284
|
+
: state.translate(gpuSharedTranslationKeys.detailsLegacy, {
|
|
3285
|
+
pressureLevel: state.lastDecision.pressureLevel,
|
|
3286
|
+
});
|
|
2587
3287
|
}
|
|
2588
3288
|
|
|
2589
3289
|
function updateSceneState(state, dt, shipModel) {
|
|
@@ -2615,15 +3315,18 @@ function syncTextState(state, shipModel) {
|
|
|
2615
3315
|
stress: state.stress,
|
|
2616
3316
|
ships: state.ships.map((ship) => ({
|
|
2617
3317
|
id: ship.id,
|
|
3318
|
+
modelKey: ship.modelKey ?? "brigantine",
|
|
2618
3319
|
x: Number(ship.position.x.toFixed(2)),
|
|
2619
3320
|
y: Number(ship.position.y.toFixed(2)),
|
|
2620
3321
|
z: Number(ship.position.z.toFixed(2)),
|
|
2621
3322
|
vx: Number(ship.velocity.x.toFixed(2)),
|
|
2622
3323
|
vz: Number(ship.velocity.z.toFixed(2)),
|
|
2623
|
-
massKg: Math.round(getShipMass(ship, shipModel)),
|
|
3324
|
+
massKg: Math.round(getShipMass(ship, resolveShipModel(state, ship, shipModel))),
|
|
2624
3325
|
lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0,
|
|
2625
3326
|
})),
|
|
2626
|
-
shipPhysics:
|
|
3327
|
+
shipPhysics: Object.fromEntries(
|
|
3328
|
+
state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
|
|
3329
|
+
),
|
|
2627
3330
|
sprays: state.sprays.length,
|
|
2628
3331
|
waveImpulses: state.waveImpulses.length,
|
|
2629
3332
|
pressure: state.lastDecision?.pressureLevel ?? "stable",
|
|
@@ -2647,23 +3350,39 @@ function syncTextState(state, shipModel) {
|
|
|
2647
3350
|
};
|
|
2648
3351
|
}
|
|
2649
3352
|
|
|
2650
|
-
export async function mountGpuShowcase(options = {}) {
|
|
3353
|
+
export async function mountGpuShowcase(options = {}, featureFlags = null) {
|
|
2651
3354
|
injectStyles();
|
|
2652
3355
|
const root = options.root ?? document.body;
|
|
2653
3356
|
root.classList?.add?.(ROOT_CLASS);
|
|
3357
|
+
const captureSettings = resolveCaptureSettings(options);
|
|
3358
|
+
if (captureSettings.captureMode) {
|
|
3359
|
+
root.classList?.add?.(CAPTURE_CLASS);
|
|
3360
|
+
}
|
|
2654
3361
|
const previousMarkup = root.innerHTML;
|
|
2655
3362
|
const previousRenderGameToText = window.render_game_to_text;
|
|
2656
3363
|
const previousAdvanceTime = window.advanceTime;
|
|
2657
3364
|
const focus = options.focus ?? new URLSearchParams(window.location.search).get("focus") ?? "integrated";
|
|
3365
|
+
const translate = createGpuSharedTranslator(options.translate);
|
|
2658
3366
|
const dom = buildDemoDom(root, {
|
|
2659
3367
|
packageName: options.packageName ?? "@plasius/gpu-demo-viewer",
|
|
2660
|
-
title: options.title ??
|
|
2661
|
-
subtitle: options.subtitle ??
|
|
3368
|
+
title: options.title ?? translate(gpuSharedTranslationKeys.showcaseTitle),
|
|
3369
|
+
subtitle: options.subtitle ?? translate(gpuSharedTranslationKeys.showcaseSubtitle),
|
|
3370
|
+
translate,
|
|
2662
3371
|
});
|
|
2663
3372
|
dom.focusMode.value = focus;
|
|
3373
|
+
const state = createSceneState({
|
|
3374
|
+
focus,
|
|
3375
|
+
translate,
|
|
3376
|
+
realisticModelsEnabled: isFeatureEnabled(featureFlags, GPU_SHOWCASE_REALISTIC_MODELS_FEATURE, true),
|
|
3377
|
+
captureMode: captureSettings.captureMode,
|
|
3378
|
+
renderScale: captureSettings.renderScale,
|
|
3379
|
+
});
|
|
3380
|
+
const assetCatalog = await (state.showcaseRealisticModelsEnabled
|
|
3381
|
+
? loadShowcaseAssetCatalog()
|
|
3382
|
+
: createLegacyShowcaseAssetCatalog());
|
|
3383
|
+
const shipModel = assetCatalog.ships[assetCatalog.primaryShipKey];
|
|
2664
3384
|
|
|
2665
|
-
|
|
2666
|
-
const shipModel = await loadGltfModel(resolveShowcaseAssetUrl());
|
|
3385
|
+
state.assetCatalog = assetCatalog;
|
|
2667
3386
|
state.shipModel = shipModel;
|
|
2668
3387
|
state.packageState =
|
|
2669
3388
|
typeof options.createState === "function" ? options.createState() : undefined;
|
|
@@ -2676,6 +3395,8 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2676
3395
|
if (!ctx) {
|
|
2677
3396
|
throw new Error("2D canvas context is required for the shared showcase.");
|
|
2678
3397
|
}
|
|
3398
|
+
ctx.imageSmoothingEnabled = true;
|
|
3399
|
+
ctx.imageSmoothingQuality = "high";
|
|
2679
3400
|
let animationFrameId = 0;
|
|
2680
3401
|
let destroyed = false;
|
|
2681
3402
|
const renderFrame = (nowMs) => {
|
|
@@ -2697,6 +3418,7 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2697
3418
|
}
|
|
2698
3419
|
|
|
2699
3420
|
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
3421
|
+
resizeCanvasToDisplaySize(dom.canvas, state);
|
|
2700
3422
|
renderScene(ctx, dom.canvas, state, shipModel, dom);
|
|
2701
3423
|
syncTextState(state, shipModel);
|
|
2702
3424
|
animationFrameId = requestAnimationFrame(renderFrame);
|
|
@@ -2704,7 +3426,9 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2704
3426
|
|
|
2705
3427
|
const handlePauseClick = () => {
|
|
2706
3428
|
state.paused = !state.paused;
|
|
2707
|
-
dom.pauseButton.textContent = state.paused
|
|
3429
|
+
dom.pauseButton.textContent = state.paused
|
|
3430
|
+
? state.translate(gpuSharedTranslationKeys.resume)
|
|
3431
|
+
: state.translate(gpuSharedTranslationKeys.pause);
|
|
2708
3432
|
};
|
|
2709
3433
|
const handleStressChange = () => {
|
|
2710
3434
|
state.stress = dom.stressToggle.checked;
|
|
@@ -2741,6 +3465,7 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2741
3465
|
state.packageState = undefined;
|
|
2742
3466
|
}
|
|
2743
3467
|
root.classList?.remove?.(ROOT_CLASS);
|
|
3468
|
+
root.classList?.remove?.(CAPTURE_CLASS);
|
|
2744
3469
|
root.innerHTML = previousMarkup;
|
|
2745
3470
|
if (typeof previousRenderGameToText === "function") {
|
|
2746
3471
|
window.render_game_to_text = previousRenderGameToText;
|
|
@@ -2762,6 +3487,13 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2762
3487
|
}
|
|
2763
3488
|
|
|
2764
3489
|
function updatePhysicsSnapshot(state, shipModel) {
|
|
3490
|
+
const rigidBodyShapes = Object.fromEntries(
|
|
3491
|
+
state.ships.map((ship) => [
|
|
3492
|
+
ship.id,
|
|
3493
|
+
resolveShipModel(state, ship, shipModel)?.physics?.shape ?? "box",
|
|
3494
|
+
])
|
|
3495
|
+
);
|
|
3496
|
+
|
|
2765
3497
|
state.physics.snapshot = createPhysicsWorldSnapshot({
|
|
2766
3498
|
frameId: `showcase-${state.frame}`,
|
|
2767
3499
|
tick: state.frame,
|
|
@@ -2778,12 +3510,14 @@ function updatePhysicsSnapshot(state, shipModel) {
|
|
|
2778
3510
|
contactCount: state.contactCount,
|
|
2779
3511
|
snapshotStageId: state.physics.plan.snapshotStageId,
|
|
2780
3512
|
rigidBodyShape: shipModel.physics.shape ?? "box",
|
|
3513
|
+
rigidBodyShapes,
|
|
2781
3514
|
},
|
|
2782
3515
|
});
|
|
2783
3516
|
}
|
|
2784
3517
|
|
|
2785
3518
|
export {
|
|
2786
3519
|
advanceShowcaseClothSimulationState as __testOnlyAdvanceShowcaseClothSimulationState,
|
|
3520
|
+
buildWaterBands as __testOnlyBuildWaterBands,
|
|
2787
3521
|
buildWaterMotionEffects as __testOnlyBuildWaterMotionEffects,
|
|
2788
3522
|
collectSceneLightSources as __testOnlyCollectSceneLightSources,
|
|
2789
3523
|
createShowcaseClothSimulationState as __testOnlyCreateShowcaseClothSimulationState,
|