@plasius/gpu-shared 0.1.3 → 0.1.6
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 +35 -1
- package/dist/chunk-OTCJ3VOK.js +35 -0
- package/dist/chunk-OTCJ3VOK.js.map +1 -0
- package/dist/{chunk-UUJLYYQS.js → chunk-QBMXJ3V2.js} +42 -15
- package/dist/chunk-QBMXJ3V2.js.map +1 -0
- package/dist/{gltf-loader-4FNTT63R.js → gltf-loader-LKALCZAV.js} +2 -2
- package/dist/index.cjs +605 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/{showcase-runtime-V72XV55N.js → showcase-runtime-JZIYGQAU.js} +533 -167
- package/dist/{showcase-runtime-V72XV55N.js.map → showcase-runtime-JZIYGQAU.js.map} +1 -1
- package/package.json +2 -2
- package/src/asset-url.js +37 -0
- package/src/gltf-loader.js +36 -1
- package/src/index.d.ts +4 -0
- package/src/index.js +2 -4
- package/src/showcase-runtime.js +619 -166
- package/dist/chunk-UUJLYYQS.js.map +0 -1
- /package/dist/{gltf-loader-4FNTT63R.js.map → gltf-loader-LKALCZAV.js.map} +0 -0
package/src/showcase-runtime.js
CHANGED
|
@@ -31,12 +31,21 @@ import {
|
|
|
31
31
|
getPhysicsWorkerManifest,
|
|
32
32
|
} from "@plasius/gpu-physics/browser";
|
|
33
33
|
|
|
34
|
+
import { resolveShowcaseAssetUrl } from "./asset-url.js";
|
|
34
35
|
import { loadGltfModel } from "./gltf-loader.js";
|
|
35
36
|
|
|
36
37
|
const STYLE_ID = "plasius-shared-3d-showcase-style";
|
|
38
|
+
const ROOT_CLASS = "plasius-showcase-root";
|
|
37
39
|
const DEFAULT_TITLE = "Flag by the Sea";
|
|
38
40
|
const DEFAULT_SUBTITLE =
|
|
39
41
|
"Shared 3D validation scene using GLTF ships, cloth, fluid continuity, adaptive performance, and telemetry.";
|
|
42
|
+
const SHIP_SCALE = 1.1;
|
|
43
|
+
const HARBOR_BOUNDS = Object.freeze({
|
|
44
|
+
minX: -11.2,
|
|
45
|
+
maxX: 11.2,
|
|
46
|
+
minZ: 1.8,
|
|
47
|
+
maxZ: 17.2,
|
|
48
|
+
});
|
|
40
49
|
const CAMERA_PRESETS = Object.freeze({
|
|
41
50
|
integrated: Object.freeze({ yaw: -0.55, pitch: 0.34, distance: 27, target: [0, 2.2, 0] }),
|
|
42
51
|
lighting: Object.freeze({ yaw: -0.28, pitch: 0.28, distance: 23, target: [0, 2.8, 0] }),
|
|
@@ -49,10 +58,10 @@ const CAMERA_PRESETS = Object.freeze({
|
|
|
49
58
|
export const showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
|
|
50
59
|
|
|
51
60
|
const SCENE_NOTES = Object.freeze([
|
|
52
|
-
"Ships are loaded from a GLTF asset and carry
|
|
53
|
-
"
|
|
54
|
-
"Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands.",
|
|
55
|
-
"Performance pressure reduces visual detail before authoritative collision motion is touched.",
|
|
61
|
+
"Ships are loaded from a GLTF asset and carry mass, damping, restitution, and hull extents from node extras.",
|
|
62
|
+
"Moonlight sets the cold ambient read while deck lanterns and harbor torches provide warm local contrast.",
|
|
63
|
+
"Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands even in the darker night palette.",
|
|
64
|
+
"Performance pressure reduces visual detail before mass-weighted authoritative collision motion is touched.",
|
|
56
65
|
]);
|
|
57
66
|
|
|
58
67
|
const UNIT_BOX_MESH = Object.freeze({
|
|
@@ -76,10 +85,18 @@ const UNIT_BOX_MESH = Object.freeze({
|
|
|
76
85
|
]),
|
|
77
86
|
});
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
88
|
+
const SHIP_LANTERNS = Object.freeze([
|
|
89
|
+
Object.freeze({ x: 0.94, y: 1.54, z: 2.52, glow: 1 }),
|
|
90
|
+
Object.freeze({ x: -0.9, y: 1.58, z: 2.44, glow: 0.92 }),
|
|
91
|
+
Object.freeze({ x: 0.62, y: 1.42, z: -2.18, glow: 0.88 }),
|
|
92
|
+
Object.freeze({ x: -0.58, y: 1.46, z: -2.04, glow: 0.84 }),
|
|
93
|
+
]);
|
|
82
94
|
|
|
95
|
+
const HARBOR_TORCHES = Object.freeze([
|
|
96
|
+
Object.freeze({ x: -5.2, y: 1.25, z: 1.36, glow: 1.1 }),
|
|
97
|
+
Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
|
|
98
|
+
Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 }),
|
|
99
|
+
]);
|
|
83
100
|
function injectStyles() {
|
|
84
101
|
if (document.getElementById(STYLE_ID)) {
|
|
85
102
|
return;
|
|
@@ -88,27 +105,27 @@ function injectStyles() {
|
|
|
88
105
|
const style = document.createElement("style");
|
|
89
106
|
style.id = STYLE_ID;
|
|
90
107
|
style.textContent = `
|
|
91
|
-
|
|
92
|
-
color-scheme:
|
|
93
|
-
--plasius-paper: #
|
|
94
|
-
--plasius-ink: #
|
|
95
|
-
--plasius-muted: #
|
|
96
|
-
--plasius-accent: #
|
|
97
|
-
--plasius-panel: rgba(
|
|
98
|
-
--plasius-border: rgba(
|
|
99
|
-
--plasius-shadow: 0
|
|
100
|
-
}
|
|
101
|
-
* {
|
|
102
|
-
box-sizing: border-box;
|
|
103
|
-
}
|
|
104
|
-
body {
|
|
108
|
+
.${ROOT_CLASS} {
|
|
109
|
+
color-scheme: dark;
|
|
110
|
+
--plasius-paper: #081321;
|
|
111
|
+
--plasius-ink: #edf4ff;
|
|
112
|
+
--plasius-muted: #b6c5dd;
|
|
113
|
+
--plasius-accent: #f3b16a;
|
|
114
|
+
--plasius-panel: rgba(8, 19, 33, 0.72);
|
|
115
|
+
--plasius-border: rgba(159, 185, 223, 0.18);
|
|
116
|
+
--plasius-shadow: 0 24px 56px rgba(1, 6, 14, 0.44);
|
|
105
117
|
margin: 0;
|
|
106
|
-
min-height:
|
|
118
|
+
min-height: 100%;
|
|
107
119
|
font-family: "Fraunces", "Iowan Old Style", serif;
|
|
108
120
|
color: var(--plasius-ink);
|
|
109
121
|
background:
|
|
110
|
-
radial-gradient(circle at
|
|
111
|
-
|
|
122
|
+
radial-gradient(circle at 18% 12%, rgba(73, 101, 170, 0.28), transparent 30%),
|
|
123
|
+
radial-gradient(circle at 82% 18%, rgba(240, 188, 103, 0.08), transparent 18%),
|
|
124
|
+
linear-gradient(180deg, #04101d 0%, #0b1930 42%, #081321 100%);
|
|
125
|
+
}
|
|
126
|
+
.${ROOT_CLASS},
|
|
127
|
+
.${ROOT_CLASS} * {
|
|
128
|
+
box-sizing: border-box;
|
|
112
129
|
}
|
|
113
130
|
.plasius-demo {
|
|
114
131
|
width: min(1560px, calc(100vw - 32px));
|
|
@@ -142,7 +159,7 @@ function injectStyles() {
|
|
|
142
159
|
text-transform: uppercase;
|
|
143
160
|
letter-spacing: 0.18em;
|
|
144
161
|
font-size: 12px;
|
|
145
|
-
color: rgba(
|
|
162
|
+
color: rgba(226, 236, 255, 0.58);
|
|
146
163
|
}
|
|
147
164
|
.plasius-demo h1,
|
|
148
165
|
.plasius-demo h2,
|
|
@@ -160,7 +177,7 @@ function injectStyles() {
|
|
|
160
177
|
margin: 0;
|
|
161
178
|
padding: 8px 12px;
|
|
162
179
|
border-radius: 999px;
|
|
163
|
-
background: rgba(
|
|
180
|
+
background: rgba(243, 177, 106, 0.14);
|
|
164
181
|
color: var(--plasius-accent);
|
|
165
182
|
font-weight: 700;
|
|
166
183
|
}
|
|
@@ -182,8 +199,8 @@ function injectStyles() {
|
|
|
182
199
|
aspect-ratio: 16 / 9;
|
|
183
200
|
display: block;
|
|
184
201
|
border-radius: 20px;
|
|
185
|
-
border: 1px solid rgba(
|
|
186
|
-
background: linear-gradient(180deg, #
|
|
202
|
+
border: 1px solid rgba(159, 185, 223, 0.12);
|
|
203
|
+
background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
|
|
187
204
|
}
|
|
188
205
|
.plasius-demo__toolbar {
|
|
189
206
|
position: absolute;
|
|
@@ -203,9 +220,9 @@ function injectStyles() {
|
|
|
203
220
|
.plasius-demo button,
|
|
204
221
|
.plasius-demo .plasius-toggle,
|
|
205
222
|
.plasius-demo select {
|
|
206
|
-
border: 1px solid rgba(
|
|
223
|
+
border: 1px solid rgba(159, 185, 223, 0.18);
|
|
207
224
|
border-radius: 999px;
|
|
208
|
-
background: rgba(
|
|
225
|
+
background: rgba(9, 20, 34, 0.84);
|
|
209
226
|
color: var(--plasius-ink);
|
|
210
227
|
padding: 10px 14px;
|
|
211
228
|
}
|
|
@@ -244,8 +261,8 @@ function injectStyles() {
|
|
|
244
261
|
bottom: 24px;
|
|
245
262
|
padding: 10px 14px;
|
|
246
263
|
border-radius: 16px;
|
|
247
|
-
background: rgba(
|
|
248
|
-
border: 1px solid rgba(
|
|
264
|
+
background: rgba(9, 20, 34, 0.82);
|
|
265
|
+
border: 1px solid rgba(159, 185, 223, 0.16);
|
|
249
266
|
color: var(--plasius-muted);
|
|
250
267
|
font-size: 12px;
|
|
251
268
|
line-height: 1.45;
|
|
@@ -257,7 +274,7 @@ function injectStyles() {
|
|
|
257
274
|
}
|
|
258
275
|
.plasius-demo__footer {
|
|
259
276
|
margin-top: 4px;
|
|
260
|
-
color: rgba(
|
|
277
|
+
color: rgba(226, 236, 255, 0.68);
|
|
261
278
|
font-size: 13px;
|
|
262
279
|
line-height: 1.6;
|
|
263
280
|
}
|
|
@@ -279,6 +296,16 @@ function mix(a, b, t) {
|
|
|
279
296
|
return a + (b - a) * t;
|
|
280
297
|
}
|
|
281
298
|
|
|
299
|
+
function smoothstep(min, max, value) {
|
|
300
|
+
const t = clamp((value - min) / Math.max(0.0001, max - min), 0, 1);
|
|
301
|
+
return t * t * (3 - 2 * t);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function pseudoRandom(seed) {
|
|
305
|
+
const value = Math.sin(seed * 12.9898 + seed * seed * 0.0017) * 43758.5453;
|
|
306
|
+
return value - Math.floor(value);
|
|
307
|
+
}
|
|
308
|
+
|
|
282
309
|
function vec3(x = 0, y = 0, z = 0) {
|
|
283
310
|
return { x, y, z };
|
|
284
311
|
}
|
|
@@ -321,6 +348,14 @@ function reflectVec3(vector, normal) {
|
|
|
321
348
|
return subVec3(vector, scaleVec3(unitNormal, 2 * dotVec3(vector, unitNormal)));
|
|
322
349
|
}
|
|
323
350
|
|
|
351
|
+
function directionFromYaw(yaw) {
|
|
352
|
+
return normalizeVec3(vec3(Math.sin(yaw), 0, Math.cos(yaw)));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function perpendicularOnWater(direction) {
|
|
356
|
+
return vec3(-direction.z, 0, direction.x);
|
|
357
|
+
}
|
|
358
|
+
|
|
324
359
|
function rotateY(point, angle) {
|
|
325
360
|
const cosine = Math.cos(angle);
|
|
326
361
|
const sine = Math.sin(angle);
|
|
@@ -525,7 +560,7 @@ function buildDemoDom(root, options) {
|
|
|
525
560
|
<section class="plasius-panel plasius-demo__status">
|
|
526
561
|
<p id="demoStatus" class="plasius-demo__status-badge">Booting 3D scene…</p>
|
|
527
562
|
<p id="demoDetails" class="plasius-demo__status-text">
|
|
528
|
-
Preparing GLTF
|
|
563
|
+
Preparing a moonlit harbor scene, GLTF hull data, cloth and fluid continuity plans, and adaptive quality metadata.
|
|
529
564
|
</p>
|
|
530
565
|
</section>
|
|
531
566
|
</section>
|
|
@@ -553,9 +588,9 @@ function buildDemoDom(root, options) {
|
|
|
553
588
|
</div>
|
|
554
589
|
<div class="plasius-demo__legend">
|
|
555
590
|
<strong>Scene</strong>
|
|
556
|
-
GLTF ships carry
|
|
557
|
-
|
|
558
|
-
|
|
591
|
+
GLTF ships carry hull mass and damping metadata.<br />
|
|
592
|
+
Lanterns and torches warm the moonlit harbor.<br />
|
|
593
|
+
Mass-aware collisions stay authoritative near the camera.
|
|
559
594
|
</div>
|
|
560
595
|
</section>
|
|
561
596
|
<aside class="plasius-demo__sidebar">
|
|
@@ -638,6 +673,7 @@ function buildSceneSnapshot(state, shipModel) {
|
|
|
638
673
|
})
|
|
639
674
|
)
|
|
640
675
|
),
|
|
676
|
+
shipPhysics: shipModel?.physics ?? null,
|
|
641
677
|
physics: Object.freeze({
|
|
642
678
|
profile: state.physics.profile,
|
|
643
679
|
plan: state.physics.plan,
|
|
@@ -689,28 +725,39 @@ function readVisualNumber(value, fallback) {
|
|
|
689
725
|
function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
|
|
690
726
|
const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
|
|
691
727
|
const defaults = {
|
|
692
|
-
skyTop: premiumShadows ? "#
|
|
693
|
-
skyMid: premiumShadows ? "#
|
|
694
|
-
skyBottom: premiumShadows ? "#
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
728
|
+
skyTop: premiumShadows ? "#040c1a" : "#06101f",
|
|
729
|
+
skyMid: premiumShadows ? "#11203b" : "#152643",
|
|
730
|
+
skyBottom: premiumShadows ? "#2f4468" : "#364d73",
|
|
731
|
+
duskGlow: premiumShadows ? "rgba(116, 142, 201, 0.26)" : "rgba(104, 128, 188, 0.22)",
|
|
732
|
+
seaTop: premiumShadows ? "#102946" : "#153050",
|
|
733
|
+
seaMid: premiumShadows ? "#0a1d33" : "#0d2138",
|
|
734
|
+
seaBottom: "#04101d",
|
|
735
|
+
moonCore: "rgba(241, 246, 255, 0.98)",
|
|
736
|
+
moonHalo: "rgba(167, 191, 255, 0.24)",
|
|
737
|
+
moonReflection: "rgba(192, 214, 255, 0.22)",
|
|
738
|
+
starColor: "rgba(232, 239, 255, 0.82)",
|
|
739
|
+
ambientMist: "rgba(41, 63, 97, 0.16)",
|
|
699
740
|
reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
|
|
700
741
|
shadowAccent: lightingSnapshot.currentLevel.config.shadowStrength,
|
|
701
|
-
waveAmplitude:
|
|
702
|
-
waveDirection: { x: 0.
|
|
703
|
-
wavePhaseSpeed:
|
|
704
|
-
wakeStrength: 0.
|
|
705
|
-
wakeLength:
|
|
706
|
-
collisionRippleStrength: 0.
|
|
707
|
-
waterNear: { r: 0.
|
|
708
|
-
waterFar: { r: 0.
|
|
709
|
-
harborWall: { r: 0.
|
|
710
|
-
harborDeck: { r: 0.
|
|
711
|
-
harborTower: { r: 0.
|
|
712
|
-
flagColor: { r: 0.
|
|
713
|
-
flagMotion:
|
|
742
|
+
waveAmplitude: 0.94,
|
|
743
|
+
waveDirection: { x: 0.88, z: 0.28 },
|
|
744
|
+
wavePhaseSpeed: 0.88,
|
|
745
|
+
wakeStrength: 0.31,
|
|
746
|
+
wakeLength: 18,
|
|
747
|
+
collisionRippleStrength: 0.42,
|
|
748
|
+
waterNear: { r: 0.08, g: 0.23, b: 0.33 },
|
|
749
|
+
waterFar: { r: 0.18, g: 0.35, b: 0.49 },
|
|
750
|
+
harborWall: { r: 0.26, g: 0.24, b: 0.28 },
|
|
751
|
+
harborDeck: { r: 0.33, g: 0.22, b: 0.16 },
|
|
752
|
+
harborTower: { r: 0.23, g: 0.24, b: 0.29 },
|
|
753
|
+
flagColor: { r: 0.66, g: 0.16, b: 0.13 },
|
|
754
|
+
flagMotion: 0.92,
|
|
755
|
+
lanternCore: { r: 0.98, g: 0.8, b: 0.48 },
|
|
756
|
+
lanternGlow: { r: 1, g: 0.56, b: 0.2 },
|
|
757
|
+
lanternReflectionStrength: 0.42,
|
|
758
|
+
torchCore: { r: 0.99, g: 0.72, b: 0.36 },
|
|
759
|
+
torchGlow: { r: 0.98, g: 0.38, b: 0.15 },
|
|
760
|
+
collisionFlash: "rgba(255, 212, 168, 0.16)",
|
|
714
761
|
};
|
|
715
762
|
|
|
716
763
|
return {
|
|
@@ -722,8 +769,26 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
|
|
|
722
769
|
seaMid: typeof customVisuals.seaMid === "string" ? customVisuals.seaMid : defaults.seaMid,
|
|
723
770
|
seaBottom:
|
|
724
771
|
typeof customVisuals.seaBottom === "string" ? customVisuals.seaBottom : defaults.seaBottom,
|
|
725
|
-
|
|
726
|
-
typeof customVisuals.
|
|
772
|
+
duskGlow:
|
|
773
|
+
typeof customVisuals.duskGlow === "string" ? customVisuals.duskGlow : defaults.duskGlow,
|
|
774
|
+
moonCore:
|
|
775
|
+
typeof customVisuals.moonCore === "string"
|
|
776
|
+
? customVisuals.moonCore
|
|
777
|
+
: typeof customVisuals.sunCore === "string"
|
|
778
|
+
? customVisuals.sunCore
|
|
779
|
+
: defaults.moonCore,
|
|
780
|
+
moonHalo:
|
|
781
|
+
typeof customVisuals.moonHalo === "string" ? customVisuals.moonHalo : defaults.moonHalo,
|
|
782
|
+
moonReflection:
|
|
783
|
+
typeof customVisuals.moonReflection === "string"
|
|
784
|
+
? customVisuals.moonReflection
|
|
785
|
+
: defaults.moonReflection,
|
|
786
|
+
starColor:
|
|
787
|
+
typeof customVisuals.starColor === "string" ? customVisuals.starColor : defaults.starColor,
|
|
788
|
+
ambientMist:
|
|
789
|
+
typeof customVisuals.ambientMist === "string"
|
|
790
|
+
? customVisuals.ambientMist
|
|
791
|
+
: defaults.ambientMist,
|
|
727
792
|
reflectionStrength: readVisualNumber(
|
|
728
793
|
customVisuals.reflectionStrength,
|
|
729
794
|
defaults.reflectionStrength
|
|
@@ -750,6 +815,18 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
|
|
|
750
815
|
harborTower: normalizeColorOverride(customVisuals.harborTower, defaults.harborTower),
|
|
751
816
|
flagColor: normalizeColorOverride(customVisuals.flagColor, defaults.flagColor),
|
|
752
817
|
flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion),
|
|
818
|
+
lanternCore: normalizeColorOverride(customVisuals.lanternCore, defaults.lanternCore),
|
|
819
|
+
lanternGlow: normalizeColorOverride(customVisuals.lanternGlow, defaults.lanternGlow),
|
|
820
|
+
lanternReflectionStrength: readVisualNumber(
|
|
821
|
+
customVisuals.lanternReflectionStrength,
|
|
822
|
+
defaults.lanternReflectionStrength
|
|
823
|
+
),
|
|
824
|
+
torchCore: normalizeColorOverride(customVisuals.torchCore, defaults.torchCore),
|
|
825
|
+
torchGlow: normalizeColorOverride(customVisuals.torchGlow, defaults.torchGlow),
|
|
826
|
+
collisionFlash:
|
|
827
|
+
typeof customVisuals.collisionFlash === "string"
|
|
828
|
+
? customVisuals.collisionFlash
|
|
829
|
+
: defaults.collisionFlash,
|
|
753
830
|
};
|
|
754
831
|
}
|
|
755
832
|
|
|
@@ -1055,18 +1132,34 @@ function createSceneState(options) {
|
|
|
1055
1132
|
{
|
|
1056
1133
|
id: "northwind",
|
|
1057
1134
|
position: vec3(-5.2, 0, 7.2),
|
|
1058
|
-
velocity: vec3(2.
|
|
1059
|
-
rotationY: 0.
|
|
1060
|
-
angularVelocity: 0.
|
|
1135
|
+
velocity: vec3(2.35, 0, -1.08),
|
|
1136
|
+
rotationY: 0.58,
|
|
1137
|
+
angularVelocity: 0.09,
|
|
1061
1138
|
tint: { r: 0.62, g: 0.39, b: 0.23 },
|
|
1139
|
+
massScale: 1.42,
|
|
1140
|
+
cruiseSpeed: 2.25,
|
|
1141
|
+
throttleResponse: 0.46,
|
|
1142
|
+
rudderResponse: 0.54,
|
|
1143
|
+
wanderPhase: 0.35,
|
|
1144
|
+
lanterns: SHIP_LANTERNS,
|
|
1145
|
+
lanternStrength: 1.06,
|
|
1146
|
+
collisionRadiusScale: 1.04,
|
|
1062
1147
|
},
|
|
1063
1148
|
{
|
|
1064
1149
|
id: "tidecaller",
|
|
1065
1150
|
position: vec3(4.8, 0, 4.4),
|
|
1066
|
-
velocity: vec3(-
|
|
1067
|
-
rotationY: -2.
|
|
1068
|
-
angularVelocity: -0.
|
|
1151
|
+
velocity: vec3(-2.15, 0, 1.74),
|
|
1152
|
+
rotationY: -2.48,
|
|
1153
|
+
angularVelocity: -0.2,
|
|
1069
1154
|
tint: { r: 0.48, g: 0.28, b: 0.19 },
|
|
1155
|
+
massScale: 0.84,
|
|
1156
|
+
cruiseSpeed: 2.68,
|
|
1157
|
+
throttleResponse: 0.7,
|
|
1158
|
+
rudderResponse: 0.78,
|
|
1159
|
+
wanderPhase: 1.6,
|
|
1160
|
+
lanterns: SHIP_LANTERNS,
|
|
1161
|
+
lanternStrength: 1.18,
|
|
1162
|
+
collisionRadiusScale: 0.94,
|
|
1070
1163
|
},
|
|
1071
1164
|
],
|
|
1072
1165
|
sprays: [],
|
|
@@ -1090,14 +1183,30 @@ function setListContent(element, values) {
|
|
|
1090
1183
|
}
|
|
1091
1184
|
|
|
1092
1185
|
function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength, visuals) {
|
|
1093
|
-
const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
|
|
1094
1186
|
const sky = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.5);
|
|
1095
1187
|
sky.addColorStop(0, visuals.skyTop);
|
|
1096
|
-
sky.addColorStop(0.
|
|
1188
|
+
sky.addColorStop(0.54, visuals.skyMid);
|
|
1097
1189
|
sky.addColorStop(1, visuals.skyBottom);
|
|
1098
1190
|
ctx.fillStyle = sky;
|
|
1099
1191
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
1100
1192
|
|
|
1193
|
+
for (let index = 0; index < 70; index += 1) {
|
|
1194
|
+
const x = pseudoRandom(index + 13) * canvas.width;
|
|
1195
|
+
const y = pseudoRandom(index * 7 + 5) * canvas.height * 0.42;
|
|
1196
|
+
const twinkle = 0.45 + Math.sin(state.time * 1.4 + index * 0.73) * 0.25;
|
|
1197
|
+
const radius = 0.6 + pseudoRandom(index * 11 + 2) * 1.9;
|
|
1198
|
+
ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${clamp(twinkle, 0.16, 0.92)})`);
|
|
1199
|
+
ctx.beginPath();
|
|
1200
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
1201
|
+
ctx.fill();
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const horizonGlow = ctx.createLinearGradient(0, canvas.height * 0.22, 0, canvas.height * 0.62);
|
|
1205
|
+
horizonGlow.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
1206
|
+
horizonGlow.addColorStop(1, visuals.duskGlow);
|
|
1207
|
+
ctx.fillStyle = horizonGlow;
|
|
1208
|
+
ctx.fillRect(0, canvas.height * 0.2, canvas.width, canvas.height * 0.45);
|
|
1209
|
+
|
|
1101
1210
|
const shoreline = ctx.createLinearGradient(0, canvas.height * 0.45, 0, canvas.height);
|
|
1102
1211
|
shoreline.addColorStop(0, visuals.seaTop);
|
|
1103
1212
|
shoreline.addColorStop(0.52, visuals.seaMid);
|
|
@@ -1105,30 +1214,48 @@ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, s
|
|
|
1105
1214
|
ctx.fillStyle = shoreline;
|
|
1106
1215
|
ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
|
|
1107
1216
|
|
|
1108
|
-
const
|
|
1109
|
-
const
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1217
|
+
const moonX = canvas.width * 0.76 + Math.sin(state.time * 0.045) * 18;
|
|
1218
|
+
const moonY = canvas.height * 0.17 + Math.cos(state.time * 0.05) * 10;
|
|
1219
|
+
const moon = ctx.createRadialGradient(moonX, moonY, 14, moonX, moonY, 126);
|
|
1220
|
+
moon.addColorStop(0, visuals.moonCore);
|
|
1221
|
+
moon.addColorStop(0.46, visuals.moonHalo);
|
|
1222
|
+
moon.addColorStop(1, "rgba(167, 191, 255, 0)");
|
|
1223
|
+
ctx.fillStyle = moon;
|
|
1224
|
+
ctx.beginPath();
|
|
1225
|
+
ctx.arc(moonX, moonY, 94, 0, Math.PI * 2);
|
|
1226
|
+
ctx.fill();
|
|
1227
|
+
|
|
1228
|
+
const moonCore = ctx.createRadialGradient(moonX, moonY, 4, moonX, moonY, 28);
|
|
1229
|
+
moonCore.addColorStop(0, "rgba(255, 255, 255, 0.98)");
|
|
1230
|
+
moonCore.addColorStop(1, visuals.moonCore);
|
|
1231
|
+
ctx.fillStyle = moonCore;
|
|
1114
1232
|
ctx.beginPath();
|
|
1115
|
-
ctx.arc(
|
|
1233
|
+
ctx.arc(moonX, moonY, 24, 0, Math.PI * 2);
|
|
1116
1234
|
ctx.fill();
|
|
1117
1235
|
|
|
1118
|
-
const track = ctx.createLinearGradient(
|
|
1119
|
-
track.addColorStop(0,
|
|
1120
|
-
track.addColorStop(0.42,
|
|
1121
|
-
track.addColorStop(1, "rgba(
|
|
1236
|
+
const track = ctx.createLinearGradient(moonX, canvas.height * 0.44, moonX, canvas.height * 0.98);
|
|
1237
|
+
track.addColorStop(0, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.08 + reflectionStrength * 0.12})`));
|
|
1238
|
+
track.addColorStop(0.42, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.04 + reflectionStrength * 0.18})`));
|
|
1239
|
+
track.addColorStop(1, "rgba(192, 214, 255, 0)");
|
|
1122
1240
|
ctx.save();
|
|
1123
1241
|
ctx.globalCompositeOperation = "screen";
|
|
1124
1242
|
ctx.fillStyle = track;
|
|
1125
1243
|
ctx.beginPath();
|
|
1126
|
-
ctx.ellipse(
|
|
1244
|
+
ctx.ellipse(moonX, canvas.height * 0.75, 38 + shadowStrength * 42, canvas.height * 0.24, 0, 0, Math.PI * 2);
|
|
1127
1245
|
ctx.fill();
|
|
1128
1246
|
ctx.restore();
|
|
1129
1247
|
|
|
1248
|
+
const mist = ctx.createLinearGradient(0, canvas.height * 0.5, 0, canvas.height);
|
|
1249
|
+
mist.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
1250
|
+
mist.addColorStop(1, visuals.ambientMist);
|
|
1251
|
+
ctx.fillStyle = mist;
|
|
1252
|
+
ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
|
|
1253
|
+
|
|
1130
1254
|
if (state.collisionFlash > 0.01) {
|
|
1131
|
-
ctx.fillStyle =
|
|
1255
|
+
ctx.fillStyle = visuals.collisionFlash.replace(
|
|
1256
|
+
/[\d.]+\)$/u,
|
|
1257
|
+
`${clamp(state.collisionFlash * 0.22, 0, 0.26)})`
|
|
1258
|
+
);
|
|
1132
1259
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
1133
1260
|
}
|
|
1134
1261
|
}
|
|
@@ -1238,7 +1365,7 @@ function pushHarborGeometry(camera, viewport, triangles, visuals) {
|
|
|
1238
1365
|
}
|
|
1239
1366
|
|
|
1240
1367
|
function renderShipRigging(ctx, ship, camera, viewport) {
|
|
1241
|
-
const transform = { position: ship.position, rotationY: ship.rotationY, scale:
|
|
1368
|
+
const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
|
|
1242
1369
|
const mastBase = transformPoint(vec3(0, 0.38, -0.4), transform);
|
|
1243
1370
|
const mastTop = transformPoint(vec3(0, 3.8, -0.2), transform);
|
|
1244
1371
|
const aftBase = transformPoint(vec3(-0.25, 0.32, -1.9), transform);
|
|
@@ -1353,6 +1480,43 @@ function renderWaterHighlights(ctx, waterBands, camera, viewport) {
|
|
|
1353
1480
|
}
|
|
1354
1481
|
}
|
|
1355
1482
|
|
|
1483
|
+
function readPhysicsNumber(physics, key, fallback) {
|
|
1484
|
+
const value = physics?.[key];
|
|
1485
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function getShipMass(ship, shipModel) {
|
|
1489
|
+
const baseMass = readPhysicsNumber(shipModel.physics, "mass", 3200);
|
|
1490
|
+
return baseMass * readVisualNumber(ship.massScale, 1);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function getShipHalfExtents(ship, shipModel) {
|
|
1494
|
+
const physicsHalfExtents = Array.isArray(shipModel.physics.halfExtents)
|
|
1495
|
+
? shipModel.physics.halfExtents
|
|
1496
|
+
: [1.35, 0.95, 3.9];
|
|
1497
|
+
const scale = SHIP_SCALE * readVisualNumber(ship.collisionRadiusScale, 1);
|
|
1498
|
+
return {
|
|
1499
|
+
x: physicsHalfExtents[0] * scale,
|
|
1500
|
+
y: physicsHalfExtents[1] * scale,
|
|
1501
|
+
z: physicsHalfExtents[2] * scale,
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function getShipCollisionRadius(ship, shipModel) {
|
|
1506
|
+
const halfExtents = getShipHalfExtents(ship, shipModel);
|
|
1507
|
+
return Math.max(halfExtents.x * 1.08, halfExtents.z * 0.62);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function getShipInverseMass(ship, shipModel) {
|
|
1511
|
+
return 1 / Math.max(1, getShipMass(ship, shipModel));
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function getShipInverseInertia(ship, shipModel) {
|
|
1515
|
+
const radius = getShipCollisionRadius(ship, shipModel);
|
|
1516
|
+
const inertia = getShipMass(ship, shipModel) * radius * radius * 0.72;
|
|
1517
|
+
return 1 / Math.max(1, inertia);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1356
1520
|
function spawnSpray(state, point, intensity) {
|
|
1357
1521
|
const count = state.fluidDetail.getSnapshot().currentLevel.config.splashCount;
|
|
1358
1522
|
for (let index = 0; index < count; index += 1) {
|
|
@@ -1366,58 +1530,226 @@ function spawnSpray(state, point, intensity) {
|
|
|
1366
1530
|
}
|
|
1367
1531
|
}
|
|
1368
1532
|
|
|
1369
|
-
function
|
|
1533
|
+
function resolveShipRoute(ship, state, radius) {
|
|
1534
|
+
if (typeof ship.routeDirection !== "number") {
|
|
1535
|
+
ship.routeDirection = ship.velocity.x >= 0 ? 1 : -1;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (ship.position.x > HARBOR_BOUNDS.maxX - radius * 1.1) {
|
|
1539
|
+
ship.routeDirection = -1;
|
|
1540
|
+
} else if (ship.position.x < HARBOR_BOUNDS.minX + radius * 1.1) {
|
|
1541
|
+
ship.routeDirection = 1;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const wander = Math.sin(state.time * 0.22 + readVisualNumber(ship.wanderPhase, 0));
|
|
1545
|
+
const crossCurrent = Math.cos(state.time * 0.31 + readVisualNumber(ship.wanderPhase, 0));
|
|
1546
|
+
const laneCenter =
|
|
1547
|
+
ship.id === "northwind"
|
|
1548
|
+
? 10.2 + wander * 2.1 + crossCurrent * 0.6
|
|
1549
|
+
: 7 + wander * 3.3 - crossCurrent * 1.1;
|
|
1550
|
+
const targetX =
|
|
1551
|
+
ship.routeDirection > 0
|
|
1552
|
+
? HARBOR_BOUNDS.maxX - radius * 1.7
|
|
1553
|
+
: HARBOR_BOUNDS.minX + radius * 1.7;
|
|
1554
|
+
return vec3(targetX, 0, clamp(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function updateShipMotion(state, ship, dt, shipModel) {
|
|
1370
1558
|
const physics = shipModel.physics;
|
|
1371
|
-
const
|
|
1559
|
+
const massScale = Math.max(0.55, readVisualNumber(ship.massScale, 1));
|
|
1560
|
+
const radius = getShipCollisionRadius(ship, shipModel);
|
|
1561
|
+
const waterline = readPhysicsNumber(physics, "waterline", 0.42);
|
|
1562
|
+
const linearDamping = readPhysicsNumber(physics, "linearDamping", 0.04);
|
|
1563
|
+
const angularDamping = readPhysicsNumber(physics, "angularDamping", 0.08);
|
|
1564
|
+
const throttleResponse = readVisualNumber(ship.throttleResponse, 0.58);
|
|
1565
|
+
const rudderResponse = readVisualNumber(ship.rudderResponse, 0.62);
|
|
1566
|
+
const cruiseSpeed = readVisualNumber(ship.cruiseSpeed, 2.4);
|
|
1567
|
+
|
|
1568
|
+
ship.collisionCooldown = Math.max(0, readVisualNumber(ship.collisionCooldown, 0) - dt);
|
|
1569
|
+
|
|
1570
|
+
const forward = directionFromYaw(ship.rotationY);
|
|
1571
|
+
const lateral = perpendicularOnWater(forward);
|
|
1572
|
+
const routeTarget = resolveShipRoute(ship, state, radius);
|
|
1573
|
+
const desiredHeading = Math.atan2(routeTarget.x - ship.position.x, routeTarget.z - ship.position.z);
|
|
1574
|
+
const headingError = Math.atan2(
|
|
1575
|
+
Math.sin(desiredHeading - ship.rotationY),
|
|
1576
|
+
Math.cos(desiredHeading - ship.rotationY)
|
|
1577
|
+
);
|
|
1578
|
+
ship.angularVelocity +=
|
|
1579
|
+
headingError * rudderResponse * dt * (1.18 / Math.sqrt(massScale)) +
|
|
1580
|
+
Math.sin(state.time * 0.9 + readVisualNumber(ship.wanderPhase, 0)) * dt * 0.04;
|
|
1581
|
+
|
|
1582
|
+
const waveDirection = resolveWaveDirection(state);
|
|
1583
|
+
const forwardSpeed = dotVec3(ship.velocity, forward);
|
|
1584
|
+
const lateralSpeed = dotVec3(ship.velocity, lateral);
|
|
1585
|
+
const thrust = (cruiseSpeed - forwardSpeed) * throttleResponse;
|
|
1586
|
+
const currentDrift = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.016;
|
|
1587
|
+
const acceleration = addVec3(
|
|
1588
|
+
scaleVec3(forward, thrust),
|
|
1589
|
+
addVec3(
|
|
1590
|
+
scaleVec3(lateral, -lateralSpeed * (1.28 + rudderResponse * 0.4)),
|
|
1591
|
+
scaleVec3(waveDirection, currentDrift / Math.sqrt(massScale))
|
|
1592
|
+
)
|
|
1593
|
+
);
|
|
1594
|
+
|
|
1595
|
+
ship.velocity = addVec3(ship.velocity, scaleVec3(acceleration, dt));
|
|
1596
|
+
ship.velocity = scaleVec3(
|
|
1597
|
+
ship.velocity,
|
|
1598
|
+
Math.max(0, 1 - (linearDamping / Math.pow(massScale, 0.22)) * dt)
|
|
1599
|
+
);
|
|
1600
|
+
ship.angularVelocity *= Math.max(
|
|
1601
|
+
0,
|
|
1602
|
+
1 - (angularDamping / Math.pow(massScale, 0.15)) * dt
|
|
1603
|
+
);
|
|
1604
|
+
ship.rotationY += ship.angularVelocity * dt;
|
|
1605
|
+
ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
|
|
1606
|
+
ship.position.y =
|
|
1607
|
+
sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 + waterline;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function resolveBoundaryCollision(ship, state, shipModel) {
|
|
1611
|
+
const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.56;
|
|
1612
|
+
const radius = getShipCollisionRadius(ship, shipModel);
|
|
1613
|
+
const boundaries = [
|
|
1614
|
+
{ axis: "x", min: HARBOR_BOUNDS.minX + radius, max: HARBOR_BOUNDS.maxX - radius, normalMin: vec3(1, 0, 0), normalMax: vec3(-1, 0, 0) },
|
|
1615
|
+
{ axis: "z", min: HARBOR_BOUNDS.minZ + radius, max: HARBOR_BOUNDS.maxZ - radius, normalMin: vec3(0, 0, 1), normalMax: vec3(0, 0, -1) },
|
|
1616
|
+
];
|
|
1617
|
+
|
|
1618
|
+
for (const boundary of boundaries) {
|
|
1619
|
+
if (ship.position[boundary.axis] < boundary.min) {
|
|
1620
|
+
ship.position[boundary.axis] = boundary.min;
|
|
1621
|
+
const normal = boundary.normalMin;
|
|
1622
|
+
const speedIntoWall = dotVec3(ship.velocity, normal);
|
|
1623
|
+
if (speedIntoWall < 0) {
|
|
1624
|
+
ship.velocity = subVec3(
|
|
1625
|
+
ship.velocity,
|
|
1626
|
+
scaleVec3(normal, (1 + restitution) * speedIntoWall)
|
|
1627
|
+
);
|
|
1628
|
+
const tangent = vec3(-normal.z, 0, normal.x);
|
|
1629
|
+
const tangentSpeed = dotVec3(ship.velocity, tangent);
|
|
1630
|
+
ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
|
|
1631
|
+
ship.angularVelocity += tangentSpeed * 0.004;
|
|
1632
|
+
}
|
|
1633
|
+
} else if (ship.position[boundary.axis] > boundary.max) {
|
|
1634
|
+
ship.position[boundary.axis] = boundary.max;
|
|
1635
|
+
const normal = boundary.normalMax;
|
|
1636
|
+
const speedIntoWall = dotVec3(ship.velocity, normal);
|
|
1637
|
+
if (speedIntoWall < 0) {
|
|
1638
|
+
ship.velocity = subVec3(
|
|
1639
|
+
ship.velocity,
|
|
1640
|
+
scaleVec3(normal, (1 + restitution) * speedIntoWall)
|
|
1641
|
+
);
|
|
1642
|
+
const tangent = vec3(-normal.z, 0, normal.x);
|
|
1643
|
+
const tangentSpeed = dotVec3(ship.velocity, tangent);
|
|
1644
|
+
ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
|
|
1645
|
+
ship.angularVelocity += tangentSpeed * 0.004;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function resolveShipCollision(state, a, b, shipModel) {
|
|
1652
|
+
const delta = subVec3(b.position, a.position);
|
|
1653
|
+
const radiusA = getShipCollisionRadius(a, shipModel);
|
|
1654
|
+
const radiusB = getShipCollisionRadius(b, shipModel);
|
|
1655
|
+
const distance = Math.hypot(delta.x, delta.z);
|
|
1656
|
+
const minDistance = radiusA + radiusB;
|
|
1657
|
+
if (distance >= minDistance) {
|
|
1658
|
+
return false;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const normal =
|
|
1662
|
+
distance > 0.0001
|
|
1663
|
+
? normalizeVec3(vec3(delta.x / distance, 0, delta.z / distance))
|
|
1664
|
+
: normalizeVec3(vec3(Math.cos(state.time * 5.2), 0, Math.sin(state.time * 4.8)));
|
|
1665
|
+
const tangent = vec3(-normal.z, 0, normal.x);
|
|
1666
|
+
const penetration = minDistance - distance;
|
|
1667
|
+
const invMassA = getShipInverseMass(a, shipModel);
|
|
1668
|
+
const invMassB = getShipInverseMass(b, shipModel);
|
|
1669
|
+
const invMassSum = invMassA + invMassB;
|
|
1670
|
+
const correction = scaleVec3(normal, (penetration / Math.max(0.0001, invMassSum)) * 0.72);
|
|
1671
|
+
a.position = subVec3(a.position, scaleVec3(correction, invMassA));
|
|
1672
|
+
b.position = addVec3(b.position, scaleVec3(correction, invMassB));
|
|
1673
|
+
|
|
1674
|
+
const relativeVelocity = subVec3(b.velocity, a.velocity);
|
|
1675
|
+
const velocityAlongNormal = dotVec3(relativeVelocity, normal);
|
|
1676
|
+
const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.88;
|
|
1677
|
+
if (velocityAlongNormal < 0) {
|
|
1678
|
+
const impulseMagnitude =
|
|
1679
|
+
(-(1 + restitution) * velocityAlongNormal) / Math.max(0.0001, invMassSum);
|
|
1680
|
+
const impulse = scaleVec3(normal, impulseMagnitude);
|
|
1681
|
+
a.velocity = subVec3(a.velocity, scaleVec3(impulse, invMassA));
|
|
1682
|
+
b.velocity = addVec3(b.velocity, scaleVec3(impulse, invMassB));
|
|
1683
|
+
|
|
1684
|
+
const tangentSpeed = dotVec3(relativeVelocity, tangent);
|
|
1685
|
+
const frictionMagnitude = clamp(
|
|
1686
|
+
(-tangentSpeed / Math.max(0.0001, invMassSum)),
|
|
1687
|
+
-impulseMagnitude * 0.16,
|
|
1688
|
+
impulseMagnitude * 0.16
|
|
1689
|
+
);
|
|
1690
|
+
const frictionImpulse = scaleVec3(tangent, frictionMagnitude);
|
|
1691
|
+
a.velocity = subVec3(a.velocity, scaleVec3(frictionImpulse, invMassA));
|
|
1692
|
+
b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
|
|
1693
|
+
|
|
1694
|
+
a.angularVelocity -=
|
|
1695
|
+
tangentSpeed * radiusA * getShipInverseInertia(a, shipModel) * 0.2 +
|
|
1696
|
+
impulseMagnitude * 0.00024;
|
|
1697
|
+
b.angularVelocity +=
|
|
1698
|
+
tangentSpeed * radiusB * getShipInverseInertia(b, shipModel) * 0.2 +
|
|
1699
|
+
impulseMagnitude * 0.00024;
|
|
1700
|
+
|
|
1701
|
+
const impactSpeed = Math.abs(velocityAlongNormal);
|
|
1702
|
+
if (
|
|
1703
|
+
impactSpeed > 0.18 &&
|
|
1704
|
+
Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0
|
|
1705
|
+
) {
|
|
1706
|
+
const contactPoint = vec3(
|
|
1707
|
+
(a.position.x + b.position.x) * 0.5,
|
|
1708
|
+
(a.position.y + b.position.y) * 0.5 + 0.14,
|
|
1709
|
+
(a.position.z + b.position.z) * 0.5
|
|
1710
|
+
);
|
|
1711
|
+
spawnSpray(state, contactPoint, impactSpeed * 2.4 + penetration * 8);
|
|
1712
|
+
state.waveImpulses.push({
|
|
1713
|
+
x: contactPoint.x,
|
|
1714
|
+
z: contactPoint.z,
|
|
1715
|
+
strength: clamp(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
|
|
1716
|
+
radius: 0.9 + penetration * 1.4,
|
|
1717
|
+
life: 1,
|
|
1718
|
+
});
|
|
1719
|
+
state.collisionCount += 1;
|
|
1720
|
+
state.collisionFlash = Math.max(
|
|
1721
|
+
state.collisionFlash,
|
|
1722
|
+
clamp(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
|
|
1723
|
+
);
|
|
1724
|
+
a.collisionCooldown = 0.2;
|
|
1725
|
+
b.collisionCooldown = 0.2;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
state.contactCount += 1;
|
|
1730
|
+
return true;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function updateShips(state, dt, shipModel) {
|
|
1372
1734
|
let collided = false;
|
|
1373
1735
|
state.contactCount = 0;
|
|
1736
|
+
|
|
1374
1737
|
for (const ship of state.ships) {
|
|
1375
|
-
|
|
1376
|
-
ship
|
|
1377
|
-
ship.velocity = scaleVec3(ship.velocity, 1 - (physics.linearDamping ?? 0.04) * dt);
|
|
1378
|
-
ship.angularVelocity *= 1 - (physics.angularDamping ?? 0.08) * dt;
|
|
1379
|
-
ship.position.y =
|
|
1380
|
-
sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.22 +
|
|
1381
|
-
(physics.waterline ?? 0.42);
|
|
1382
|
-
if (Math.abs(ship.position.x) > 10) {
|
|
1383
|
-
ship.velocity.x *= -1;
|
|
1384
|
-
ship.angularVelocity *= -1;
|
|
1385
|
-
}
|
|
1386
|
-
if (ship.position.z < 2 || ship.position.z > 16) {
|
|
1387
|
-
ship.velocity.z *= -1;
|
|
1388
|
-
ship.angularVelocity *= -1;
|
|
1389
|
-
}
|
|
1738
|
+
updateShipMotion(state, ship, dt, shipModel);
|
|
1739
|
+
resolveBoundaryCollision(ship, state, shipModel);
|
|
1390
1740
|
}
|
|
1391
1741
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
const restitution = physics.restitution ?? 0.22;
|
|
1399
|
-
const swapX = a.velocity.x;
|
|
1400
|
-
const swapZ = a.velocity.z;
|
|
1401
|
-
a.velocity.x = -b.velocity.x * (0.86 + restitution);
|
|
1402
|
-
a.velocity.z = -b.velocity.z * (0.82 + restitution);
|
|
1403
|
-
b.velocity.x = -swapX * (0.86 + restitution);
|
|
1404
|
-
b.velocity.z = -swapZ * (0.82 + restitution);
|
|
1405
|
-
a.angularVelocity += 0.55;
|
|
1406
|
-
b.angularVelocity -= 0.55;
|
|
1407
|
-
const contactPoint = vec3((a.position.x + b.position.x) * 0.5, (a.position.y + b.position.y) * 0.5 + 0.1, (a.position.z + b.position.z) * 0.5);
|
|
1408
|
-
spawnSpray(state, contactPoint, Math.abs(dx) + Math.abs(dz));
|
|
1409
|
-
state.waveImpulses.push({
|
|
1410
|
-
x: contactPoint.x,
|
|
1411
|
-
z: contactPoint.z,
|
|
1412
|
-
strength: Math.min(1.4, 0.2 + (Math.abs(dx) + Math.abs(dz)) * 0.18),
|
|
1413
|
-
radius: 0.8,
|
|
1414
|
-
life: 1,
|
|
1415
|
-
});
|
|
1416
|
-
state.collisionCount += 1;
|
|
1417
|
-
state.contactCount = 1;
|
|
1418
|
-
collided = true;
|
|
1742
|
+
for (let index = 0; index < state.ships.length; index += 1) {
|
|
1743
|
+
for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
|
|
1744
|
+
collided =
|
|
1745
|
+
resolveShipCollision(state, state.ships[index], state.ships[otherIndex], shipModel) ||
|
|
1746
|
+
collided;
|
|
1747
|
+
}
|
|
1419
1748
|
}
|
|
1420
|
-
|
|
1749
|
+
|
|
1750
|
+
state.collisionFlash = collided
|
|
1751
|
+
? Math.max(0.12, state.collisionFlash)
|
|
1752
|
+
: Math.max(0, state.collisionFlash - dt * 1.3);
|
|
1421
1753
|
}
|
|
1422
1754
|
|
|
1423
1755
|
function updateWaveImpulses(state, dt) {
|
|
@@ -1540,7 +1872,7 @@ function renderFlagPole(ctx, camera, viewport) {
|
|
|
1540
1872
|
function renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength) {
|
|
1541
1873
|
const bounds = shipModel.bounds;
|
|
1542
1874
|
const keelY = (shipModel.physics.waterline ?? 0.42) - 0.28;
|
|
1543
|
-
const transform = { position: ship.position, rotationY: ship.rotationY, scale:
|
|
1875
|
+
const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
|
|
1544
1876
|
const hullCorners = [
|
|
1545
1877
|
vec3(bounds.min[0], keelY, bounds.min[2]),
|
|
1546
1878
|
vec3(bounds.max[0], keelY, bounds.min[2]),
|
|
@@ -1570,6 +1902,113 @@ function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength
|
|
|
1570
1902
|
});
|
|
1571
1903
|
}
|
|
1572
1904
|
|
|
1905
|
+
function renderGlowLight(
|
|
1906
|
+
ctx,
|
|
1907
|
+
point,
|
|
1908
|
+
camera,
|
|
1909
|
+
viewport,
|
|
1910
|
+
coreColor,
|
|
1911
|
+
glowColor,
|
|
1912
|
+
glowScale,
|
|
1913
|
+
reflectionStrength = 0,
|
|
1914
|
+
state = null
|
|
1915
|
+
) {
|
|
1916
|
+
const projected = projectPoint(point, camera, viewport);
|
|
1917
|
+
if (!projected) {
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
const radius = clamp((1 / projected.depth) * 420 * glowScale, 4, 34);
|
|
1922
|
+
const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
|
|
1923
|
+
halo.addColorStop(0, colorToRgba(coreColor, 0.98));
|
|
1924
|
+
halo.addColorStop(0.5, colorToRgba(glowColor, 0.42));
|
|
1925
|
+
halo.addColorStop(1, colorToRgba(glowColor, 0));
|
|
1926
|
+
ctx.save();
|
|
1927
|
+
ctx.globalCompositeOperation = "screen";
|
|
1928
|
+
ctx.fillStyle = halo;
|
|
1929
|
+
ctx.beginPath();
|
|
1930
|
+
ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
|
|
1931
|
+
ctx.fill();
|
|
1932
|
+
ctx.restore();
|
|
1933
|
+
|
|
1934
|
+
ctx.fillStyle = colorToRgba(coreColor, 0.98);
|
|
1935
|
+
ctx.beginPath();
|
|
1936
|
+
ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
|
|
1937
|
+
ctx.fill();
|
|
1938
|
+
|
|
1939
|
+
if (state && reflectionStrength > 0) {
|
|
1940
|
+
const waterline = sampleWave(state, point.x, point.z, state.time) * 0.22;
|
|
1941
|
+
const reflectedPoint = vec3(point.x, waterline - (point.y - waterline) * 0.58, point.z + 0.08);
|
|
1942
|
+
const reflected = projectPoint(reflectedPoint, camera, viewport);
|
|
1943
|
+
if (reflected) {
|
|
1944
|
+
const reflectionRadius = radius * 0.72;
|
|
1945
|
+
const glow = ctx.createRadialGradient(
|
|
1946
|
+
reflected.x,
|
|
1947
|
+
reflected.y,
|
|
1948
|
+
reflectionRadius * 0.1,
|
|
1949
|
+
reflected.x,
|
|
1950
|
+
reflected.y,
|
|
1951
|
+
reflectionRadius
|
|
1952
|
+
);
|
|
1953
|
+
glow.addColorStop(0, colorToRgba(coreColor, reflectionStrength * 0.34));
|
|
1954
|
+
glow.addColorStop(1, colorToRgba(glowColor, 0));
|
|
1955
|
+
ctx.save();
|
|
1956
|
+
ctx.globalCompositeOperation = "screen";
|
|
1957
|
+
ctx.fillStyle = glow;
|
|
1958
|
+
ctx.beginPath();
|
|
1959
|
+
ctx.ellipse(
|
|
1960
|
+
reflected.x,
|
|
1961
|
+
reflected.y,
|
|
1962
|
+
reflectionRadius * 0.34,
|
|
1963
|
+
reflectionRadius,
|
|
1964
|
+
0,
|
|
1965
|
+
0,
|
|
1966
|
+
Math.PI * 2
|
|
1967
|
+
);
|
|
1968
|
+
ctx.fill();
|
|
1969
|
+
ctx.restore();
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
function renderShipLanterns(ctx, ship, state, camera, viewport, visuals) {
|
|
1975
|
+
const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
|
|
1976
|
+
const strength = readVisualNumber(ship.lanternStrength, 1);
|
|
1977
|
+
for (const lantern of lanterns) {
|
|
1978
|
+
const position = transformPoint(
|
|
1979
|
+
vec3(lantern.x, lantern.y, lantern.z),
|
|
1980
|
+
{ position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
|
|
1981
|
+
);
|
|
1982
|
+
renderGlowLight(
|
|
1983
|
+
ctx,
|
|
1984
|
+
position,
|
|
1985
|
+
camera,
|
|
1986
|
+
viewport,
|
|
1987
|
+
visuals.lanternCore,
|
|
1988
|
+
visuals.lanternGlow,
|
|
1989
|
+
lantern.glow * strength,
|
|
1990
|
+
visuals.lanternReflectionStrength,
|
|
1991
|
+
state
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function renderHarborTorches(ctx, state, camera, viewport, visuals) {
|
|
1997
|
+
for (const torch of HARBOR_TORCHES) {
|
|
1998
|
+
renderGlowLight(
|
|
1999
|
+
ctx,
|
|
2000
|
+
vec3(torch.x, torch.y, torch.z),
|
|
2001
|
+
camera,
|
|
2002
|
+
viewport,
|
|
2003
|
+
visuals.torchCore,
|
|
2004
|
+
visuals.torchGlow,
|
|
2005
|
+
torch.glow,
|
|
2006
|
+
visuals.lanternReflectionStrength * 0.55,
|
|
2007
|
+
state
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1573
2012
|
function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
1574
2013
|
const viewport = { width: canvas.width, height: canvas.height };
|
|
1575
2014
|
const camera = buildCamera(state, canvas);
|
|
@@ -1579,7 +2018,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
1579
2018
|
importance: state.focus === "lighting" ? "critical" : "high",
|
|
1580
2019
|
});
|
|
1581
2020
|
const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
|
|
1582
|
-
const lightDir = normalizeVec3(vec3(-0.
|
|
2021
|
+
const lightDir = normalizeVec3(vec3(-0.22, 0.94, -0.31));
|
|
1583
2022
|
const lightingSnapshot = state.lightingDetail.getSnapshot();
|
|
1584
2023
|
const visuals = resolveVisualConfig(
|
|
1585
2024
|
nearLighting,
|
|
@@ -1656,7 +2095,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
1656
2095
|
for (const ship of state.ships) {
|
|
1657
2096
|
buildTrianglesFromMesh(
|
|
1658
2097
|
shipModel,
|
|
1659
|
-
{ position: ship.position, rotationY: ship.rotationY, scale:
|
|
2098
|
+
{ position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
|
|
1660
2099
|
ship.tint,
|
|
1661
2100
|
camera,
|
|
1662
2101
|
viewport,
|
|
@@ -1672,10 +2111,12 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
1672
2111
|
|
|
1673
2112
|
drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength);
|
|
1674
2113
|
renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
|
|
2114
|
+
renderHarborTorches(ctx, state, camera, viewport, visuals);
|
|
1675
2115
|
renderFlagPole(ctx, camera, viewport);
|
|
1676
2116
|
renderClothAccent(ctx, cloth, camera, viewport);
|
|
1677
2117
|
for (const ship of state.ships) {
|
|
1678
2118
|
renderShipRigging(ctx, ship, camera, viewport);
|
|
2119
|
+
renderShipLanterns(ctx, ship, state, camera, viewport, visuals);
|
|
1679
2120
|
}
|
|
1680
2121
|
renderSprays(ctx, state.sprays, camera, viewport);
|
|
1681
2122
|
|
|
@@ -1689,8 +2130,10 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
1689
2130
|
const sceneMetrics = [
|
|
1690
2131
|
`focus: ${state.focus}`,
|
|
1691
2132
|
`ships: ${state.ships.length} active GLTF hulls`,
|
|
2133
|
+
`moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.length * SHIP_LANTERNS.length} warm deck and harbor lights`,
|
|
1692
2134
|
`physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
|
|
1693
|
-
`physics contacts: ${state.
|
|
2135
|
+
`physics contacts: ${state.contactCount}`,
|
|
2136
|
+
`mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, shipModel) / 1000).toFixed(1)}t`).join(" · ")}`,
|
|
1694
2137
|
`cloth band: ${cloth.band} -> ${cloth.representation.output}`,
|
|
1695
2138
|
`fluid near band: ${water.bandMeshes[0].representation.output}`,
|
|
1696
2139
|
`lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`,
|
|
@@ -1715,8 +2158,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
1715
2158
|
state.focus === "physics"
|
|
1716
2159
|
? [
|
|
1717
2160
|
"Stable world snapshots are taken after the authoritative rigid-body commit and before visual follow-up work.",
|
|
1718
|
-
"The ships collide
|
|
1719
|
-
"
|
|
2161
|
+
"The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
|
|
2162
|
+
"Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water.",
|
|
1720
2163
|
]
|
|
1721
2164
|
: SCENE_NOTES;
|
|
1722
2165
|
const custom = state.demoDescription ?? null;
|
|
@@ -1743,8 +2186,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
1743
2186
|
typeof custom?.details === "string"
|
|
1744
2187
|
? custom.details
|
|
1745
2188
|
: state.focus === "physics"
|
|
1746
|
-
? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver;
|
|
1747
|
-
: `GLTF ships
|
|
2189
|
+
? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver; the heavier hull now carries momentum through mass-aware collision impulses while cloth and fluid remain downstream.`
|
|
2190
|
+
: `Moonlit GLTF ships collide on ${shipModel.physics.shape ?? "box"} physics volumes; lantern reflections, cloth, and fluid remain continuous while the governor pressure is ${state.lastDecision.pressureLevel}.`;
|
|
1748
2191
|
}
|
|
1749
2192
|
|
|
1750
2193
|
function updateSceneState(state, dt, shipModel) {
|
|
@@ -1766,6 +2209,8 @@ function syncTextState(state, shipModel) {
|
|
|
1766
2209
|
z: Number(ship.position.z.toFixed(2)),
|
|
1767
2210
|
vx: Number(ship.velocity.x.toFixed(2)),
|
|
1768
2211
|
vz: Number(ship.velocity.z.toFixed(2)),
|
|
2212
|
+
massKg: Math.round(getShipMass(ship, shipModel)),
|
|
2213
|
+
lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0,
|
|
1769
2214
|
})),
|
|
1770
2215
|
shipPhysics: shipModel.physics,
|
|
1771
2216
|
sprays: state.sprays.length,
|
|
@@ -1794,6 +2239,7 @@ function syncTextState(state, shipModel) {
|
|
|
1794
2239
|
export async function mountGpuShowcase(options = {}) {
|
|
1795
2240
|
injectStyles();
|
|
1796
2241
|
const root = options.root ?? document.body;
|
|
2242
|
+
root.classList?.add?.(ROOT_CLASS);
|
|
1797
2243
|
const previousMarkup = root.innerHTML;
|
|
1798
2244
|
const previousRenderGameToText = window.render_game_to_text;
|
|
1799
2245
|
const previousAdvanceTime = window.advanceTime;
|
|
@@ -1816,14 +2262,15 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
1816
2262
|
syncTextState(state, shipModel);
|
|
1817
2263
|
|
|
1818
2264
|
const ctx = dom.canvas.getContext("2d");
|
|
2265
|
+
if (!ctx) {
|
|
2266
|
+
throw new Error("2D canvas context is required for the shared showcase.");
|
|
2267
|
+
}
|
|
2268
|
+
let animationFrameId = 0;
|
|
1819
2269
|
let destroyed = false;
|
|
1820
|
-
let frameHandle = null;
|
|
1821
|
-
|
|
1822
2270
|
const renderFrame = (nowMs) => {
|
|
1823
2271
|
if (destroyed) {
|
|
1824
2272
|
return;
|
|
1825
2273
|
}
|
|
1826
|
-
|
|
1827
2274
|
if (!state.paused) {
|
|
1828
2275
|
if (state.lastTimeMs == null) {
|
|
1829
2276
|
state.lastTimeMs = nowMs;
|
|
@@ -1841,7 +2288,7 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
1841
2288
|
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
1842
2289
|
renderScene(ctx, dom.canvas, state, shipModel, dom);
|
|
1843
2290
|
syncTextState(state, shipModel);
|
|
1844
|
-
|
|
2291
|
+
animationFrameId = requestAnimationFrame(renderFrame);
|
|
1845
2292
|
};
|
|
1846
2293
|
|
|
1847
2294
|
const handlePauseClick = () => {
|
|
@@ -1863,37 +2310,43 @@ export async function mountGpuShowcase(options = {}) {
|
|
|
1863
2310
|
dom.stressToggle.addEventListener("change", handleStressChange);
|
|
1864
2311
|
dom.focusMode.addEventListener("change", handleFocusChange);
|
|
1865
2312
|
|
|
1866
|
-
|
|
2313
|
+
animationFrameId = requestAnimationFrame(renderFrame);
|
|
2314
|
+
const destroy = () => {
|
|
2315
|
+
if (destroyed) {
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
destroyed = true;
|
|
2319
|
+
if (animationFrameId) {
|
|
2320
|
+
cancelAnimationFrame(animationFrameId);
|
|
2321
|
+
}
|
|
2322
|
+
dom.pauseButton.removeEventListener("click", handlePauseClick);
|
|
2323
|
+
dom.stressToggle.removeEventListener("change", handleStressChange);
|
|
2324
|
+
dom.focusMode.removeEventListener("change", handleFocusChange);
|
|
2325
|
+
try {
|
|
2326
|
+
if (typeof options.destroyState === "function") {
|
|
2327
|
+
options.destroyState(state.packageState);
|
|
2328
|
+
}
|
|
2329
|
+
} finally {
|
|
2330
|
+
state.packageState = undefined;
|
|
2331
|
+
}
|
|
2332
|
+
root.classList?.remove?.(ROOT_CLASS);
|
|
2333
|
+
root.innerHTML = previousMarkup;
|
|
2334
|
+
if (typeof previousRenderGameToText === "function") {
|
|
2335
|
+
window.render_game_to_text = previousRenderGameToText;
|
|
2336
|
+
} else {
|
|
2337
|
+
delete window.render_game_to_text;
|
|
2338
|
+
}
|
|
2339
|
+
if (typeof previousAdvanceTime === "function") {
|
|
2340
|
+
window.advanceTime = previousAdvanceTime;
|
|
2341
|
+
} else {
|
|
2342
|
+
delete window.advanceTime;
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
1867
2345
|
return {
|
|
1868
2346
|
state,
|
|
1869
2347
|
shipModel,
|
|
1870
2348
|
canvas: dom.canvas,
|
|
1871
|
-
destroy
|
|
1872
|
-
if (destroyed) {
|
|
1873
|
-
return;
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
destroyed = true;
|
|
1877
|
-
if (frameHandle != null) {
|
|
1878
|
-
cancelAnimationFrame(frameHandle);
|
|
1879
|
-
}
|
|
1880
|
-
dom.pauseButton.removeEventListener("click", handlePauseClick);
|
|
1881
|
-
dom.stressToggle.removeEventListener("change", handleStressChange);
|
|
1882
|
-
dom.focusMode.removeEventListener("change", handleFocusChange);
|
|
1883
|
-
root.innerHTML = previousMarkup;
|
|
1884
|
-
|
|
1885
|
-
if (typeof previousRenderGameToText === "function") {
|
|
1886
|
-
window.render_game_to_text = previousRenderGameToText;
|
|
1887
|
-
} else {
|
|
1888
|
-
delete window.render_game_to_text;
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
if (typeof previousAdvanceTime === "function") {
|
|
1892
|
-
window.advanceTime = previousAdvanceTime;
|
|
1893
|
-
} else {
|
|
1894
|
-
delete window.advanceTime;
|
|
1895
|
-
}
|
|
1896
|
-
},
|
|
2349
|
+
destroy,
|
|
1897
2350
|
};
|
|
1898
2351
|
}
|
|
1899
2352
|
|
|
@@ -1908,7 +2361,7 @@ function updatePhysicsSnapshot(state, shipModel) {
|
|
|
1908
2361
|
animationInputRevision: state.frame,
|
|
1909
2362
|
bodyCount: state.ships.length + 2,
|
|
1910
2363
|
dynamicBodyCount: state.ships.length,
|
|
1911
|
-
contactCount: state.
|
|
2364
|
+
contactCount: state.contactCount,
|
|
1912
2365
|
metadata: {
|
|
1913
2366
|
collisionCount: state.collisionCount,
|
|
1914
2367
|
contactCount: state.contactCount,
|