@plasius/gpu-shared 0.1.11 → 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 +36 -3
- package/README.md +55 -1
- 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 +1255 -279
- 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-2ZNPKD7D.js → showcase-runtime-PN7N3FZY.js} +808 -237
- 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 +912 -188
- 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.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",
|
|
@@ -1375,6 +1802,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
|
|
|
1375
1802
|
}
|
|
1376
1803
|
|
|
1377
1804
|
function createSceneState(options) {
|
|
1805
|
+
const translate = options.translate;
|
|
1378
1806
|
const { governor, fluidDetail, clothDetail, lightingDetail } = createPerformanceGovernor();
|
|
1379
1807
|
const physicsProfile = defaultPhysicsWorkerProfile;
|
|
1380
1808
|
const physicsPlan = createPhysicsSimulationPlan(physicsProfile);
|
|
@@ -1382,7 +1810,7 @@ function createSceneState(options) {
|
|
|
1382
1810
|
const debugSession = createGpuDebugSession({
|
|
1383
1811
|
enabled: true,
|
|
1384
1812
|
adapter: {
|
|
1385
|
-
label:
|
|
1813
|
+
label: translate(gpuSharedTranslationKeys.debugAdapterShowcase),
|
|
1386
1814
|
memoryCapacityHintBytes: 6 * 1024 * 1024 * 1024,
|
|
1387
1815
|
coreCountHint: 12,
|
|
1388
1816
|
},
|
|
@@ -1392,23 +1820,27 @@ function createSceneState(options) {
|
|
|
1392
1820
|
owner: "renderer",
|
|
1393
1821
|
category: "texture",
|
|
1394
1822
|
sizeBytes: 1280 * 720 * 4,
|
|
1395
|
-
label:
|
|
1823
|
+
label: translate(gpuSharedTranslationKeys.debugMainColorBuffer),
|
|
1396
1824
|
});
|
|
1397
1825
|
debugSession.trackAllocation({
|
|
1398
1826
|
id: "showcase.shadow-impression",
|
|
1399
1827
|
owner: "lighting",
|
|
1400
1828
|
category: "texture",
|
|
1401
1829
|
sizeBytes: 12 * 1024 * 1024,
|
|
1402
|
-
label:
|
|
1830
|
+
label: translate(gpuSharedTranslationKeys.debugShadowImpressionAtlas),
|
|
1403
1831
|
});
|
|
1404
1832
|
|
|
1405
1833
|
return {
|
|
1834
|
+
translate,
|
|
1406
1835
|
focus: options.focus,
|
|
1407
1836
|
governor,
|
|
1408
1837
|
fluidDetail,
|
|
1409
1838
|
clothDetail,
|
|
1410
1839
|
lightingDetail,
|
|
1411
1840
|
debugSession,
|
|
1841
|
+
showcaseRealisticModelsEnabled: options.realisticModelsEnabled !== false,
|
|
1842
|
+
captureMode: options.captureMode === true,
|
|
1843
|
+
renderScale: readPositiveNumber(options.renderScale, undefined),
|
|
1412
1844
|
packageState: undefined,
|
|
1413
1845
|
demoDescription: null,
|
|
1414
1846
|
demoVisuals: null,
|
|
@@ -1423,6 +1855,7 @@ function createSceneState(options) {
|
|
|
1423
1855
|
ships: [
|
|
1424
1856
|
{
|
|
1425
1857
|
id: "northwind",
|
|
1858
|
+
modelKey: "brigantine",
|
|
1426
1859
|
position: vec3(-5.2, 0, 7.2),
|
|
1427
1860
|
velocity: vec3(2.35, 0, -1.08),
|
|
1428
1861
|
rotationY: 0.58,
|
|
@@ -1433,17 +1866,18 @@ function createSceneState(options) {
|
|
|
1433
1866
|
throttleResponse: 0.46,
|
|
1434
1867
|
rudderResponse: 0.54,
|
|
1435
1868
|
wanderPhase: 0.35,
|
|
1436
|
-
lanterns:
|
|
1869
|
+
lanterns: CUTTER_LANTERNS,
|
|
1437
1870
|
lanternStrength: 1.06,
|
|
1438
1871
|
collisionRadiusScale: 1.04,
|
|
1439
1872
|
},
|
|
1440
1873
|
{
|
|
1441
1874
|
id: "tidecaller",
|
|
1875
|
+
modelKey: "cutter",
|
|
1442
1876
|
position: vec3(4.8, 0, 4.4),
|
|
1443
1877
|
velocity: vec3(-2.15, 0, 1.74),
|
|
1444
1878
|
rotationY: -2.48,
|
|
1445
1879
|
angularVelocity: -0.2,
|
|
1446
|
-
tint: { r: 0.
|
|
1880
|
+
tint: { r: 0.58, g: 0.24, b: 0.16 },
|
|
1447
1881
|
massScale: 0.84,
|
|
1448
1882
|
cruiseSpeed: 2.68,
|
|
1449
1883
|
throttleResponse: 0.7,
|
|
@@ -1467,6 +1901,7 @@ function createSceneState(options) {
|
|
|
1467
1901
|
manifest: physicsManifest,
|
|
1468
1902
|
snapshot: null,
|
|
1469
1903
|
},
|
|
1904
|
+
assetCatalog: null,
|
|
1470
1905
|
shipModel: null,
|
|
1471
1906
|
};
|
|
1472
1907
|
}
|
|
@@ -1553,10 +1988,51 @@ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, s
|
|
|
1553
1988
|
}
|
|
1554
1989
|
}
|
|
1555
1990
|
|
|
1556
|
-
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
|
+
) {
|
|
1557
2028
|
triangles.sort((left, right) => right.depth - left.depth);
|
|
1558
2029
|
for (const triangle of triangles) {
|
|
1559
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
|
+
};
|
|
1560
2036
|
const shaded = shadeColor(
|
|
1561
2037
|
triangle.baseColor,
|
|
1562
2038
|
surfaceNormal,
|
|
@@ -1564,19 +2040,42 @@ function drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, sha
|
|
|
1564
2040
|
clamp((triangle.worldCenter.y + 3) / 10, 0, 1),
|
|
1565
2041
|
triangle.accent
|
|
1566
2042
|
);
|
|
1567
|
-
const reflection =
|
|
2043
|
+
const reflection = reflectionStrength * (triangle.reflection ?? 0);
|
|
1568
2044
|
const viewDir = normalizeVec3(subVec3(camera.eye, triangle.worldCenter));
|
|
1569
2045
|
const reflectedLight = reflectVec3(scaleVec3(lightDir, -1), surfaceNormal);
|
|
1570
|
-
const gloss =
|
|
1571
|
-
const
|
|
1572
|
-
const
|
|
1573
|
-
|
|
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(
|
|
1574
2054
|
{
|
|
1575
|
-
r: clamp(
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
+
),
|
|
1578
2070
|
},
|
|
1579
|
-
|
|
2071
|
+
material,
|
|
2072
|
+
triangle.worldCenter,
|
|
2073
|
+
surfaceNormal,
|
|
2074
|
+
triangle.surfaceType
|
|
2075
|
+
);
|
|
2076
|
+
const fill = colorToRgba(
|
|
2077
|
+
detailed,
|
|
2078
|
+
triangle.baseColor.a ?? 0.98
|
|
1580
2079
|
);
|
|
1581
2080
|
ctx.fillStyle = fill;
|
|
1582
2081
|
ctx.beginPath();
|
|
@@ -1615,78 +2114,111 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
|
|
|
1615
2114
|
ctx.restore();
|
|
1616
2115
|
}
|
|
1617
2116
|
|
|
1618
|
-
function pushHarborGeometry(camera, viewport, triangles,
|
|
1619
|
-
|
|
1620
|
-
{
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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
|
+
}
|
|
1642
2147
|
|
|
1643
|
-
for (const object of harborObjects) {
|
|
1644
2148
|
buildTrianglesFromMesh(
|
|
1645
|
-
|
|
2149
|
+
mesh,
|
|
1646
2150
|
{
|
|
1647
|
-
position:
|
|
1648
|
-
rotationY:
|
|
1649
|
-
scale:
|
|
2151
|
+
position: vec3(placement.position.x, placement.position.y, placement.position.z),
|
|
2152
|
+
rotationY: placement.rotationY,
|
|
2153
|
+
scale: placement.scale,
|
|
1650
2154
|
},
|
|
1651
|
-
|
|
2155
|
+
null,
|
|
1652
2156
|
camera,
|
|
1653
2157
|
viewport,
|
|
1654
2158
|
triangles,
|
|
1655
|
-
|
|
2159
|
+
{
|
|
2160
|
+
accent: placement.accent,
|
|
2161
|
+
reflection: 0,
|
|
2162
|
+
surfaceType: "structure",
|
|
2163
|
+
}
|
|
1656
2164
|
);
|
|
1657
2165
|
}
|
|
1658
2166
|
}
|
|
1659
2167
|
|
|
1660
2168
|
function renderShipRigging(ctx, ship, camera, viewport) {
|
|
1661
2169
|
const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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));
|
|
1672
2203
|
if (projected.some((value) => value === null)) {
|
|
1673
2204
|
return;
|
|
1674
2205
|
}
|
|
1675
2206
|
|
|
1676
|
-
ctx.strokeStyle =
|
|
1677
|
-
ctx.lineWidth = 3.5;
|
|
2207
|
+
ctx.strokeStyle = layout.lineColor;
|
|
2208
|
+
ctx.lineWidth = ship.modelKey === "cutter" ? 2.2 : 3.5;
|
|
1678
2209
|
ctx.beginPath();
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
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
|
+
}
|
|
1683
2214
|
ctx.stroke();
|
|
1684
2215
|
|
|
1685
|
-
|
|
2216
|
+
const [a, b, c] = layout.sailTriangle;
|
|
2217
|
+
ctx.fillStyle = layout.sailColor;
|
|
1686
2218
|
ctx.beginPath();
|
|
1687
|
-
ctx.moveTo(projected[
|
|
1688
|
-
ctx.lineTo(projected[
|
|
1689
|
-
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);
|
|
1690
2222
|
ctx.closePath();
|
|
1691
2223
|
ctx.fill();
|
|
1692
2224
|
}
|
|
@@ -1941,10 +2473,10 @@ function resolveBoundaryCollision(ship, state, shipModel) {
|
|
|
1941
2473
|
}
|
|
1942
2474
|
}
|
|
1943
2475
|
|
|
1944
|
-
function resolveShipCollision(state, a, b,
|
|
2476
|
+
function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
|
|
1945
2477
|
const delta = subVec3(b.position, a.position);
|
|
1946
|
-
const radiusA = getShipCollisionRadius(a,
|
|
1947
|
-
const radiusB = getShipCollisionRadius(b,
|
|
2478
|
+
const radiusA = getShipCollisionRadius(a, shipModelA);
|
|
2479
|
+
const radiusB = getShipCollisionRadius(b, shipModelB);
|
|
1948
2480
|
const distance = Math.hypot(delta.x, delta.z);
|
|
1949
2481
|
const minDistance = radiusA + radiusB;
|
|
1950
2482
|
if (distance >= minDistance) {
|
|
@@ -1957,8 +2489,8 @@ function resolveShipCollision(state, a, b, shipModel) {
|
|
|
1957
2489
|
: normalizeVec3(vec3(Math.cos(state.time * 5.2), 0, Math.sin(state.time * 4.8)));
|
|
1958
2490
|
const tangent = vec3(-normal.z, 0, normal.x);
|
|
1959
2491
|
const penetration = minDistance - distance;
|
|
1960
|
-
const invMassA = getShipInverseMass(a,
|
|
1961
|
-
const invMassB = getShipInverseMass(b,
|
|
2492
|
+
const invMassA = getShipInverseMass(a, shipModelA);
|
|
2493
|
+
const invMassB = getShipInverseMass(b, shipModelB);
|
|
1962
2494
|
const invMassSum = invMassA + invMassB;
|
|
1963
2495
|
const correction = scaleVec3(normal, (penetration / Math.max(0.0001, invMassSum)) * 0.72);
|
|
1964
2496
|
a.position = subVec3(a.position, scaleVec3(correction, invMassA));
|
|
@@ -1966,7 +2498,11 @@ function resolveShipCollision(state, a, b, shipModel) {
|
|
|
1966
2498
|
|
|
1967
2499
|
const relativeVelocity = subVec3(b.velocity, a.velocity);
|
|
1968
2500
|
const velocityAlongNormal = dotVec3(relativeVelocity, normal);
|
|
1969
|
-
const restitution =
|
|
2501
|
+
const restitution =
|
|
2502
|
+
((readPhysicsNumber(shipModelA.physics, "restitution", 0.22) +
|
|
2503
|
+
readPhysicsNumber(shipModelB.physics, "restitution", 0.22)) /
|
|
2504
|
+
2) *
|
|
2505
|
+
0.88;
|
|
1970
2506
|
if (velocityAlongNormal < 0) {
|
|
1971
2507
|
const impulseMagnitude =
|
|
1972
2508
|
(-(1 + restitution) * velocityAlongNormal) / Math.max(0.0001, invMassSum);
|
|
@@ -1985,10 +2521,10 @@ function resolveShipCollision(state, a, b, shipModel) {
|
|
|
1985
2521
|
b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
|
|
1986
2522
|
|
|
1987
2523
|
a.angularVelocity -=
|
|
1988
|
-
tangentSpeed * radiusA * getShipInverseInertia(a,
|
|
2524
|
+
tangentSpeed * radiusA * getShipInverseInertia(a, shipModelA) * 0.2 +
|
|
1989
2525
|
impulseMagnitude * 0.00024;
|
|
1990
2526
|
b.angularVelocity +=
|
|
1991
|
-
tangentSpeed * radiusB * getShipInverseInertia(b,
|
|
2527
|
+
tangentSpeed * radiusB * getShipInverseInertia(b, shipModelB) * 0.2 +
|
|
1992
2528
|
impulseMagnitude * 0.00024;
|
|
1993
2529
|
|
|
1994
2530
|
const impactSpeed = Math.abs(velocityAlongNormal);
|
|
@@ -2028,14 +2564,19 @@ function updateShips(state, dt, shipModel) {
|
|
|
2028
2564
|
state.contactCount = 0;
|
|
2029
2565
|
|
|
2030
2566
|
for (const ship of state.ships) {
|
|
2031
|
-
|
|
2032
|
-
|
|
2567
|
+
const activeShipModel = resolveShipModel(state, ship, shipModel);
|
|
2568
|
+
updateShipMotion(state, ship, dt, activeShipModel);
|
|
2569
|
+
resolveBoundaryCollision(ship, state, activeShipModel);
|
|
2033
2570
|
}
|
|
2034
2571
|
|
|
2035
2572
|
for (let index = 0; index < state.ships.length; index += 1) {
|
|
2036
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);
|
|
2037
2578
|
collided =
|
|
2038
|
-
resolveShipCollision(state,
|
|
2579
|
+
resolveShipCollision(state, shipA, shipB, shipModelA, shipModelB) ||
|
|
2039
2580
|
collided;
|
|
2040
2581
|
}
|
|
2041
2582
|
}
|
|
@@ -2327,6 +2868,108 @@ function renderWaterLightReflection(ctx, source, state, camera, viewport) {
|
|
|
2327
2868
|
ctx.restore();
|
|
2328
2869
|
}
|
|
2329
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
|
+
|
|
2330
2973
|
function renderWaterMotionEffects(ctx, effects, camera, viewport) {
|
|
2331
2974
|
ctx.save();
|
|
2332
2975
|
ctx.globalCompositeOperation = "screen";
|
|
@@ -2459,6 +3102,15 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2459
3102
|
normal,
|
|
2460
3103
|
baseColor: bandMesh.color,
|
|
2461
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",
|
|
2462
3114
|
});
|
|
2463
3115
|
}
|
|
2464
3116
|
}
|
|
@@ -2466,7 +3118,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2466
3118
|
const waterMotionEffects = buildWaterMotionEffects(state);
|
|
2467
3119
|
const lightSources = collectSceneLightSources(state, visuals);
|
|
2468
3120
|
|
|
2469
|
-
pushHarborGeometry(camera, viewport, sceneTriangles,
|
|
3121
|
+
pushHarborGeometry(camera, viewport, sceneTriangles, state);
|
|
2470
3122
|
const cloth = buildClothSurface(
|
|
2471
3123
|
state,
|
|
2472
3124
|
state,
|
|
@@ -2489,24 +3141,47 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2489
3141
|
normal,
|
|
2490
3142
|
baseColor: cloth.color,
|
|
2491
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",
|
|
2492
3153
|
});
|
|
2493
3154
|
}
|
|
2494
3155
|
|
|
2495
3156
|
for (const ship of state.ships) {
|
|
3157
|
+
const activeShipModel = resolveShipModel(state, ship, shipModel);
|
|
2496
3158
|
buildTrianglesFromMesh(
|
|
2497
|
-
|
|
3159
|
+
activeShipModel,
|
|
2498
3160
|
{ position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
|
|
2499
3161
|
ship.tint,
|
|
2500
3162
|
camera,
|
|
2501
3163
|
viewport,
|
|
2502
3164
|
sceneTriangles,
|
|
2503
|
-
|
|
3165
|
+
{
|
|
3166
|
+
accent: nearLighting.rtParticipation.directShadows === "premium" ? 0.08 : 0.02,
|
|
3167
|
+
reflection: 0,
|
|
3168
|
+
surfaceType: "ship",
|
|
3169
|
+
}
|
|
2504
3170
|
);
|
|
2505
3171
|
}
|
|
2506
3172
|
|
|
2507
3173
|
drawTriangles(ctx, waterTriangles, lightDir, reflectionStrength, camera, shadowStrength);
|
|
2508
3174
|
for (const ship of state.ships) {
|
|
2509
|
-
renderShipShadow(
|
|
3175
|
+
renderShipShadow(
|
|
3176
|
+
ctx,
|
|
3177
|
+
resolveShipModel(state, ship, shipModel),
|
|
3178
|
+
ship,
|
|
3179
|
+
state,
|
|
3180
|
+
camera,
|
|
3181
|
+
viewport,
|
|
3182
|
+
lightDir,
|
|
3183
|
+
shadowStrength
|
|
3184
|
+
);
|
|
2510
3185
|
}
|
|
2511
3186
|
renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
|
|
2512
3187
|
for (const source of lightSources.reflectionLights) {
|
|
@@ -2514,9 +3189,18 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2514
3189
|
}
|
|
2515
3190
|
renderWaterMotionEffects(ctx, waterMotionEffects, camera, viewport);
|
|
2516
3191
|
renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
|
|
2517
|
-
drawTriangles(
|
|
3192
|
+
drawTriangles(
|
|
3193
|
+
ctx,
|
|
3194
|
+
sceneTriangles,
|
|
3195
|
+
lightDir,
|
|
3196
|
+
reflectionStrength,
|
|
3197
|
+
camera,
|
|
3198
|
+
shadowStrength,
|
|
3199
|
+
lightSources.directLights
|
|
3200
|
+
);
|
|
2518
3201
|
renderFlagPole(ctx, camera, viewport);
|
|
2519
3202
|
renderClothAccent(ctx, cloth, camera, viewport);
|
|
3203
|
+
renderLighthouseBeam(ctx, state, camera, viewport, visuals);
|
|
2520
3204
|
for (const source of lightSources.directLights) {
|
|
2521
3205
|
renderDirectLightGlow(ctx, source, camera, viewport);
|
|
2522
3206
|
}
|
|
@@ -2524,6 +3208,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2524
3208
|
renderShipRigging(ctx, ship, camera, viewport);
|
|
2525
3209
|
}
|
|
2526
3210
|
renderSprays(ctx, state.sprays, camera, viewport);
|
|
3211
|
+
renderAtmosphericGrade(ctx, canvas, state, visuals);
|
|
2527
3212
|
|
|
2528
3213
|
const debugSnapshot = state.debugSession.getSnapshot();
|
|
2529
3214
|
const quality = {
|
|
@@ -2534,11 +3219,11 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2534
3219
|
|
|
2535
3220
|
const sceneMetrics = [
|
|
2536
3221
|
`focus: ${state.focus}`,
|
|
2537
|
-
`ships: ${state.ships.length} active GLTF hulls`,
|
|
2538
|
-
`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`,
|
|
2539
3224
|
`physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
|
|
2540
3225
|
`physics contacts: ${state.contactCount}`,
|
|
2541
|
-
`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(" · ")}`,
|
|
2542
3227
|
`cloth band: ${cloth.band} -> ${cloth.representation.output}`,
|
|
2543
3228
|
`fluid near band: ${water.bandMeshes[0].representation.output}`,
|
|
2544
3229
|
`lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`,
|
|
@@ -2561,12 +3246,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2561
3246
|
];
|
|
2562
3247
|
const sceneNotes =
|
|
2563
3248
|
state.focus === "physics"
|
|
2564
|
-
?
|
|
2565
|
-
|
|
2566
|
-
"The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
|
|
2567
|
-
"Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water.",
|
|
2568
|
-
]
|
|
2569
|
-
: SCENE_NOTES;
|
|
3249
|
+
? PHYSICS_SCENE_NOTE_KEYS.map((key) => state.translate(key))
|
|
3250
|
+
: SCENE_NOTE_KEYS.map((key) => state.translate(key));
|
|
2570
3251
|
const custom = state.demoDescription ?? null;
|
|
2571
3252
|
|
|
2572
3253
|
setListContent(
|
|
@@ -2586,13 +3267,23 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
2586
3267
|
dom.status.textContent =
|
|
2587
3268
|
typeof custom?.status === "string"
|
|
2588
3269
|
? custom.status
|
|
2589
|
-
:
|
|
3270
|
+
: state.translate(gpuSharedTranslationKeys.statusLive, {
|
|
3271
|
+
fps: state.lastDecision.metrics.fps.toFixed(1),
|
|
3272
|
+
});
|
|
2590
3273
|
dom.details.textContent =
|
|
2591
3274
|
typeof custom?.details === "string"
|
|
2592
3275
|
? custom.details
|
|
2593
3276
|
: state.focus === "physics"
|
|
2594
|
-
?
|
|
2595
|
-
|
|
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
|
+
});
|
|
2596
3287
|
}
|
|
2597
3288
|
|
|
2598
3289
|
function updateSceneState(state, dt, shipModel) {
|
|
@@ -2624,15 +3315,18 @@ function syncTextState(state, shipModel) {
|
|
|
2624
3315
|
stress: state.stress,
|
|
2625
3316
|
ships: state.ships.map((ship) => ({
|
|
2626
3317
|
id: ship.id,
|
|
3318
|
+
modelKey: ship.modelKey ?? "brigantine",
|
|
2627
3319
|
x: Number(ship.position.x.toFixed(2)),
|
|
2628
3320
|
y: Number(ship.position.y.toFixed(2)),
|
|
2629
3321
|
z: Number(ship.position.z.toFixed(2)),
|
|
2630
3322
|
vx: Number(ship.velocity.x.toFixed(2)),
|
|
2631
3323
|
vz: Number(ship.velocity.z.toFixed(2)),
|
|
2632
|
-
massKg: Math.round(getShipMass(ship, shipModel)),
|
|
3324
|
+
massKg: Math.round(getShipMass(ship, resolveShipModel(state, ship, shipModel))),
|
|
2633
3325
|
lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0,
|
|
2634
3326
|
})),
|
|
2635
|
-
shipPhysics:
|
|
3327
|
+
shipPhysics: Object.fromEntries(
|
|
3328
|
+
state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
|
|
3329
|
+
),
|
|
2636
3330
|
sprays: state.sprays.length,
|
|
2637
3331
|
waveImpulses: state.waveImpulses.length,
|
|
2638
3332
|
pressure: state.lastDecision?.pressureLevel ?? "stable",
|
|
@@ -2656,23 +3350,39 @@ function syncTextState(state, shipModel) {
|
|
|
2656
3350
|
};
|
|
2657
3351
|
}
|
|
2658
3352
|
|
|
2659
|
-
export async function mountGpuShowcase(options = {}) {
|
|
3353
|
+
export async function mountGpuShowcase(options = {}, featureFlags = null) {
|
|
2660
3354
|
injectStyles();
|
|
2661
3355
|
const root = options.root ?? document.body;
|
|
2662
3356
|
root.classList?.add?.(ROOT_CLASS);
|
|
3357
|
+
const captureSettings = resolveCaptureSettings(options);
|
|
3358
|
+
if (captureSettings.captureMode) {
|
|
3359
|
+
root.classList?.add?.(CAPTURE_CLASS);
|
|
3360
|
+
}
|
|
2663
3361
|
const previousMarkup = root.innerHTML;
|
|
2664
3362
|
const previousRenderGameToText = window.render_game_to_text;
|
|
2665
3363
|
const previousAdvanceTime = window.advanceTime;
|
|
2666
3364
|
const focus = options.focus ?? new URLSearchParams(window.location.search).get("focus") ?? "integrated";
|
|
3365
|
+
const translate = createGpuSharedTranslator(options.translate);
|
|
2667
3366
|
const dom = buildDemoDom(root, {
|
|
2668
3367
|
packageName: options.packageName ?? "@plasius/gpu-demo-viewer",
|
|
2669
|
-
title: options.title ??
|
|
2670
|
-
subtitle: options.subtitle ??
|
|
3368
|
+
title: options.title ?? translate(gpuSharedTranslationKeys.showcaseTitle),
|
|
3369
|
+
subtitle: options.subtitle ?? translate(gpuSharedTranslationKeys.showcaseSubtitle),
|
|
3370
|
+
translate,
|
|
2671
3371
|
});
|
|
2672
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];
|
|
2673
3384
|
|
|
2674
|
-
|
|
2675
|
-
const shipModel = await loadGltfModel(resolveShowcaseAssetUrl());
|
|
3385
|
+
state.assetCatalog = assetCatalog;
|
|
2676
3386
|
state.shipModel = shipModel;
|
|
2677
3387
|
state.packageState =
|
|
2678
3388
|
typeof options.createState === "function" ? options.createState() : undefined;
|
|
@@ -2685,6 +3395,8 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2685
3395
|
if (!ctx) {
|
|
2686
3396
|
throw new Error("2D canvas context is required for the shared showcase.");
|
|
2687
3397
|
}
|
|
3398
|
+
ctx.imageSmoothingEnabled = true;
|
|
3399
|
+
ctx.imageSmoothingQuality = "high";
|
|
2688
3400
|
let animationFrameId = 0;
|
|
2689
3401
|
let destroyed = false;
|
|
2690
3402
|
const renderFrame = (nowMs) => {
|
|
@@ -2706,6 +3418,7 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2706
3418
|
}
|
|
2707
3419
|
|
|
2708
3420
|
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
3421
|
+
resizeCanvasToDisplaySize(dom.canvas, state);
|
|
2709
3422
|
renderScene(ctx, dom.canvas, state, shipModel, dom);
|
|
2710
3423
|
syncTextState(state, shipModel);
|
|
2711
3424
|
animationFrameId = requestAnimationFrame(renderFrame);
|
|
@@ -2713,7 +3426,9 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2713
3426
|
|
|
2714
3427
|
const handlePauseClick = () => {
|
|
2715
3428
|
state.paused = !state.paused;
|
|
2716
|
-
dom.pauseButton.textContent = state.paused
|
|
3429
|
+
dom.pauseButton.textContent = state.paused
|
|
3430
|
+
? state.translate(gpuSharedTranslationKeys.resume)
|
|
3431
|
+
: state.translate(gpuSharedTranslationKeys.pause);
|
|
2717
3432
|
};
|
|
2718
3433
|
const handleStressChange = () => {
|
|
2719
3434
|
state.stress = dom.stressToggle.checked;
|
|
@@ -2750,6 +3465,7 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2750
3465
|
state.packageState = undefined;
|
|
2751
3466
|
}
|
|
2752
3467
|
root.classList?.remove?.(ROOT_CLASS);
|
|
3468
|
+
root.classList?.remove?.(CAPTURE_CLASS);
|
|
2753
3469
|
root.innerHTML = previousMarkup;
|
|
2754
3470
|
if (typeof previousRenderGameToText === "function") {
|
|
2755
3471
|
window.render_game_to_text = previousRenderGameToText;
|
|
@@ -2771,6 +3487,13 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
2771
3487
|
}
|
|
2772
3488
|
|
|
2773
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
|
+
|
|
2774
3497
|
state.physics.snapshot = createPhysicsWorldSnapshot({
|
|
2775
3498
|
frameId: `showcase-${state.frame}`,
|
|
2776
3499
|
tick: state.frame,
|
|
@@ -2787,6 +3510,7 @@ function updatePhysicsSnapshot(state, shipModel) {
|
|
|
2787
3510
|
contactCount: state.contactCount,
|
|
2788
3511
|
snapshotStageId: state.physics.plan.snapshotStageId,
|
|
2789
3512
|
rigidBodyShape: shipModel.physics.shape ?? "box",
|
|
3513
|
+
rigidBodyShapes,
|
|
2790
3514
|
},
|
|
2791
3515
|
});
|
|
2792
3516
|
}
|