@plasius/gpu-shared 0.1.4 → 0.1.7
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 +34 -0
- package/dist/{chunk-S5NCFNKJ.js → chunk-OTCJ3VOK.js} +23 -2
- package/dist/{chunk-S5NCFNKJ.js.map → chunk-OTCJ3VOK.js.map} +1 -1
- 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 +587 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{showcase-runtime-4BS7TWHS.js → showcase-runtime-AIPDRK7G.js} +535 -165
- package/dist/showcase-runtime-AIPDRK7G.js.map +1 -0
- package/package.json +3 -3
- package/src/asset-url.js +28 -1
- package/src/gltf-loader.js +36 -1
- package/src/index.d.ts +4 -0
- package/src/showcase-runtime.js +619 -163
- package/dist/chunk-UUJLYYQS.js.map +0 -1
- package/dist/showcase-runtime-4BS7TWHS.js.map +0 -1
- /package/dist/{gltf-loader-4FNTT63R.js.map → gltf-loader-LKALCZAV.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -20,11 +20,32 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
20
20
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
21
|
|
|
22
22
|
// src/asset-url.js
|
|
23
|
+
function createInlineShowcaseAssetUrl() {
|
|
24
|
+
return new URL(INLINE_BRIGANTINE_GLTF_URL);
|
|
25
|
+
}
|
|
26
|
+
function getBrowserBaseUrl() {
|
|
27
|
+
if (typeof document !== "undefined" && typeof document.baseURI === "string" && document.baseURI.length > 0) {
|
|
28
|
+
return document.baseURI;
|
|
29
|
+
}
|
|
30
|
+
if (typeof window !== "undefined" && typeof window.location?.href === "string" && window.location.href.length > 0) {
|
|
31
|
+
return window.location.href;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
23
35
|
function resolveShowcaseAssetUrl(baseUrl2 = import_meta.url) {
|
|
24
36
|
try {
|
|
25
37
|
return new URL("../assets/brigantine.gltf", baseUrl2);
|
|
26
38
|
} catch {
|
|
27
|
-
|
|
39
|
+
const browserBaseUrl = getBrowserBaseUrl();
|
|
40
|
+
if (browserBaseUrl) {
|
|
41
|
+
try {
|
|
42
|
+
const normalizedBaseUrl = new URL(baseUrl2, browserBaseUrl);
|
|
43
|
+
return new URL("../assets/brigantine.gltf", normalizedBaseUrl);
|
|
44
|
+
} catch {
|
|
45
|
+
return createInlineShowcaseAssetUrl();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return createInlineShowcaseAssetUrl();
|
|
28
49
|
}
|
|
29
50
|
}
|
|
30
51
|
var import_meta, INLINE_BRIGANTINE_GLTF_URL;
|
|
@@ -110,12 +131,39 @@ function computeBounds(positions) {
|
|
|
110
131
|
}
|
|
111
132
|
return { min, max };
|
|
112
133
|
}
|
|
134
|
+
function resolveBrowserRequestBaseUrl() {
|
|
135
|
+
if (typeof document !== "undefined" && typeof document.baseURI === "string" && document.baseURI.length > 0) {
|
|
136
|
+
return document.baseURI;
|
|
137
|
+
}
|
|
138
|
+
if (typeof window !== "undefined" && typeof window.location?.href === "string" && window.location.href.length > 0) {
|
|
139
|
+
return window.location.href;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
function resolveFetchBaseUrl(requestUrl, responseUrl) {
|
|
144
|
+
if (typeof responseUrl === "string" && responseUrl.length > 0) {
|
|
145
|
+
try {
|
|
146
|
+
return new URL(responseUrl);
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
return new URL(requestUrl);
|
|
152
|
+
} catch {
|
|
153
|
+
const browserBaseUrl = resolveBrowserRequestBaseUrl();
|
|
154
|
+
if (browserBaseUrl) {
|
|
155
|
+
return new URL(requestUrl, browserBaseUrl);
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`Unable to resolve a stable base URL for glTF asset loading: ${String(requestUrl)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
113
160
|
async function loadGltfModel(url) {
|
|
114
161
|
const response = await fetch(url);
|
|
115
162
|
if (!response.ok) {
|
|
116
163
|
throw new Error(`Failed to load glTF asset: ${response.status} ${response.statusText}`);
|
|
117
164
|
}
|
|
118
165
|
const document2 = await response.json();
|
|
166
|
+
const baseUrl2 = resolveFetchBaseUrl(url, response.url);
|
|
119
167
|
const buffers = await Promise.all(
|
|
120
168
|
(document2.buffers ?? []).map(async (buffer) => {
|
|
121
169
|
if (typeof buffer.uri !== "string") {
|
|
@@ -124,7 +172,7 @@ async function loadGltfModel(url) {
|
|
|
124
172
|
if (buffer.uri.startsWith("data:")) {
|
|
125
173
|
return decodeDataUri(buffer.uri);
|
|
126
174
|
}
|
|
127
|
-
const nested = await fetch(new URL(buffer.uri,
|
|
175
|
+
const nested = await fetch(new URL(buffer.uri, baseUrl2));
|
|
128
176
|
if (!nested.ok) {
|
|
129
177
|
throw new Error(`Failed to load glTF buffer: ${nested.status} ${nested.statusText}`);
|
|
130
178
|
}
|
|
@@ -2172,6 +2220,9 @@ var init_dist2 = __esm({
|
|
|
2172
2220
|
});
|
|
2173
2221
|
|
|
2174
2222
|
// node_modules/@plasius/gpu-lighting/dist/index.js
|
|
2223
|
+
function createModuleBaseUrl(metaUrl) {
|
|
2224
|
+
return Reflect.construct(URL, [String(metaUrl)]);
|
|
2225
|
+
}
|
|
2175
2226
|
function buildTechnique(name, spec) {
|
|
2176
2227
|
const preludeUrl = new URL(`./techniques/${name}/${spec.prelude}`, baseUrl);
|
|
2177
2228
|
const jobs = Object.entries(spec.jobs).map(([key, file]) => {
|
|
@@ -2480,7 +2531,7 @@ var init_dist3 = __esm({
|
|
|
2480
2531
|
});
|
|
2481
2532
|
baseUrl = (() => {
|
|
2482
2533
|
if (typeof import_meta2.url !== "undefined") {
|
|
2483
|
-
return
|
|
2534
|
+
return createModuleBaseUrl(import_meta2.url);
|
|
2484
2535
|
}
|
|
2485
2536
|
if (typeof __filename !== "undefined" && typeof __require !== "undefined") {
|
|
2486
2537
|
const { pathToFileURL } = __require("url");
|
|
@@ -5107,7 +5158,7 @@ var init_dist5 = __esm({
|
|
|
5107
5158
|
}
|
|
5108
5159
|
});
|
|
5109
5160
|
|
|
5110
|
-
// node_modules/@plasius/gpu-physics/dist/chunk-
|
|
5161
|
+
// node_modules/@plasius/gpu-physics/dist/chunk-PFUNZLNF.js
|
|
5111
5162
|
function assertIdentifier5(name, value) {
|
|
5112
5163
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
5113
5164
|
throw new Error(`${name} must be a non-empty string.`);
|
|
@@ -5256,7 +5307,7 @@ function getPhysicsWorkerManifest(profile = defaultPhysicsWorkerProfile) {
|
|
|
5256
5307
|
return manifest;
|
|
5257
5308
|
}
|
|
5258
5309
|
function createPhysicsSimulationPlan(profile = defaultPhysicsWorkerProfile) {
|
|
5259
|
-
const plan =
|
|
5310
|
+
const plan = physicsSimulationPlans[profile];
|
|
5260
5311
|
if (!plan) {
|
|
5261
5312
|
const available = physicsWorkerProfileNames.join(", ");
|
|
5262
5313
|
throw new Error(
|
|
@@ -5320,9 +5371,9 @@ function createPhysicsWorldSnapshot(input) {
|
|
|
5320
5371
|
metadata: normalizeMetadata(input.metadata)
|
|
5321
5372
|
});
|
|
5322
5373
|
}
|
|
5323
|
-
var physicsDebugOwner, physicsWorkerQueueClasses, defaultPhysicsWorkerProfile, physicsSimulationStageOrder, physicsWorkerProfileSpecs, physicsWorkerDagSpecs,
|
|
5324
|
-
var
|
|
5325
|
-
"node_modules/@plasius/gpu-physics/dist/chunk-
|
|
5374
|
+
var physicsDebugOwner, physicsWorkerQueueClasses, defaultPhysicsWorkerProfile, physicsSimulationStageOrder, physicsWorkerProfileSpecs, physicsWorkerDagSpecs, physicsSimulationPlans, physicsWorkerManifests, physicsWorkerProfileNames;
|
|
5375
|
+
var init_chunk_PFUNZLNF = __esm({
|
|
5376
|
+
"node_modules/@plasius/gpu-physics/dist/chunk-PFUNZLNF.js"() {
|
|
5326
5377
|
physicsDebugOwner = "physics";
|
|
5327
5378
|
physicsWorkerQueueClasses = Object.freeze({
|
|
5328
5379
|
simulation: "simulation",
|
|
@@ -6068,7 +6119,7 @@ var init_chunk_CNTXT5QJ = __esm({
|
|
|
6068
6119
|
contactVisuals: { priority: 1, dependencies: ["worldSnapshot"] }
|
|
6069
6120
|
}
|
|
6070
6121
|
};
|
|
6071
|
-
|
|
6122
|
+
physicsSimulationPlans = Object.freeze({
|
|
6072
6123
|
gameplay: Object.freeze({
|
|
6073
6124
|
description: "Gameplay simulation plan that hands off a stable post-commit world snapshot to visual preparation.",
|
|
6074
6125
|
snapshotStageId: "worldSnapshot",
|
|
@@ -6284,7 +6335,7 @@ var init_chunk_CNTXT5QJ = __esm({
|
|
|
6284
6335
|
// node_modules/@plasius/gpu-physics/dist/browser.js
|
|
6285
6336
|
var init_browser = __esm({
|
|
6286
6337
|
"node_modules/@plasius/gpu-physics/dist/browser.js"() {
|
|
6287
|
-
|
|
6338
|
+
init_chunk_PFUNZLNF();
|
|
6288
6339
|
}
|
|
6289
6340
|
});
|
|
6290
6341
|
|
|
@@ -6301,27 +6352,27 @@ function injectStyles() {
|
|
|
6301
6352
|
const style = document.createElement("style");
|
|
6302
6353
|
style.id = STYLE_ID;
|
|
6303
6354
|
style.textContent = `
|
|
6304
|
-
|
|
6305
|
-
color-scheme:
|
|
6306
|
-
--plasius-paper: #
|
|
6307
|
-
--plasius-ink: #
|
|
6308
|
-
--plasius-muted: #
|
|
6309
|
-
--plasius-accent: #
|
|
6310
|
-
--plasius-panel: rgba(
|
|
6311
|
-
--plasius-border: rgba(
|
|
6312
|
-
--plasius-shadow: 0
|
|
6313
|
-
}
|
|
6314
|
-
* {
|
|
6315
|
-
box-sizing: border-box;
|
|
6316
|
-
}
|
|
6317
|
-
body {
|
|
6355
|
+
.${ROOT_CLASS} {
|
|
6356
|
+
color-scheme: dark;
|
|
6357
|
+
--plasius-paper: #081321;
|
|
6358
|
+
--plasius-ink: #edf4ff;
|
|
6359
|
+
--plasius-muted: #b6c5dd;
|
|
6360
|
+
--plasius-accent: #f3b16a;
|
|
6361
|
+
--plasius-panel: rgba(8, 19, 33, 0.72);
|
|
6362
|
+
--plasius-border: rgba(159, 185, 223, 0.18);
|
|
6363
|
+
--plasius-shadow: 0 24px 56px rgba(1, 6, 14, 0.44);
|
|
6318
6364
|
margin: 0;
|
|
6319
|
-
min-height:
|
|
6365
|
+
min-height: 100%;
|
|
6320
6366
|
font-family: "Fraunces", "Iowan Old Style", serif;
|
|
6321
6367
|
color: var(--plasius-ink);
|
|
6322
6368
|
background:
|
|
6323
|
-
radial-gradient(circle at
|
|
6324
|
-
|
|
6369
|
+
radial-gradient(circle at 18% 12%, rgba(73, 101, 170, 0.28), transparent 30%),
|
|
6370
|
+
radial-gradient(circle at 82% 18%, rgba(240, 188, 103, 0.08), transparent 18%),
|
|
6371
|
+
linear-gradient(180deg, #04101d 0%, #0b1930 42%, #081321 100%);
|
|
6372
|
+
}
|
|
6373
|
+
.${ROOT_CLASS},
|
|
6374
|
+
.${ROOT_CLASS} * {
|
|
6375
|
+
box-sizing: border-box;
|
|
6325
6376
|
}
|
|
6326
6377
|
.plasius-demo {
|
|
6327
6378
|
width: min(1560px, calc(100vw - 32px));
|
|
@@ -6355,7 +6406,7 @@ function injectStyles() {
|
|
|
6355
6406
|
text-transform: uppercase;
|
|
6356
6407
|
letter-spacing: 0.18em;
|
|
6357
6408
|
font-size: 12px;
|
|
6358
|
-
color: rgba(
|
|
6409
|
+
color: rgba(226, 236, 255, 0.58);
|
|
6359
6410
|
}
|
|
6360
6411
|
.plasius-demo h1,
|
|
6361
6412
|
.plasius-demo h2,
|
|
@@ -6373,7 +6424,7 @@ function injectStyles() {
|
|
|
6373
6424
|
margin: 0;
|
|
6374
6425
|
padding: 8px 12px;
|
|
6375
6426
|
border-radius: 999px;
|
|
6376
|
-
background: rgba(
|
|
6427
|
+
background: rgba(243, 177, 106, 0.14);
|
|
6377
6428
|
color: var(--plasius-accent);
|
|
6378
6429
|
font-weight: 700;
|
|
6379
6430
|
}
|
|
@@ -6395,8 +6446,8 @@ function injectStyles() {
|
|
|
6395
6446
|
aspect-ratio: 16 / 9;
|
|
6396
6447
|
display: block;
|
|
6397
6448
|
border-radius: 20px;
|
|
6398
|
-
border: 1px solid rgba(
|
|
6399
|
-
background: linear-gradient(180deg, #
|
|
6449
|
+
border: 1px solid rgba(159, 185, 223, 0.12);
|
|
6450
|
+
background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
|
|
6400
6451
|
}
|
|
6401
6452
|
.plasius-demo__toolbar {
|
|
6402
6453
|
position: absolute;
|
|
@@ -6416,9 +6467,9 @@ function injectStyles() {
|
|
|
6416
6467
|
.plasius-demo button,
|
|
6417
6468
|
.plasius-demo .plasius-toggle,
|
|
6418
6469
|
.plasius-demo select {
|
|
6419
|
-
border: 1px solid rgba(
|
|
6470
|
+
border: 1px solid rgba(159, 185, 223, 0.18);
|
|
6420
6471
|
border-radius: 999px;
|
|
6421
|
-
background: rgba(
|
|
6472
|
+
background: rgba(9, 20, 34, 0.84);
|
|
6422
6473
|
color: var(--plasius-ink);
|
|
6423
6474
|
padding: 10px 14px;
|
|
6424
6475
|
}
|
|
@@ -6457,8 +6508,8 @@ function injectStyles() {
|
|
|
6457
6508
|
bottom: 24px;
|
|
6458
6509
|
padding: 10px 14px;
|
|
6459
6510
|
border-radius: 16px;
|
|
6460
|
-
background: rgba(
|
|
6461
|
-
border: 1px solid rgba(
|
|
6511
|
+
background: rgba(9, 20, 34, 0.82);
|
|
6512
|
+
border: 1px solid rgba(159, 185, 223, 0.16);
|
|
6462
6513
|
color: var(--plasius-muted);
|
|
6463
6514
|
font-size: 12px;
|
|
6464
6515
|
line-height: 1.45;
|
|
@@ -6470,7 +6521,7 @@ function injectStyles() {
|
|
|
6470
6521
|
}
|
|
6471
6522
|
.plasius-demo__footer {
|
|
6472
6523
|
margin-top: 4px;
|
|
6473
|
-
color: rgba(
|
|
6524
|
+
color: rgba(226, 236, 255, 0.68);
|
|
6474
6525
|
font-size: 13px;
|
|
6475
6526
|
line-height: 1.6;
|
|
6476
6527
|
}
|
|
@@ -6489,6 +6540,10 @@ function clamp(value, min, max) {
|
|
|
6489
6540
|
function mix(a, b, t) {
|
|
6490
6541
|
return a + (b - a) * t;
|
|
6491
6542
|
}
|
|
6543
|
+
function pseudoRandom(seed) {
|
|
6544
|
+
const value = Math.sin(seed * 12.9898 + seed * seed * 17e-4) * 43758.5453;
|
|
6545
|
+
return value - Math.floor(value);
|
|
6546
|
+
}
|
|
6492
6547
|
function vec3(x = 0, y = 0, z = 0) {
|
|
6493
6548
|
return { x, y, z };
|
|
6494
6549
|
}
|
|
@@ -6522,6 +6577,12 @@ function reflectVec3(vector, normal) {
|
|
|
6522
6577
|
const unitNormal = normalizeVec3(normal);
|
|
6523
6578
|
return subVec3(vector, scaleVec3(unitNormal, 2 * dotVec3(vector, unitNormal)));
|
|
6524
6579
|
}
|
|
6580
|
+
function directionFromYaw(yaw) {
|
|
6581
|
+
return normalizeVec3(vec3(Math.sin(yaw), 0, Math.cos(yaw)));
|
|
6582
|
+
}
|
|
6583
|
+
function perpendicularOnWater(direction) {
|
|
6584
|
+
return vec3(-direction.z, 0, direction.x);
|
|
6585
|
+
}
|
|
6525
6586
|
function rotateY(point, angle) {
|
|
6526
6587
|
const cosine = Math.cos(angle);
|
|
6527
6588
|
const sine = Math.sin(angle);
|
|
@@ -6704,7 +6765,7 @@ function buildDemoDom(root, options) {
|
|
|
6704
6765
|
<section class="plasius-panel plasius-demo__status">
|
|
6705
6766
|
<p id="demoStatus" class="plasius-demo__status-badge">Booting 3D scene\u2026</p>
|
|
6706
6767
|
<p id="demoDetails" class="plasius-demo__status-text">
|
|
6707
|
-
Preparing GLTF
|
|
6768
|
+
Preparing a moonlit harbor scene, GLTF hull data, cloth and fluid continuity plans, and adaptive quality metadata.
|
|
6708
6769
|
</p>
|
|
6709
6770
|
</section>
|
|
6710
6771
|
</section>
|
|
@@ -6732,9 +6793,9 @@ function buildDemoDom(root, options) {
|
|
|
6732
6793
|
</div>
|
|
6733
6794
|
<div class="plasius-demo__legend">
|
|
6734
6795
|
<strong>Scene</strong>
|
|
6735
|
-
GLTF ships carry
|
|
6736
|
-
|
|
6737
|
-
|
|
6796
|
+
GLTF ships carry hull mass and damping metadata.<br />
|
|
6797
|
+
Lanterns and torches warm the moonlit harbor.<br />
|
|
6798
|
+
Mass-aware collisions stay authoritative near the camera.
|
|
6738
6799
|
</div>
|
|
6739
6800
|
</section>
|
|
6740
6801
|
<aside class="plasius-demo__sidebar">
|
|
@@ -6815,6 +6876,7 @@ function buildSceneSnapshot(state, shipModel) {
|
|
|
6815
6876
|
})
|
|
6816
6877
|
)
|
|
6817
6878
|
),
|
|
6879
|
+
shipPhysics: shipModel?.physics ?? null,
|
|
6818
6880
|
physics: Object.freeze({
|
|
6819
6881
|
profile: state.physics.profile,
|
|
6820
6882
|
plan: state.physics.plan,
|
|
@@ -6858,28 +6920,39 @@ function readVisualNumber(value, fallback) {
|
|
|
6858
6920
|
function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
|
|
6859
6921
|
const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
|
|
6860
6922
|
const defaults = {
|
|
6861
|
-
skyTop: premiumShadows ? "#
|
|
6862
|
-
skyMid: premiumShadows ? "#
|
|
6863
|
-
skyBottom: premiumShadows ? "#
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6923
|
+
skyTop: premiumShadows ? "#040c1a" : "#06101f",
|
|
6924
|
+
skyMid: premiumShadows ? "#11203b" : "#152643",
|
|
6925
|
+
skyBottom: premiumShadows ? "#2f4468" : "#364d73",
|
|
6926
|
+
duskGlow: premiumShadows ? "rgba(116, 142, 201, 0.26)" : "rgba(104, 128, 188, 0.22)",
|
|
6927
|
+
seaTop: premiumShadows ? "#102946" : "#153050",
|
|
6928
|
+
seaMid: premiumShadows ? "#0a1d33" : "#0d2138",
|
|
6929
|
+
seaBottom: "#04101d",
|
|
6930
|
+
moonCore: "rgba(241, 246, 255, 0.98)",
|
|
6931
|
+
moonHalo: "rgba(167, 191, 255, 0.24)",
|
|
6932
|
+
moonReflection: "rgba(192, 214, 255, 0.22)",
|
|
6933
|
+
starColor: "rgba(232, 239, 255, 0.82)",
|
|
6934
|
+
ambientMist: "rgba(41, 63, 97, 0.16)",
|
|
6868
6935
|
reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
|
|
6869
6936
|
shadowAccent: lightingSnapshot.currentLevel.config.shadowStrength,
|
|
6870
|
-
waveAmplitude:
|
|
6871
|
-
waveDirection: { x: 0.
|
|
6872
|
-
wavePhaseSpeed:
|
|
6873
|
-
wakeStrength: 0.
|
|
6874
|
-
wakeLength:
|
|
6875
|
-
collisionRippleStrength: 0.
|
|
6876
|
-
waterNear: { r: 0.
|
|
6877
|
-
waterFar: { r: 0.
|
|
6878
|
-
harborWall: { r: 0.
|
|
6879
|
-
harborDeck: { r: 0.
|
|
6880
|
-
harborTower: { r: 0.
|
|
6881
|
-
flagColor: { r: 0.
|
|
6882
|
-
flagMotion:
|
|
6937
|
+
waveAmplitude: 0.94,
|
|
6938
|
+
waveDirection: { x: 0.88, z: 0.28 },
|
|
6939
|
+
wavePhaseSpeed: 0.88,
|
|
6940
|
+
wakeStrength: 0.31,
|
|
6941
|
+
wakeLength: 18,
|
|
6942
|
+
collisionRippleStrength: 0.42,
|
|
6943
|
+
waterNear: { r: 0.08, g: 0.23, b: 0.33 },
|
|
6944
|
+
waterFar: { r: 0.18, g: 0.35, b: 0.49 },
|
|
6945
|
+
harborWall: { r: 0.26, g: 0.24, b: 0.28 },
|
|
6946
|
+
harborDeck: { r: 0.33, g: 0.22, b: 0.16 },
|
|
6947
|
+
harborTower: { r: 0.23, g: 0.24, b: 0.29 },
|
|
6948
|
+
flagColor: { r: 0.66, g: 0.16, b: 0.13 },
|
|
6949
|
+
flagMotion: 0.92,
|
|
6950
|
+
lanternCore: { r: 0.98, g: 0.8, b: 0.48 },
|
|
6951
|
+
lanternGlow: { r: 1, g: 0.56, b: 0.2 },
|
|
6952
|
+
lanternReflectionStrength: 0.42,
|
|
6953
|
+
torchCore: { r: 0.99, g: 0.72, b: 0.36 },
|
|
6954
|
+
torchGlow: { r: 0.98, g: 0.38, b: 0.15 },
|
|
6955
|
+
collisionFlash: "rgba(255, 212, 168, 0.16)"
|
|
6883
6956
|
};
|
|
6884
6957
|
return {
|
|
6885
6958
|
skyTop: typeof customVisuals.skyTop === "string" ? customVisuals.skyTop : defaults.skyTop,
|
|
@@ -6888,7 +6961,12 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
|
|
|
6888
6961
|
seaTop: typeof customVisuals.seaTop === "string" ? customVisuals.seaTop : defaults.seaTop,
|
|
6889
6962
|
seaMid: typeof customVisuals.seaMid === "string" ? customVisuals.seaMid : defaults.seaMid,
|
|
6890
6963
|
seaBottom: typeof customVisuals.seaBottom === "string" ? customVisuals.seaBottom : defaults.seaBottom,
|
|
6891
|
-
|
|
6964
|
+
duskGlow: typeof customVisuals.duskGlow === "string" ? customVisuals.duskGlow : defaults.duskGlow,
|
|
6965
|
+
moonCore: typeof customVisuals.moonCore === "string" ? customVisuals.moonCore : typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.moonCore,
|
|
6966
|
+
moonHalo: typeof customVisuals.moonHalo === "string" ? customVisuals.moonHalo : defaults.moonHalo,
|
|
6967
|
+
moonReflection: typeof customVisuals.moonReflection === "string" ? customVisuals.moonReflection : defaults.moonReflection,
|
|
6968
|
+
starColor: typeof customVisuals.starColor === "string" ? customVisuals.starColor : defaults.starColor,
|
|
6969
|
+
ambientMist: typeof customVisuals.ambientMist === "string" ? customVisuals.ambientMist : defaults.ambientMist,
|
|
6892
6970
|
reflectionStrength: readVisualNumber(
|
|
6893
6971
|
customVisuals.reflectionStrength,
|
|
6894
6972
|
defaults.reflectionStrength
|
|
@@ -6909,7 +6987,16 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
|
|
|
6909
6987
|
harborDeck: normalizeColorOverride(customVisuals.harborDeck, defaults.harborDeck),
|
|
6910
6988
|
harborTower: normalizeColorOverride(customVisuals.harborTower, defaults.harborTower),
|
|
6911
6989
|
flagColor: normalizeColorOverride(customVisuals.flagColor, defaults.flagColor),
|
|
6912
|
-
flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion)
|
|
6990
|
+
flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion),
|
|
6991
|
+
lanternCore: normalizeColorOverride(customVisuals.lanternCore, defaults.lanternCore),
|
|
6992
|
+
lanternGlow: normalizeColorOverride(customVisuals.lanternGlow, defaults.lanternGlow),
|
|
6993
|
+
lanternReflectionStrength: readVisualNumber(
|
|
6994
|
+
customVisuals.lanternReflectionStrength,
|
|
6995
|
+
defaults.lanternReflectionStrength
|
|
6996
|
+
),
|
|
6997
|
+
torchCore: normalizeColorOverride(customVisuals.torchCore, defaults.torchCore),
|
|
6998
|
+
torchGlow: normalizeColorOverride(customVisuals.torchGlow, defaults.torchGlow),
|
|
6999
|
+
collisionFlash: typeof customVisuals.collisionFlash === "string" ? customVisuals.collisionFlash : defaults.collisionFlash
|
|
6913
7000
|
};
|
|
6914
7001
|
}
|
|
6915
7002
|
function buildClothSurface(model, state, meshDetail, visuals) {
|
|
@@ -7144,18 +7231,34 @@ function createSceneState(options) {
|
|
|
7144
7231
|
{
|
|
7145
7232
|
id: "northwind",
|
|
7146
7233
|
position: vec3(-5.2, 0, 7.2),
|
|
7147
|
-
velocity: vec3(2.
|
|
7148
|
-
rotationY: 0.
|
|
7149
|
-
angularVelocity: 0.
|
|
7150
|
-
tint: { r: 0.62, g: 0.39, b: 0.23 }
|
|
7234
|
+
velocity: vec3(2.35, 0, -1.08),
|
|
7235
|
+
rotationY: 0.58,
|
|
7236
|
+
angularVelocity: 0.09,
|
|
7237
|
+
tint: { r: 0.62, g: 0.39, b: 0.23 },
|
|
7238
|
+
massScale: 1.42,
|
|
7239
|
+
cruiseSpeed: 2.25,
|
|
7240
|
+
throttleResponse: 0.46,
|
|
7241
|
+
rudderResponse: 0.54,
|
|
7242
|
+
wanderPhase: 0.35,
|
|
7243
|
+
lanterns: SHIP_LANTERNS,
|
|
7244
|
+
lanternStrength: 1.06,
|
|
7245
|
+
collisionRadiusScale: 1.04
|
|
7151
7246
|
},
|
|
7152
7247
|
{
|
|
7153
7248
|
id: "tidecaller",
|
|
7154
7249
|
position: vec3(4.8, 0, 4.4),
|
|
7155
|
-
velocity: vec3(-
|
|
7156
|
-
rotationY: -2.
|
|
7157
|
-
angularVelocity: -0.
|
|
7158
|
-
tint: { r: 0.48, g: 0.28, b: 0.19 }
|
|
7250
|
+
velocity: vec3(-2.15, 0, 1.74),
|
|
7251
|
+
rotationY: -2.48,
|
|
7252
|
+
angularVelocity: -0.2,
|
|
7253
|
+
tint: { r: 0.48, g: 0.28, b: 0.19 },
|
|
7254
|
+
massScale: 0.84,
|
|
7255
|
+
cruiseSpeed: 2.68,
|
|
7256
|
+
throttleResponse: 0.7,
|
|
7257
|
+
rudderResponse: 0.78,
|
|
7258
|
+
wanderPhase: 1.6,
|
|
7259
|
+
lanterns: SHIP_LANTERNS,
|
|
7260
|
+
lanternStrength: 1.18,
|
|
7261
|
+
collisionRadiusScale: 0.94
|
|
7159
7262
|
}
|
|
7160
7263
|
],
|
|
7161
7264
|
sprays: [],
|
|
@@ -7177,41 +7280,71 @@ function setListContent(element, values) {
|
|
|
7177
7280
|
element.innerHTML = values.map((value) => `<li>${value}</li>`).join("");
|
|
7178
7281
|
}
|
|
7179
7282
|
function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength, visuals) {
|
|
7180
|
-
const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
|
|
7181
7283
|
const sky = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.5);
|
|
7182
7284
|
sky.addColorStop(0, visuals.skyTop);
|
|
7183
|
-
sky.addColorStop(0.
|
|
7285
|
+
sky.addColorStop(0.54, visuals.skyMid);
|
|
7184
7286
|
sky.addColorStop(1, visuals.skyBottom);
|
|
7185
7287
|
ctx.fillStyle = sky;
|
|
7186
7288
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
7289
|
+
for (let index = 0; index < 70; index += 1) {
|
|
7290
|
+
const x = pseudoRandom(index + 13) * canvas.width;
|
|
7291
|
+
const y = pseudoRandom(index * 7 + 5) * canvas.height * 0.42;
|
|
7292
|
+
const twinkle = 0.45 + Math.sin(state.time * 1.4 + index * 0.73) * 0.25;
|
|
7293
|
+
const radius = 0.6 + pseudoRandom(index * 11 + 2) * 1.9;
|
|
7294
|
+
ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${clamp(twinkle, 0.16, 0.92)})`);
|
|
7295
|
+
ctx.beginPath();
|
|
7296
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
7297
|
+
ctx.fill();
|
|
7298
|
+
}
|
|
7299
|
+
const horizonGlow = ctx.createLinearGradient(0, canvas.height * 0.22, 0, canvas.height * 0.62);
|
|
7300
|
+
horizonGlow.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
7301
|
+
horizonGlow.addColorStop(1, visuals.duskGlow);
|
|
7302
|
+
ctx.fillStyle = horizonGlow;
|
|
7303
|
+
ctx.fillRect(0, canvas.height * 0.2, canvas.width, canvas.height * 0.45);
|
|
7187
7304
|
const shoreline = ctx.createLinearGradient(0, canvas.height * 0.45, 0, canvas.height);
|
|
7188
7305
|
shoreline.addColorStop(0, visuals.seaTop);
|
|
7189
7306
|
shoreline.addColorStop(0.52, visuals.seaMid);
|
|
7190
7307
|
shoreline.addColorStop(1, visuals.seaBottom);
|
|
7191
7308
|
ctx.fillStyle = shoreline;
|
|
7192
7309
|
ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
|
|
7193
|
-
const
|
|
7194
|
-
const
|
|
7195
|
-
const
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7310
|
+
const moonX = canvas.width * 0.76 + Math.sin(state.time * 0.045) * 18;
|
|
7311
|
+
const moonY = canvas.height * 0.17 + Math.cos(state.time * 0.05) * 10;
|
|
7312
|
+
const moon = ctx.createRadialGradient(moonX, moonY, 14, moonX, moonY, 126);
|
|
7313
|
+
moon.addColorStop(0, visuals.moonCore);
|
|
7314
|
+
moon.addColorStop(0.46, visuals.moonHalo);
|
|
7315
|
+
moon.addColorStop(1, "rgba(167, 191, 255, 0)");
|
|
7316
|
+
ctx.fillStyle = moon;
|
|
7317
|
+
ctx.beginPath();
|
|
7318
|
+
ctx.arc(moonX, moonY, 94, 0, Math.PI * 2);
|
|
7319
|
+
ctx.fill();
|
|
7320
|
+
const moonCore = ctx.createRadialGradient(moonX, moonY, 4, moonX, moonY, 28);
|
|
7321
|
+
moonCore.addColorStop(0, "rgba(255, 255, 255, 0.98)");
|
|
7322
|
+
moonCore.addColorStop(1, visuals.moonCore);
|
|
7323
|
+
ctx.fillStyle = moonCore;
|
|
7199
7324
|
ctx.beginPath();
|
|
7200
|
-
ctx.arc(
|
|
7325
|
+
ctx.arc(moonX, moonY, 24, 0, Math.PI * 2);
|
|
7201
7326
|
ctx.fill();
|
|
7202
|
-
const track = ctx.createLinearGradient(
|
|
7203
|
-
track.addColorStop(0,
|
|
7204
|
-
track.addColorStop(0.42,
|
|
7205
|
-
track.addColorStop(1, "rgba(
|
|
7327
|
+
const track = ctx.createLinearGradient(moonX, canvas.height * 0.44, moonX, canvas.height * 0.98);
|
|
7328
|
+
track.addColorStop(0, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.08 + reflectionStrength * 0.12})`));
|
|
7329
|
+
track.addColorStop(0.42, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.04 + reflectionStrength * 0.18})`));
|
|
7330
|
+
track.addColorStop(1, "rgba(192, 214, 255, 0)");
|
|
7206
7331
|
ctx.save();
|
|
7207
7332
|
ctx.globalCompositeOperation = "screen";
|
|
7208
7333
|
ctx.fillStyle = track;
|
|
7209
7334
|
ctx.beginPath();
|
|
7210
|
-
ctx.ellipse(
|
|
7335
|
+
ctx.ellipse(moonX, canvas.height * 0.75, 38 + shadowStrength * 42, canvas.height * 0.24, 0, 0, Math.PI * 2);
|
|
7211
7336
|
ctx.fill();
|
|
7212
7337
|
ctx.restore();
|
|
7338
|
+
const mist = ctx.createLinearGradient(0, canvas.height * 0.5, 0, canvas.height);
|
|
7339
|
+
mist.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
7340
|
+
mist.addColorStop(1, visuals.ambientMist);
|
|
7341
|
+
ctx.fillStyle = mist;
|
|
7342
|
+
ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
|
|
7213
7343
|
if (state.collisionFlash > 0.01) {
|
|
7214
|
-
ctx.fillStyle =
|
|
7344
|
+
ctx.fillStyle = visuals.collisionFlash.replace(
|
|
7345
|
+
/[\d.]+\)$/u,
|
|
7346
|
+
`${clamp(state.collisionFlash * 0.22, 0, 0.26)})`
|
|
7347
|
+
);
|
|
7215
7348
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
7216
7349
|
}
|
|
7217
7350
|
}
|
|
@@ -7310,7 +7443,7 @@ function pushHarborGeometry(camera, viewport, triangles, visuals) {
|
|
|
7310
7443
|
}
|
|
7311
7444
|
}
|
|
7312
7445
|
function renderShipRigging(ctx, ship, camera, viewport) {
|
|
7313
|
-
const transform = { position: ship.position, rotationY: ship.rotationY, scale:
|
|
7446
|
+
const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
|
|
7314
7447
|
const mastBase = transformPoint(vec3(0, 0.38, -0.4), transform);
|
|
7315
7448
|
const mastTop = transformPoint(vec3(0, 3.8, -0.2), transform);
|
|
7316
7449
|
const aftBase = transformPoint(vec3(-0.25, 0.32, -1.9), transform);
|
|
@@ -7414,6 +7547,35 @@ function renderWaterHighlights(ctx, waterBands, camera, viewport) {
|
|
|
7414
7547
|
}
|
|
7415
7548
|
}
|
|
7416
7549
|
}
|
|
7550
|
+
function readPhysicsNumber(physics, key, fallback) {
|
|
7551
|
+
const value = physics?.[key];
|
|
7552
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
7553
|
+
}
|
|
7554
|
+
function getShipMass(ship, shipModel) {
|
|
7555
|
+
const baseMass = readPhysicsNumber(shipModel.physics, "mass", 3200);
|
|
7556
|
+
return baseMass * readVisualNumber(ship.massScale, 1);
|
|
7557
|
+
}
|
|
7558
|
+
function getShipHalfExtents(ship, shipModel) {
|
|
7559
|
+
const physicsHalfExtents = Array.isArray(shipModel.physics.halfExtents) ? shipModel.physics.halfExtents : [1.35, 0.95, 3.9];
|
|
7560
|
+
const scale = SHIP_SCALE * readVisualNumber(ship.collisionRadiusScale, 1);
|
|
7561
|
+
return {
|
|
7562
|
+
x: physicsHalfExtents[0] * scale,
|
|
7563
|
+
y: physicsHalfExtents[1] * scale,
|
|
7564
|
+
z: physicsHalfExtents[2] * scale
|
|
7565
|
+
};
|
|
7566
|
+
}
|
|
7567
|
+
function getShipCollisionRadius(ship, shipModel) {
|
|
7568
|
+
const halfExtents = getShipHalfExtents(ship, shipModel);
|
|
7569
|
+
return Math.max(halfExtents.x * 1.08, halfExtents.z * 0.62);
|
|
7570
|
+
}
|
|
7571
|
+
function getShipInverseMass(ship, shipModel) {
|
|
7572
|
+
return 1 / Math.max(1, getShipMass(ship, shipModel));
|
|
7573
|
+
}
|
|
7574
|
+
function getShipInverseInertia(ship, shipModel) {
|
|
7575
|
+
const radius = getShipCollisionRadius(ship, shipModel);
|
|
7576
|
+
const inertia = getShipMass(ship, shipModel) * radius * radius * 0.72;
|
|
7577
|
+
return 1 / Math.max(1, inertia);
|
|
7578
|
+
}
|
|
7417
7579
|
function spawnSpray(state, point, intensity) {
|
|
7418
7580
|
const count = state.fluidDetail.getSnapshot().currentLevel.config.splashCount;
|
|
7419
7581
|
for (let index = 0; index < count; index += 1) {
|
|
@@ -7426,55 +7588,182 @@ function spawnSpray(state, point, intensity) {
|
|
|
7426
7588
|
});
|
|
7427
7589
|
}
|
|
7428
7590
|
}
|
|
7429
|
-
function
|
|
7591
|
+
function resolveShipRoute(ship, state, radius) {
|
|
7592
|
+
if (typeof ship.routeDirection !== "number") {
|
|
7593
|
+
ship.routeDirection = ship.velocity.x >= 0 ? 1 : -1;
|
|
7594
|
+
}
|
|
7595
|
+
if (ship.position.x > HARBOR_BOUNDS.maxX - radius * 1.1) {
|
|
7596
|
+
ship.routeDirection = -1;
|
|
7597
|
+
} else if (ship.position.x < HARBOR_BOUNDS.minX + radius * 1.1) {
|
|
7598
|
+
ship.routeDirection = 1;
|
|
7599
|
+
}
|
|
7600
|
+
const wander = Math.sin(state.time * 0.22 + readVisualNumber(ship.wanderPhase, 0));
|
|
7601
|
+
const crossCurrent = Math.cos(state.time * 0.31 + readVisualNumber(ship.wanderPhase, 0));
|
|
7602
|
+
const laneCenter = ship.id === "northwind" ? 10.2 + wander * 2.1 + crossCurrent * 0.6 : 7 + wander * 3.3 - crossCurrent * 1.1;
|
|
7603
|
+
const targetX = ship.routeDirection > 0 ? HARBOR_BOUNDS.maxX - radius * 1.7 : HARBOR_BOUNDS.minX + radius * 1.7;
|
|
7604
|
+
return vec3(targetX, 0, clamp(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
|
|
7605
|
+
}
|
|
7606
|
+
function updateShipMotion(state, ship, dt, shipModel) {
|
|
7430
7607
|
const physics = shipModel.physics;
|
|
7431
|
-
const
|
|
7608
|
+
const massScale = Math.max(0.55, readVisualNumber(ship.massScale, 1));
|
|
7609
|
+
const radius = getShipCollisionRadius(ship, shipModel);
|
|
7610
|
+
const waterline = readPhysicsNumber(physics, "waterline", 0.42);
|
|
7611
|
+
const linearDamping = readPhysicsNumber(physics, "linearDamping", 0.04);
|
|
7612
|
+
const angularDamping = readPhysicsNumber(physics, "angularDamping", 0.08);
|
|
7613
|
+
const throttleResponse = readVisualNumber(ship.throttleResponse, 0.58);
|
|
7614
|
+
const rudderResponse = readVisualNumber(ship.rudderResponse, 0.62);
|
|
7615
|
+
const cruiseSpeed = readVisualNumber(ship.cruiseSpeed, 2.4);
|
|
7616
|
+
ship.collisionCooldown = Math.max(0, readVisualNumber(ship.collisionCooldown, 0) - dt);
|
|
7617
|
+
const forward = directionFromYaw(ship.rotationY);
|
|
7618
|
+
const lateral = perpendicularOnWater(forward);
|
|
7619
|
+
const routeTarget = resolveShipRoute(ship, state, radius);
|
|
7620
|
+
const desiredHeading = Math.atan2(routeTarget.x - ship.position.x, routeTarget.z - ship.position.z);
|
|
7621
|
+
const headingError = Math.atan2(
|
|
7622
|
+
Math.sin(desiredHeading - ship.rotationY),
|
|
7623
|
+
Math.cos(desiredHeading - ship.rotationY)
|
|
7624
|
+
);
|
|
7625
|
+
ship.angularVelocity += headingError * rudderResponse * dt * (1.18 / Math.sqrt(massScale)) + Math.sin(state.time * 0.9 + readVisualNumber(ship.wanderPhase, 0)) * dt * 0.04;
|
|
7626
|
+
const waveDirection = resolveWaveDirection(state);
|
|
7627
|
+
const forwardSpeed = dotVec3(ship.velocity, forward);
|
|
7628
|
+
const lateralSpeed = dotVec3(ship.velocity, lateral);
|
|
7629
|
+
const thrust = (cruiseSpeed - forwardSpeed) * throttleResponse;
|
|
7630
|
+
const currentDrift = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.016;
|
|
7631
|
+
const acceleration = addVec3(
|
|
7632
|
+
scaleVec3(forward, thrust),
|
|
7633
|
+
addVec3(
|
|
7634
|
+
scaleVec3(lateral, -lateralSpeed * (1.28 + rudderResponse * 0.4)),
|
|
7635
|
+
scaleVec3(waveDirection, currentDrift / Math.sqrt(massScale))
|
|
7636
|
+
)
|
|
7637
|
+
);
|
|
7638
|
+
ship.velocity = addVec3(ship.velocity, scaleVec3(acceleration, dt));
|
|
7639
|
+
ship.velocity = scaleVec3(
|
|
7640
|
+
ship.velocity,
|
|
7641
|
+
Math.max(0, 1 - linearDamping / Math.pow(massScale, 0.22) * dt)
|
|
7642
|
+
);
|
|
7643
|
+
ship.angularVelocity *= Math.max(
|
|
7644
|
+
0,
|
|
7645
|
+
1 - angularDamping / Math.pow(massScale, 0.15) * dt
|
|
7646
|
+
);
|
|
7647
|
+
ship.rotationY += ship.angularVelocity * dt;
|
|
7648
|
+
ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
|
|
7649
|
+
ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 + waterline;
|
|
7650
|
+
}
|
|
7651
|
+
function resolveBoundaryCollision(ship, state, shipModel) {
|
|
7652
|
+
const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.56;
|
|
7653
|
+
const radius = getShipCollisionRadius(ship, shipModel);
|
|
7654
|
+
const boundaries = [
|
|
7655
|
+
{ axis: "x", min: HARBOR_BOUNDS.minX + radius, max: HARBOR_BOUNDS.maxX - radius, normalMin: vec3(1, 0, 0), normalMax: vec3(-1, 0, 0) },
|
|
7656
|
+
{ axis: "z", min: HARBOR_BOUNDS.minZ + radius, max: HARBOR_BOUNDS.maxZ - radius, normalMin: vec3(0, 0, 1), normalMax: vec3(0, 0, -1) }
|
|
7657
|
+
];
|
|
7658
|
+
for (const boundary of boundaries) {
|
|
7659
|
+
if (ship.position[boundary.axis] < boundary.min) {
|
|
7660
|
+
ship.position[boundary.axis] = boundary.min;
|
|
7661
|
+
const normal = boundary.normalMin;
|
|
7662
|
+
const speedIntoWall = dotVec3(ship.velocity, normal);
|
|
7663
|
+
if (speedIntoWall < 0) {
|
|
7664
|
+
ship.velocity = subVec3(
|
|
7665
|
+
ship.velocity,
|
|
7666
|
+
scaleVec3(normal, (1 + restitution) * speedIntoWall)
|
|
7667
|
+
);
|
|
7668
|
+
const tangent = vec3(-normal.z, 0, normal.x);
|
|
7669
|
+
const tangentSpeed = dotVec3(ship.velocity, tangent);
|
|
7670
|
+
ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
|
|
7671
|
+
ship.angularVelocity += tangentSpeed * 4e-3;
|
|
7672
|
+
}
|
|
7673
|
+
} else if (ship.position[boundary.axis] > boundary.max) {
|
|
7674
|
+
ship.position[boundary.axis] = boundary.max;
|
|
7675
|
+
const normal = boundary.normalMax;
|
|
7676
|
+
const speedIntoWall = dotVec3(ship.velocity, normal);
|
|
7677
|
+
if (speedIntoWall < 0) {
|
|
7678
|
+
ship.velocity = subVec3(
|
|
7679
|
+
ship.velocity,
|
|
7680
|
+
scaleVec3(normal, (1 + restitution) * speedIntoWall)
|
|
7681
|
+
);
|
|
7682
|
+
const tangent = vec3(-normal.z, 0, normal.x);
|
|
7683
|
+
const tangentSpeed = dotVec3(ship.velocity, tangent);
|
|
7684
|
+
ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
|
|
7685
|
+
ship.angularVelocity += tangentSpeed * 4e-3;
|
|
7686
|
+
}
|
|
7687
|
+
}
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
function resolveShipCollision(state, a, b, shipModel) {
|
|
7691
|
+
const delta = subVec3(b.position, a.position);
|
|
7692
|
+
const radiusA = getShipCollisionRadius(a, shipModel);
|
|
7693
|
+
const radiusB = getShipCollisionRadius(b, shipModel);
|
|
7694
|
+
const distance = Math.hypot(delta.x, delta.z);
|
|
7695
|
+
const minDistance = radiusA + radiusB;
|
|
7696
|
+
if (distance >= minDistance) {
|
|
7697
|
+
return false;
|
|
7698
|
+
}
|
|
7699
|
+
const normal = distance > 1e-4 ? normalizeVec3(vec3(delta.x / distance, 0, delta.z / distance)) : normalizeVec3(vec3(Math.cos(state.time * 5.2), 0, Math.sin(state.time * 4.8)));
|
|
7700
|
+
const tangent = vec3(-normal.z, 0, normal.x);
|
|
7701
|
+
const penetration = minDistance - distance;
|
|
7702
|
+
const invMassA = getShipInverseMass(a, shipModel);
|
|
7703
|
+
const invMassB = getShipInverseMass(b, shipModel);
|
|
7704
|
+
const invMassSum = invMassA + invMassB;
|
|
7705
|
+
const correction = scaleVec3(normal, penetration / Math.max(1e-4, invMassSum) * 0.72);
|
|
7706
|
+
a.position = subVec3(a.position, scaleVec3(correction, invMassA));
|
|
7707
|
+
b.position = addVec3(b.position, scaleVec3(correction, invMassB));
|
|
7708
|
+
const relativeVelocity = subVec3(b.velocity, a.velocity);
|
|
7709
|
+
const velocityAlongNormal = dotVec3(relativeVelocity, normal);
|
|
7710
|
+
const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.88;
|
|
7711
|
+
if (velocityAlongNormal < 0) {
|
|
7712
|
+
const impulseMagnitude = -(1 + restitution) * velocityAlongNormal / Math.max(1e-4, invMassSum);
|
|
7713
|
+
const impulse = scaleVec3(normal, impulseMagnitude);
|
|
7714
|
+
a.velocity = subVec3(a.velocity, scaleVec3(impulse, invMassA));
|
|
7715
|
+
b.velocity = addVec3(b.velocity, scaleVec3(impulse, invMassB));
|
|
7716
|
+
const tangentSpeed = dotVec3(relativeVelocity, tangent);
|
|
7717
|
+
const frictionMagnitude = clamp(
|
|
7718
|
+
-tangentSpeed / Math.max(1e-4, invMassSum),
|
|
7719
|
+
-impulseMagnitude * 0.16,
|
|
7720
|
+
impulseMagnitude * 0.16
|
|
7721
|
+
);
|
|
7722
|
+
const frictionImpulse = scaleVec3(tangent, frictionMagnitude);
|
|
7723
|
+
a.velocity = subVec3(a.velocity, scaleVec3(frictionImpulse, invMassA));
|
|
7724
|
+
b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
|
|
7725
|
+
a.angularVelocity -= tangentSpeed * radiusA * getShipInverseInertia(a, shipModel) * 0.2 + impulseMagnitude * 24e-5;
|
|
7726
|
+
b.angularVelocity += tangentSpeed * radiusB * getShipInverseInertia(b, shipModel) * 0.2 + impulseMagnitude * 24e-5;
|
|
7727
|
+
const impactSpeed = Math.abs(velocityAlongNormal);
|
|
7728
|
+
if (impactSpeed > 0.18 && Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0) {
|
|
7729
|
+
const contactPoint = vec3(
|
|
7730
|
+
(a.position.x + b.position.x) * 0.5,
|
|
7731
|
+
(a.position.y + b.position.y) * 0.5 + 0.14,
|
|
7732
|
+
(a.position.z + b.position.z) * 0.5
|
|
7733
|
+
);
|
|
7734
|
+
spawnSpray(state, contactPoint, impactSpeed * 2.4 + penetration * 8);
|
|
7735
|
+
state.waveImpulses.push({
|
|
7736
|
+
x: contactPoint.x,
|
|
7737
|
+
z: contactPoint.z,
|
|
7738
|
+
strength: clamp(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
|
|
7739
|
+
radius: 0.9 + penetration * 1.4,
|
|
7740
|
+
life: 1
|
|
7741
|
+
});
|
|
7742
|
+
state.collisionCount += 1;
|
|
7743
|
+
state.collisionFlash = Math.max(
|
|
7744
|
+
state.collisionFlash,
|
|
7745
|
+
clamp(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
|
|
7746
|
+
);
|
|
7747
|
+
a.collisionCooldown = 0.2;
|
|
7748
|
+
b.collisionCooldown = 0.2;
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
state.contactCount += 1;
|
|
7752
|
+
return true;
|
|
7753
|
+
}
|
|
7754
|
+
function updateShips(state, dt, shipModel) {
|
|
7432
7755
|
let collided = false;
|
|
7433
7756
|
state.contactCount = 0;
|
|
7434
7757
|
for (const ship of state.ships) {
|
|
7435
|
-
|
|
7436
|
-
ship
|
|
7437
|
-
ship.velocity = scaleVec3(ship.velocity, 1 - (physics.linearDamping ?? 0.04) * dt);
|
|
7438
|
-
ship.angularVelocity *= 1 - (physics.angularDamping ?? 0.08) * dt;
|
|
7439
|
-
ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.22 + (physics.waterline ?? 0.42);
|
|
7440
|
-
if (Math.abs(ship.position.x) > 10) {
|
|
7441
|
-
ship.velocity.x *= -1;
|
|
7442
|
-
ship.angularVelocity *= -1;
|
|
7443
|
-
}
|
|
7444
|
-
if (ship.position.z < 2 || ship.position.z > 16) {
|
|
7445
|
-
ship.velocity.z *= -1;
|
|
7446
|
-
ship.angularVelocity *= -1;
|
|
7447
|
-
}
|
|
7758
|
+
updateShipMotion(state, ship, dt, shipModel);
|
|
7759
|
+
resolveBoundaryCollision(ship, state, shipModel);
|
|
7448
7760
|
}
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
const overlapZ = Math.abs(dz) < halfExtents[2] * 0.8;
|
|
7454
|
-
if (overlapX && overlapZ) {
|
|
7455
|
-
const restitution = physics.restitution ?? 0.22;
|
|
7456
|
-
const swapX = a.velocity.x;
|
|
7457
|
-
const swapZ = a.velocity.z;
|
|
7458
|
-
a.velocity.x = -b.velocity.x * (0.86 + restitution);
|
|
7459
|
-
a.velocity.z = -b.velocity.z * (0.82 + restitution);
|
|
7460
|
-
b.velocity.x = -swapX * (0.86 + restitution);
|
|
7461
|
-
b.velocity.z = -swapZ * (0.82 + restitution);
|
|
7462
|
-
a.angularVelocity += 0.55;
|
|
7463
|
-
b.angularVelocity -= 0.55;
|
|
7464
|
-
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);
|
|
7465
|
-
spawnSpray(state, contactPoint, Math.abs(dx) + Math.abs(dz));
|
|
7466
|
-
state.waveImpulses.push({
|
|
7467
|
-
x: contactPoint.x,
|
|
7468
|
-
z: contactPoint.z,
|
|
7469
|
-
strength: Math.min(1.4, 0.2 + (Math.abs(dx) + Math.abs(dz)) * 0.18),
|
|
7470
|
-
radius: 0.8,
|
|
7471
|
-
life: 1
|
|
7472
|
-
});
|
|
7473
|
-
state.collisionCount += 1;
|
|
7474
|
-
state.contactCount = 1;
|
|
7475
|
-
collided = true;
|
|
7761
|
+
for (let index = 0; index < state.ships.length; index += 1) {
|
|
7762
|
+
for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
|
|
7763
|
+
collided = resolveShipCollision(state, state.ships[index], state.ships[otherIndex], shipModel) || collided;
|
|
7764
|
+
}
|
|
7476
7765
|
}
|
|
7477
|
-
state.collisionFlash = collided ?
|
|
7766
|
+
state.collisionFlash = collided ? Math.max(0.12, state.collisionFlash) : Math.max(0, state.collisionFlash - dt * 1.3);
|
|
7478
7767
|
}
|
|
7479
7768
|
function updateWaveImpulses(state, dt) {
|
|
7480
7769
|
state.waveImpulses = state.waveImpulses.map((impulse) => ({
|
|
@@ -7587,7 +7876,7 @@ function renderFlagPole(ctx, camera, viewport) {
|
|
|
7587
7876
|
function renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength) {
|
|
7588
7877
|
const bounds = shipModel.bounds;
|
|
7589
7878
|
const keelY = (shipModel.physics.waterline ?? 0.42) - 0.28;
|
|
7590
|
-
const transform = { position: ship.position, rotationY: ship.rotationY, scale:
|
|
7879
|
+
const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
|
|
7591
7880
|
const hullCorners = [
|
|
7592
7881
|
vec3(bounds.min[0], keelY, bounds.min[2]),
|
|
7593
7882
|
vec3(bounds.max[0], keelY, bounds.min[2]),
|
|
@@ -7613,6 +7902,97 @@ function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength
|
|
|
7613
7902
|
blur: 12 + shadowStrength * 20
|
|
7614
7903
|
});
|
|
7615
7904
|
}
|
|
7905
|
+
function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glowScale, reflectionStrength = 0, state = null) {
|
|
7906
|
+
const projected = projectPoint(point, camera, viewport);
|
|
7907
|
+
if (!projected) {
|
|
7908
|
+
return;
|
|
7909
|
+
}
|
|
7910
|
+
const radius = clamp(1 / projected.depth * 420 * glowScale, 4, 34);
|
|
7911
|
+
const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
|
|
7912
|
+
halo.addColorStop(0, colorToRgba(coreColor, 0.98));
|
|
7913
|
+
halo.addColorStop(0.5, colorToRgba(glowColor, 0.42));
|
|
7914
|
+
halo.addColorStop(1, colorToRgba(glowColor, 0));
|
|
7915
|
+
ctx.save();
|
|
7916
|
+
ctx.globalCompositeOperation = "screen";
|
|
7917
|
+
ctx.fillStyle = halo;
|
|
7918
|
+
ctx.beginPath();
|
|
7919
|
+
ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
|
|
7920
|
+
ctx.fill();
|
|
7921
|
+
ctx.restore();
|
|
7922
|
+
ctx.fillStyle = colorToRgba(coreColor, 0.98);
|
|
7923
|
+
ctx.beginPath();
|
|
7924
|
+
ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
|
|
7925
|
+
ctx.fill();
|
|
7926
|
+
if (state && reflectionStrength > 0) {
|
|
7927
|
+
const waterline = sampleWave(state, point.x, point.z, state.time) * 0.22;
|
|
7928
|
+
const reflectedPoint = vec3(point.x, waterline - (point.y - waterline) * 0.58, point.z + 0.08);
|
|
7929
|
+
const reflected = projectPoint(reflectedPoint, camera, viewport);
|
|
7930
|
+
if (reflected) {
|
|
7931
|
+
const reflectionRadius = radius * 0.72;
|
|
7932
|
+
const glow = ctx.createRadialGradient(
|
|
7933
|
+
reflected.x,
|
|
7934
|
+
reflected.y,
|
|
7935
|
+
reflectionRadius * 0.1,
|
|
7936
|
+
reflected.x,
|
|
7937
|
+
reflected.y,
|
|
7938
|
+
reflectionRadius
|
|
7939
|
+
);
|
|
7940
|
+
glow.addColorStop(0, colorToRgba(coreColor, reflectionStrength * 0.34));
|
|
7941
|
+
glow.addColorStop(1, colorToRgba(glowColor, 0));
|
|
7942
|
+
ctx.save();
|
|
7943
|
+
ctx.globalCompositeOperation = "screen";
|
|
7944
|
+
ctx.fillStyle = glow;
|
|
7945
|
+
ctx.beginPath();
|
|
7946
|
+
ctx.ellipse(
|
|
7947
|
+
reflected.x,
|
|
7948
|
+
reflected.y,
|
|
7949
|
+
reflectionRadius * 0.34,
|
|
7950
|
+
reflectionRadius,
|
|
7951
|
+
0,
|
|
7952
|
+
0,
|
|
7953
|
+
Math.PI * 2
|
|
7954
|
+
);
|
|
7955
|
+
ctx.fill();
|
|
7956
|
+
ctx.restore();
|
|
7957
|
+
}
|
|
7958
|
+
}
|
|
7959
|
+
}
|
|
7960
|
+
function renderShipLanterns(ctx, ship, state, camera, viewport, visuals) {
|
|
7961
|
+
const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
|
|
7962
|
+
const strength = readVisualNumber(ship.lanternStrength, 1);
|
|
7963
|
+
for (const lantern of lanterns) {
|
|
7964
|
+
const position = transformPoint(
|
|
7965
|
+
vec3(lantern.x, lantern.y, lantern.z),
|
|
7966
|
+
{ position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
|
|
7967
|
+
);
|
|
7968
|
+
renderGlowLight(
|
|
7969
|
+
ctx,
|
|
7970
|
+
position,
|
|
7971
|
+
camera,
|
|
7972
|
+
viewport,
|
|
7973
|
+
visuals.lanternCore,
|
|
7974
|
+
visuals.lanternGlow,
|
|
7975
|
+
lantern.glow * strength,
|
|
7976
|
+
visuals.lanternReflectionStrength,
|
|
7977
|
+
state
|
|
7978
|
+
);
|
|
7979
|
+
}
|
|
7980
|
+
}
|
|
7981
|
+
function renderHarborTorches(ctx, state, camera, viewport, visuals) {
|
|
7982
|
+
for (const torch of HARBOR_TORCHES) {
|
|
7983
|
+
renderGlowLight(
|
|
7984
|
+
ctx,
|
|
7985
|
+
vec3(torch.x, torch.y, torch.z),
|
|
7986
|
+
camera,
|
|
7987
|
+
viewport,
|
|
7988
|
+
visuals.torchCore,
|
|
7989
|
+
visuals.torchGlow,
|
|
7990
|
+
torch.glow,
|
|
7991
|
+
visuals.lanternReflectionStrength * 0.55,
|
|
7992
|
+
state
|
|
7993
|
+
);
|
|
7994
|
+
}
|
|
7995
|
+
}
|
|
7616
7996
|
function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
7617
7997
|
const viewport = { width: canvas.width, height: canvas.height };
|
|
7618
7998
|
const camera = buildCamera(state, canvas);
|
|
@@ -7622,7 +8002,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
7622
8002
|
importance: state.focus === "lighting" ? "critical" : "high"
|
|
7623
8003
|
});
|
|
7624
8004
|
const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
|
|
7625
|
-
const lightDir = normalizeVec3(vec3(-0.
|
|
8005
|
+
const lightDir = normalizeVec3(vec3(-0.22, 0.94, -0.31));
|
|
7626
8006
|
const lightingSnapshot = state.lightingDetail.getSnapshot();
|
|
7627
8007
|
const visuals = resolveVisualConfig(
|
|
7628
8008
|
nearLighting,
|
|
@@ -7696,7 +8076,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
7696
8076
|
for (const ship of state.ships) {
|
|
7697
8077
|
buildTrianglesFromMesh(
|
|
7698
8078
|
shipModel,
|
|
7699
|
-
{ position: ship.position, rotationY: ship.rotationY, scale:
|
|
8079
|
+
{ position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
|
|
7700
8080
|
ship.tint,
|
|
7701
8081
|
camera,
|
|
7702
8082
|
viewport,
|
|
@@ -7710,10 +8090,12 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
7710
8090
|
renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
|
|
7711
8091
|
drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength);
|
|
7712
8092
|
renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
|
|
8093
|
+
renderHarborTorches(ctx, state, camera, viewport, visuals);
|
|
7713
8094
|
renderFlagPole(ctx, camera, viewport);
|
|
7714
8095
|
renderClothAccent(ctx, cloth, camera, viewport);
|
|
7715
8096
|
for (const ship of state.ships) {
|
|
7716
8097
|
renderShipRigging(ctx, ship, camera, viewport);
|
|
8098
|
+
renderShipLanterns(ctx, ship, state, camera, viewport, visuals);
|
|
7717
8099
|
}
|
|
7718
8100
|
renderSprays(ctx, state.sprays, camera, viewport);
|
|
7719
8101
|
const debugSnapshot = state.debugSession.getSnapshot();
|
|
@@ -7725,8 +8107,10 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
7725
8107
|
const sceneMetrics = [
|
|
7726
8108
|
`focus: ${state.focus}`,
|
|
7727
8109
|
`ships: ${state.ships.length} active GLTF hulls`,
|
|
8110
|
+
`moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.length * SHIP_LANTERNS.length} warm deck and harbor lights`,
|
|
7728
8111
|
`physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
|
|
7729
|
-
`physics contacts: ${state.
|
|
8112
|
+
`physics contacts: ${state.contactCount}`,
|
|
8113
|
+
`mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, shipModel) / 1e3).toFixed(1)}t`).join(" \xB7 ")}`,
|
|
7730
8114
|
`cloth band: ${cloth.band} -> ${cloth.representation.output}`,
|
|
7731
8115
|
`fluid near band: ${water.bandMeshes[0].representation.output}`,
|
|
7732
8116
|
`lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`
|
|
@@ -7749,8 +8133,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
7749
8133
|
];
|
|
7750
8134
|
const sceneNotes = state.focus === "physics" ? [
|
|
7751
8135
|
"Stable world snapshots are taken after the authoritative rigid-body commit and before visual follow-up work.",
|
|
7752
|
-
"The ships collide
|
|
7753
|
-
"
|
|
8136
|
+
"The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
|
|
8137
|
+
"Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water."
|
|
7754
8138
|
] : SCENE_NOTES;
|
|
7755
8139
|
const custom = state.demoDescription ?? null;
|
|
7756
8140
|
setListContent(
|
|
@@ -7767,7 +8151,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
|
|
|
7767
8151
|
);
|
|
7768
8152
|
setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
|
|
7769
8153
|
dom.status.textContent = typeof custom?.status === "string" ? custom.status : `3D scene live \xB7 ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
|
|
7770
|
-
dom.details.textContent = typeof custom?.details === "string" ? custom.details : state.focus === "physics" ? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver;
|
|
8154
|
+
dom.details.textContent = typeof custom?.details === "string" ? custom.details : state.focus === "physics" ? `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.` : `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}.`;
|
|
7771
8155
|
}
|
|
7772
8156
|
function updateSceneState(state, dt, shipModel) {
|
|
7773
8157
|
updateShips(state, dt, shipModel);
|
|
@@ -7786,7 +8170,9 @@ function syncTextState(state, shipModel) {
|
|
|
7786
8170
|
y: Number(ship.position.y.toFixed(2)),
|
|
7787
8171
|
z: Number(ship.position.z.toFixed(2)),
|
|
7788
8172
|
vx: Number(ship.velocity.x.toFixed(2)),
|
|
7789
|
-
vz: Number(ship.velocity.z.toFixed(2))
|
|
8173
|
+
vz: Number(ship.velocity.z.toFixed(2)),
|
|
8174
|
+
massKg: Math.round(getShipMass(ship, shipModel)),
|
|
8175
|
+
lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0
|
|
7790
8176
|
})),
|
|
7791
8177
|
shipPhysics: shipModel.physics,
|
|
7792
8178
|
sprays: state.sprays.length,
|
|
@@ -7814,6 +8200,7 @@ function syncTextState(state, shipModel) {
|
|
|
7814
8200
|
async function mountGpuShowcase(options = {}) {
|
|
7815
8201
|
injectStyles();
|
|
7816
8202
|
const root = options.root ?? document.body;
|
|
8203
|
+
root.classList?.add?.(ROOT_CLASS);
|
|
7817
8204
|
const previousMarkup = root.innerHTML;
|
|
7818
8205
|
const previousRenderGameToText = window.render_game_to_text;
|
|
7819
8206
|
const previousAdvanceTime = window.advanceTime;
|
|
@@ -7833,8 +8220,11 @@ async function mountGpuShowcase(options = {}) {
|
|
|
7833
8220
|
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
7834
8221
|
syncTextState(state, shipModel);
|
|
7835
8222
|
const ctx = dom.canvas.getContext("2d");
|
|
8223
|
+
if (!ctx) {
|
|
8224
|
+
throw new Error("2D canvas context is required for the shared showcase.");
|
|
8225
|
+
}
|
|
8226
|
+
let animationFrameId = 0;
|
|
7836
8227
|
let destroyed = false;
|
|
7837
|
-
let frameHandle = null;
|
|
7838
8228
|
const renderFrame = (nowMs) => {
|
|
7839
8229
|
if (destroyed) {
|
|
7840
8230
|
return;
|
|
@@ -7855,7 +8245,7 @@ async function mountGpuShowcase(options = {}) {
|
|
|
7855
8245
|
state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
|
|
7856
8246
|
renderScene(ctx, dom.canvas, state, shipModel, dom);
|
|
7857
8247
|
syncTextState(state, shipModel);
|
|
7858
|
-
|
|
8248
|
+
animationFrameId = requestAnimationFrame(renderFrame);
|
|
7859
8249
|
};
|
|
7860
8250
|
const handlePauseClick = () => {
|
|
7861
8251
|
state.paused = !state.paused;
|
|
@@ -7874,34 +8264,43 @@ async function mountGpuShowcase(options = {}) {
|
|
|
7874
8264
|
dom.pauseButton.addEventListener("click", handlePauseClick);
|
|
7875
8265
|
dom.stressToggle.addEventListener("change", handleStressChange);
|
|
7876
8266
|
dom.focusMode.addEventListener("change", handleFocusChange);
|
|
7877
|
-
|
|
8267
|
+
animationFrameId = requestAnimationFrame(renderFrame);
|
|
8268
|
+
const destroy = () => {
|
|
8269
|
+
if (destroyed) {
|
|
8270
|
+
return;
|
|
8271
|
+
}
|
|
8272
|
+
destroyed = true;
|
|
8273
|
+
if (animationFrameId) {
|
|
8274
|
+
cancelAnimationFrame(animationFrameId);
|
|
8275
|
+
}
|
|
8276
|
+
dom.pauseButton.removeEventListener("click", handlePauseClick);
|
|
8277
|
+
dom.stressToggle.removeEventListener("change", handleStressChange);
|
|
8278
|
+
dom.focusMode.removeEventListener("change", handleFocusChange);
|
|
8279
|
+
try {
|
|
8280
|
+
if (typeof options.destroyState === "function") {
|
|
8281
|
+
options.destroyState(state.packageState);
|
|
8282
|
+
}
|
|
8283
|
+
} finally {
|
|
8284
|
+
state.packageState = void 0;
|
|
8285
|
+
}
|
|
8286
|
+
root.classList?.remove?.(ROOT_CLASS);
|
|
8287
|
+
root.innerHTML = previousMarkup;
|
|
8288
|
+
if (typeof previousRenderGameToText === "function") {
|
|
8289
|
+
window.render_game_to_text = previousRenderGameToText;
|
|
8290
|
+
} else {
|
|
8291
|
+
delete window.render_game_to_text;
|
|
8292
|
+
}
|
|
8293
|
+
if (typeof previousAdvanceTime === "function") {
|
|
8294
|
+
window.advanceTime = previousAdvanceTime;
|
|
8295
|
+
} else {
|
|
8296
|
+
delete window.advanceTime;
|
|
8297
|
+
}
|
|
8298
|
+
};
|
|
7878
8299
|
return {
|
|
7879
8300
|
state,
|
|
7880
8301
|
shipModel,
|
|
7881
8302
|
canvas: dom.canvas,
|
|
7882
|
-
destroy
|
|
7883
|
-
if (destroyed) {
|
|
7884
|
-
return;
|
|
7885
|
-
}
|
|
7886
|
-
destroyed = true;
|
|
7887
|
-
if (frameHandle != null) {
|
|
7888
|
-
cancelAnimationFrame(frameHandle);
|
|
7889
|
-
}
|
|
7890
|
-
dom.pauseButton.removeEventListener("click", handlePauseClick);
|
|
7891
|
-
dom.stressToggle.removeEventListener("change", handleStressChange);
|
|
7892
|
-
dom.focusMode.removeEventListener("change", handleFocusChange);
|
|
7893
|
-
root.innerHTML = previousMarkup;
|
|
7894
|
-
if (typeof previousRenderGameToText === "function") {
|
|
7895
|
-
window.render_game_to_text = previousRenderGameToText;
|
|
7896
|
-
} else {
|
|
7897
|
-
delete window.render_game_to_text;
|
|
7898
|
-
}
|
|
7899
|
-
if (typeof previousAdvanceTime === "function") {
|
|
7900
|
-
window.advanceTime = previousAdvanceTime;
|
|
7901
|
-
} else {
|
|
7902
|
-
delete window.advanceTime;
|
|
7903
|
-
}
|
|
7904
|
-
}
|
|
8303
|
+
destroy
|
|
7905
8304
|
};
|
|
7906
8305
|
}
|
|
7907
8306
|
function updatePhysicsSnapshot(state, shipModel) {
|
|
@@ -7915,7 +8314,7 @@ function updatePhysicsSnapshot(state, shipModel) {
|
|
|
7915
8314
|
animationInputRevision: state.frame,
|
|
7916
8315
|
bodyCount: state.ships.length + 2,
|
|
7917
8316
|
dynamicBodyCount: state.ships.length,
|
|
7918
|
-
contactCount: state.
|
|
8317
|
+
contactCount: state.contactCount,
|
|
7919
8318
|
metadata: {
|
|
7920
8319
|
collisionCount: state.collisionCount,
|
|
7921
8320
|
contactCount: state.contactCount,
|
|
@@ -7924,7 +8323,7 @@ function updatePhysicsSnapshot(state, shipModel) {
|
|
|
7924
8323
|
}
|
|
7925
8324
|
});
|
|
7926
8325
|
}
|
|
7927
|
-
var STYLE_ID, DEFAULT_TITLE, DEFAULT_SUBTITLE, CAMERA_PRESETS, showcaseFocusModes, SCENE_NOTES, UNIT_BOX_MESH;
|
|
8326
|
+
var STYLE_ID, ROOT_CLASS, DEFAULT_TITLE, DEFAULT_SUBTITLE, SHIP_SCALE, HARBOR_BOUNDS, CAMERA_PRESETS, showcaseFocusModes, SCENE_NOTES, UNIT_BOX_MESH, SHIP_LANTERNS, HARBOR_TORCHES;
|
|
7928
8327
|
var init_showcase_runtime = __esm({
|
|
7929
8328
|
"src/showcase-runtime.js"() {
|
|
7930
8329
|
init_dist();
|
|
@@ -7936,8 +8335,16 @@ var init_showcase_runtime = __esm({
|
|
|
7936
8335
|
init_asset_url();
|
|
7937
8336
|
init_gltf_loader();
|
|
7938
8337
|
STYLE_ID = "plasius-shared-3d-showcase-style";
|
|
8338
|
+
ROOT_CLASS = "plasius-showcase-root";
|
|
7939
8339
|
DEFAULT_TITLE = "Flag by the Sea";
|
|
7940
8340
|
DEFAULT_SUBTITLE = "Shared 3D validation scene using GLTF ships, cloth, fluid continuity, adaptive performance, and telemetry.";
|
|
8341
|
+
SHIP_SCALE = 1.1;
|
|
8342
|
+
HARBOR_BOUNDS = Object.freeze({
|
|
8343
|
+
minX: -11.2,
|
|
8344
|
+
maxX: 11.2,
|
|
8345
|
+
minZ: 1.8,
|
|
8346
|
+
maxZ: 17.2
|
|
8347
|
+
});
|
|
7941
8348
|
CAMERA_PRESETS = Object.freeze({
|
|
7942
8349
|
integrated: Object.freeze({ yaw: -0.55, pitch: 0.34, distance: 27, target: [0, 2.2, 0] }),
|
|
7943
8350
|
lighting: Object.freeze({ yaw: -0.28, pitch: 0.28, distance: 23, target: [0, 2.8, 0] }),
|
|
@@ -7949,10 +8356,10 @@ var init_showcase_runtime = __esm({
|
|
|
7949
8356
|
});
|
|
7950
8357
|
showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
|
|
7951
8358
|
SCENE_NOTES = Object.freeze([
|
|
7952
|
-
"Ships are loaded from a GLTF asset and carry
|
|
7953
|
-
"
|
|
7954
|
-
"Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands.",
|
|
7955
|
-
"Performance pressure reduces visual detail before authoritative collision motion is touched."
|
|
8359
|
+
"Ships are loaded from a GLTF asset and carry mass, damping, restitution, and hull extents from node extras.",
|
|
8360
|
+
"Moonlight sets the cold ambient read while deck lanterns and harbor torches provide warm local contrast.",
|
|
8361
|
+
"Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands even in the darker night palette.",
|
|
8362
|
+
"Performance pressure reduces visual detail before mass-weighted authoritative collision motion is touched."
|
|
7956
8363
|
]);
|
|
7957
8364
|
UNIT_BOX_MESH = Object.freeze({
|
|
7958
8365
|
positions: Object.freeze([
|
|
@@ -8020,6 +8427,17 @@ var init_showcase_runtime = __esm({
|
|
|
8020
8427
|
0
|
|
8021
8428
|
])
|
|
8022
8429
|
});
|
|
8430
|
+
SHIP_LANTERNS = Object.freeze([
|
|
8431
|
+
Object.freeze({ x: 0.94, y: 1.54, z: 2.52, glow: 1 }),
|
|
8432
|
+
Object.freeze({ x: -0.9, y: 1.58, z: 2.44, glow: 0.92 }),
|
|
8433
|
+
Object.freeze({ x: 0.62, y: 1.42, z: -2.18, glow: 0.88 }),
|
|
8434
|
+
Object.freeze({ x: -0.58, y: 1.46, z: -2.04, glow: 0.84 })
|
|
8435
|
+
]);
|
|
8436
|
+
HARBOR_TORCHES = Object.freeze([
|
|
8437
|
+
Object.freeze({ x: -5.2, y: 1.25, z: 1.36, glow: 1.1 }),
|
|
8438
|
+
Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
|
|
8439
|
+
Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 })
|
|
8440
|
+
]);
|
|
8023
8441
|
}
|
|
8024
8442
|
});
|
|
8025
8443
|
|