@plasius/gpu-shared 0.1.10 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +59 -2
  3. package/assets/brigantine.gltf +549 -24
  4. package/assets/cutter.gltf +538 -0
  5. package/assets/harbor-dock.gltf +680 -0
  6. package/assets/lighthouse.gltf +604 -0
  7. package/dist/chunk-2FIFSBB4.js +74 -0
  8. package/dist/chunk-2FIFSBB4.js.map +1 -0
  9. package/dist/chunk-DABW627O.js +113 -0
  10. package/dist/chunk-DABW627O.js.map +1 -0
  11. package/dist/chunk-DQX4DXBR.js +369 -0
  12. package/dist/chunk-DQX4DXBR.js.map +1 -0
  13. package/dist/chunk-NCPJWLX3.js +17 -0
  14. package/dist/chunk-NCPJWLX3.js.map +1 -0
  15. package/dist/gltf-loader-WAM23F37.js +9 -0
  16. package/dist/index.cjs +1265 -281
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.js +19 -6
  19. package/dist/index.js.map +1 -1
  20. package/dist/showcase-inline-assets-B7U7VX5H.js +7 -0
  21. package/dist/{showcase-runtime-55OVDYWT.js → showcase-runtime-PN7N3FZY.js} +818 -239
  22. package/dist/showcase-runtime-PN7N3FZY.js.map +1 -0
  23. package/package.json +15 -1
  24. package/src/asset-url.js +62 -11
  25. package/src/feature-flags.js +1 -0
  26. package/src/gltf-loader.js +322 -32
  27. package/src/i18n.js +71 -0
  28. package/src/index.d.ts +115 -1
  29. package/src/index.js +9 -1
  30. package/src/showcase-inline-assets.js +3 -0
  31. package/src/showcase-runtime.js +924 -190
  32. package/src/translations/en-GB.js +55 -0
  33. package/dist/chunk-DGUM43GV.js +0 -11
  34. package/dist/chunk-OTCJ3VOK.js +0 -35
  35. package/dist/chunk-OTCJ3VOK.js.map +0 -1
  36. package/dist/chunk-QBMXJ3V2.js +0 -142
  37. package/dist/chunk-QBMXJ3V2.js.map +0 -1
  38. package/dist/gltf-loader-LKALCZAV.js +0 -8
  39. package/dist/showcase-runtime-55OVDYWT.js.map +0 -1
  40. /package/dist/{chunk-DGUM43GV.js.map → gltf-loader-WAM23F37.js.map} +0 -0
  41. /package/dist/{gltf-loader-LKALCZAV.js.map → showcase-inline-assets-B7U7VX5H.js.map} +0 -0
@@ -1,12 +1,16 @@
1
1
  import {
2
- resolveShowcaseAssetUrl
3
- } from "./chunk-OTCJ3VOK.js";
2
+ createGpuSharedTranslator,
3
+ gpuSharedTranslationKeys
4
+ } from "./chunk-DABW627O.js";
4
5
  import {
5
6
  loadGltfModel
6
- } from "./chunk-QBMXJ3V2.js";
7
+ } from "./chunk-DQX4DXBR.js";
8
+ import {
9
+ resolveShowcaseAssetUrl
10
+ } from "./chunk-2FIFSBB4.js";
7
11
  import {
8
12
  __require
9
- } from "./chunk-DGUM43GV.js";
13
+ } from "./chunk-NCPJWLX3.js";
10
14
 
11
15
  // node_modules/@plasius/gpu-cloth/dist/index.js
12
16
  var clothProfileNames = ["interactive", "cinematic"];
@@ -2134,6 +2138,16 @@ var lightingProfiles = Object.freeze(
2134
2138
  );
2135
2139
  var lightingProfileNames = Object.freeze(Object.keys(lightingProfiles));
2136
2140
  var defaultLightingProfile = "realtime";
2141
+ var lightingProfileModeOrder = Object.freeze([
2142
+ "realtime",
2143
+ "hybrid",
2144
+ "reference"
2145
+ ]);
2146
+ var defaultAdaptiveLightingProfilePolicy = Object.freeze({
2147
+ preferredProfile: "reference",
2148
+ minimumFrameRate: 30,
2149
+ sampleWindowSize: 4
2150
+ });
2137
2151
  var lightingDistanceBands = Object.freeze([
2138
2152
  "near",
2139
2153
  "mid",
@@ -2261,6 +2275,11 @@ function createLightingBandPlan(options = {}) {
2261
2275
  bands
2262
2276
  });
2263
2277
  }
2278
+ var lightingProfileModeEstimatedCostMs = Object.freeze({
2279
+ realtime: 4.5,
2280
+ hybrid: 7.5,
2281
+ reference: 12.5
2282
+ });
2264
2283
  function buildWorkerBudgetLevels(jobType, queueClass, presets) {
2265
2284
  return Object.freeze([
2266
2285
  Object.freeze({
@@ -6107,11 +6126,16 @@ function createPhysicsWorldSnapshot(input) {
6107
6126
  });
6108
6127
  }
6109
6128
 
6129
+ // src/feature-flags.js
6130
+ var GPU_SHOWCASE_REALISTIC_MODELS_FEATURE = "gpu_showcase_realistic_models_v1";
6131
+
6110
6132
  // src/showcase-runtime.js
6111
6133
  var STYLE_ID = "plasius-shared-3d-showcase-style";
6112
6134
  var ROOT_CLASS = "plasius-showcase-root";
6113
- var DEFAULT_TITLE = "Flag by the Sea";
6114
- var DEFAULT_SUBTITLE = "Shared 3D validation scene using GLTF ships, cloth, fluid continuity, adaptive performance, and telemetry.";
6135
+ var CAPTURE_CLASS = "plasius-showcase-root--capture";
6136
+ var DEFAULT_CANVAS_WIDTH = 1280;
6137
+ var DEFAULT_CANVAS_HEIGHT = 720;
6138
+ var CAPTURE_CANVAS_PIXEL_BUDGET = 1920 * 1080;
6115
6139
  var SHIP_SCALE = 1.1;
6116
6140
  var HARBOR_BOUNDS = Object.freeze({
6117
6141
  minX: -11.2,
@@ -6129,84 +6153,75 @@ var CAMERA_PRESETS = Object.freeze({
6129
6153
  debug: Object.freeze({ yaw: -0.7, pitch: 0.32, distance: 24, target: [0, 2.2, 0] })
6130
6154
  });
6131
6155
  var showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
6132
- var SCENE_NOTES = Object.freeze([
6133
- "Ships are loaded from a GLTF asset and carry mass, damping, restitution, and hull extents from node extras.",
6134
- "Moonlight sets the cold ambient read while deck lanterns and harbor torches provide warm local contrast.",
6135
- "Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands even in the darker night palette.",
6136
- "Performance pressure reduces visual detail before mass-weighted authoritative collision motion is touched."
6137
- ]);
6138
- var UNIT_BOX_MESH = Object.freeze({
6139
- positions: Object.freeze([
6140
- -0.5,
6141
- -0.5,
6142
- -0.5,
6143
- 0.5,
6144
- -0.5,
6145
- -0.5,
6146
- 0.5,
6147
- 0.5,
6148
- -0.5,
6149
- -0.5,
6150
- 0.5,
6151
- -0.5,
6152
- -0.5,
6153
- -0.5,
6154
- 0.5,
6155
- 0.5,
6156
- -0.5,
6157
- 0.5,
6158
- 0.5,
6159
- 0.5,
6160
- 0.5,
6161
- -0.5,
6162
- 0.5,
6163
- 0.5
6164
- ]),
6165
- indices: Object.freeze([
6166
- 0,
6167
- 1,
6168
- 2,
6169
- 0,
6170
- 2,
6171
- 3,
6172
- 5,
6173
- 4,
6174
- 7,
6175
- 5,
6176
- 7,
6177
- 6,
6178
- 4,
6179
- 0,
6180
- 3,
6181
- 4,
6182
- 3,
6183
- 7,
6184
- 1,
6185
- 5,
6186
- 6,
6187
- 1,
6188
- 6,
6189
- 2,
6190
- 3,
6191
- 2,
6192
- 6,
6193
- 3,
6194
- 6,
6195
- 7,
6196
- 4,
6197
- 5,
6198
- 1,
6199
- 4,
6200
- 1,
6201
- 0
6202
- ])
6156
+ var FOCUS_MODE_TRANSLATION_KEYS = Object.freeze({
6157
+ integrated: gpuSharedTranslationKeys.focusIntegrated,
6158
+ lighting: gpuSharedTranslationKeys.focusLighting,
6159
+ cloth: gpuSharedTranslationKeys.focusCloth,
6160
+ fluid: gpuSharedTranslationKeys.focusFluid,
6161
+ physics: gpuSharedTranslationKeys.focusPhysics,
6162
+ performance: gpuSharedTranslationKeys.focusPerformance,
6163
+ debug: gpuSharedTranslationKeys.focusDebug
6203
6164
  });
6165
+ var SCENE_NOTE_KEYS = Object.freeze([
6166
+ gpuSharedTranslationKeys.noteAssetLoading,
6167
+ gpuSharedTranslationKeys.noteMoonlight,
6168
+ gpuSharedTranslationKeys.noteContinuity,
6169
+ gpuSharedTranslationKeys.notePerformance
6170
+ ]);
6171
+ var PHYSICS_SCENE_NOTE_KEYS = Object.freeze([
6172
+ gpuSharedTranslationKeys.notePhysicsSnapshots,
6173
+ gpuSharedTranslationKeys.notePhysicsCollisions,
6174
+ gpuSharedTranslationKeys.notePhysicsLighting
6175
+ ]);
6176
+ var LEGACY_HARBOR_LAYOUT = Object.freeze([
6177
+ Object.freeze({
6178
+ position: Object.freeze({ x: -8.2, y: 1.1, z: -0.9 }),
6179
+ rotationY: -0.16,
6180
+ scale: 5.4,
6181
+ color: { r: 0.32, g: 0.27, b: 0.23, a: 1 },
6182
+ accent: 0.06
6183
+ }),
6184
+ Object.freeze({
6185
+ position: Object.freeze({ x: -5.7, y: 0.45, z: 1.4 }),
6186
+ rotationY: -0.08,
6187
+ scale: { x: 6.8, y: 0.3, z: 2.1 },
6188
+ color: { r: 0.31, g: 0.31, b: 0.34, a: 1 },
6189
+ accent: 0.04
6190
+ }),
6191
+ Object.freeze({
6192
+ position: Object.freeze({ x: -10.4, y: 0.28, z: 0.8 }),
6193
+ rotationY: 0.22,
6194
+ scale: { x: 1.2, y: 0.9, z: 1.2 },
6195
+ color: { r: 0.31, g: 0.35, b: 0.39, a: 1 },
6196
+ accent: 0.02
6197
+ })
6198
+ ]);
6199
+ var SHOWCASE_ENVIRONMENT_LAYOUT = Object.freeze([
6200
+ Object.freeze({
6201
+ assetKey: "harbor-dock",
6202
+ position: Object.freeze({ x: -4.6, y: 0.16, z: 0.7 }),
6203
+ rotationY: -0.08,
6204
+ scale: 0.84,
6205
+ accent: 0.04
6206
+ }),
6207
+ Object.freeze({
6208
+ assetKey: "lighthouse",
6209
+ position: Object.freeze({ x: -9.8, y: 0, z: -0.58 }),
6210
+ rotationY: 0.12,
6211
+ scale: 0.56,
6212
+ accent: 0.08
6213
+ })
6214
+ ]);
6204
6215
  var SHIP_LANTERNS = Object.freeze([
6205
6216
  Object.freeze({ x: 0.94, y: 1.54, z: 2.52, glow: 1 }),
6206
6217
  Object.freeze({ x: -0.9, y: 1.58, z: 2.44, glow: 0.92 }),
6207
6218
  Object.freeze({ x: 0.62, y: 1.42, z: -2.18, glow: 0.88 }),
6208
6219
  Object.freeze({ x: -0.58, y: 1.46, z: -2.04, glow: 0.84 })
6209
6220
  ]);
6221
+ var CUTTER_LANTERNS = Object.freeze([
6222
+ Object.freeze({ x: 0.42, y: 1.04, z: 1.18, glow: 0.94 }),
6223
+ Object.freeze({ x: -0.42, y: 1.04, z: 1.12, glow: 0.88 })
6224
+ ]);
6210
6225
  var HARBOR_TORCHES = Object.freeze([
6211
6226
  Object.freeze({ x: -5.2, y: 1.25, z: 1.36, glow: 1.1 }),
6212
6227
  Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
@@ -6243,6 +6258,11 @@ function injectStyles() {
6243
6258
  radial-gradient(circle at 82% 18%, rgba(240, 188, 103, 0.08), transparent 18%),
6244
6259
  linear-gradient(180deg, #04101d 0%, #0b1930 42%, #081321 100%);
6245
6260
  }
6261
+ .${ROOT_CLASS}.${CAPTURE_CLASS} {
6262
+ min-height: 100vh;
6263
+ overflow: hidden;
6264
+ background: #030710;
6265
+ }
6246
6266
  .${ROOT_CLASS},
6247
6267
  .${ROOT_CLASS} * {
6248
6268
  box-sizing: border-box;
@@ -6322,6 +6342,40 @@ function injectStyles() {
6322
6342
  border: 1px solid rgba(159, 185, 223, 0.12);
6323
6343
  background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
6324
6344
  }
6345
+ .${CAPTURE_CLASS} .plasius-demo {
6346
+ width: 100vw;
6347
+ height: 100vh;
6348
+ padding: 0;
6349
+ display: block;
6350
+ }
6351
+ .${CAPTURE_CLASS} .plasius-demo__hero,
6352
+ .${CAPTURE_CLASS} .plasius-demo__toolbar,
6353
+ .${CAPTURE_CLASS} .plasius-demo__legend,
6354
+ .${CAPTURE_CLASS} .plasius-demo__sidebar,
6355
+ .${CAPTURE_CLASS} .plasius-demo__footer {
6356
+ display: none;
6357
+ }
6358
+ .${CAPTURE_CLASS} .plasius-demo__layout {
6359
+ display: block;
6360
+ height: 100%;
6361
+ }
6362
+ .${CAPTURE_CLASS} .plasius-demo__canvas-panel {
6363
+ height: 100%;
6364
+ padding: 0;
6365
+ border: 0;
6366
+ border-radius: 0;
6367
+ background: transparent;
6368
+ box-shadow: none;
6369
+ backdrop-filter: none;
6370
+ }
6371
+ .${CAPTURE_CLASS} .plasius-demo__canvas {
6372
+ width: 100%;
6373
+ height: 100%;
6374
+ aspect-ratio: auto;
6375
+ border: 0;
6376
+ border-radius: 0;
6377
+ background: #030710;
6378
+ }
6325
6379
  .plasius-demo__toolbar {
6326
6380
  position: absolute;
6327
6381
  top: 26px;
@@ -6413,6 +6467,10 @@ function clamp(value, min, max) {
6413
6467
  function mix(a, b, t) {
6414
6468
  return a + (b - a) * t;
6415
6469
  }
6470
+ function smoothstep(min, max, value) {
6471
+ const t = clamp((value - min) / Math.max(1e-4, max - min), 0, 1);
6472
+ return t * t * (3 - 2 * t);
6473
+ }
6416
6474
  function pseudoRandom(seed) {
6417
6475
  const value = Math.sin(seed * 12.9898 + seed * seed * 17e-4) * 43758.5453;
6418
6476
  return value - Math.floor(value);
@@ -6471,6 +6529,11 @@ function transformPoint(point, transform) {
6471
6529
  const rotated = rotateY(scaled, transform.rotationY);
6472
6530
  return addVec3(rotated, transform.position);
6473
6531
  }
6532
+ function transformDirection(direction, transform) {
6533
+ const scale = typeof transform.scale === "number" ? { x: transform.scale, y: transform.scale, z: transform.scale } : transform.scale;
6534
+ const scaled = vec3(direction.x * scale.x, direction.y * scale.y, direction.z * scale.z);
6535
+ return normalizeVec3(rotateY(scaled, transform.rotationY));
6536
+ }
6474
6537
  function projectPoint(point, camera, viewport) {
6475
6538
  const relative = subVec3(point, camera.eye);
6476
6539
  const viewX = dotVec3(relative, camera.right);
@@ -6494,6 +6557,66 @@ function colorToRgba(color, alpha = 1) {
6494
6557
  const b = Math.round(clamp(color.b, 0, 1) * 255);
6495
6558
  return `rgba(${r}, ${g}, ${b}, ${clamp(alpha, 0, 1)})`;
6496
6559
  }
6560
+ function mixColor(a, b, t) {
6561
+ return {
6562
+ r: mix(a.r, b.r, t),
6563
+ g: mix(a.g, b.g, t),
6564
+ b: mix(a.b, b.b, t),
6565
+ a: mix(a.a ?? 1, b.a ?? 1, t)
6566
+ };
6567
+ }
6568
+ function multiplyColor(a, b) {
6569
+ return {
6570
+ r: a.r * b.r,
6571
+ g: a.g * b.g,
6572
+ b: a.b * b.b,
6573
+ a: (a.a ?? 1) * (b.a ?? 1)
6574
+ };
6575
+ }
6576
+ function createLegacyMeshPrimitive(mesh) {
6577
+ return Object.freeze({
6578
+ name: mesh.name ?? "legacy-mesh",
6579
+ positions: mesh.positions,
6580
+ indices: mesh.indices,
6581
+ normals: null,
6582
+ colors: null,
6583
+ material: Object.freeze({
6584
+ name: "legacy-material",
6585
+ color: mesh.color ?? { r: 0.56, g: 0.33, b: 0.22, a: 1 },
6586
+ roughness: 0.88,
6587
+ metallic: 0.08,
6588
+ emissive: Object.freeze({ r: 0, g: 0, b: 0 })
6589
+ })
6590
+ });
6591
+ }
6592
+ function isFeatureEnabled(featureFlags, featureName, fallback = true) {
6593
+ const directValue = typeof featureFlags?.[featureName] === "boolean" ? featureFlags[featureName] : featureFlags?.flags?.[featureName];
6594
+ if (typeof directValue === "boolean") {
6595
+ return directValue;
6596
+ }
6597
+ const enabledValue = typeof featureFlags?.enabled?.[featureName] === "boolean" ? featureFlags.enabled[featureName] : void 0;
6598
+ if (typeof enabledValue === "boolean") {
6599
+ return enabledValue;
6600
+ }
6601
+ return fallback;
6602
+ }
6603
+ function getMeshPrimitives(mesh) {
6604
+ return Array.isArray(mesh?.primitives) && mesh.primitives.length > 0 ? mesh.primitives : [createLegacyMeshPrimitive(mesh)];
6605
+ }
6606
+ function tintPrimitiveColor(material, colorOverride) {
6607
+ if (!colorOverride) {
6608
+ return material.color;
6609
+ }
6610
+ const name = String(material.name ?? "").toLowerCase();
6611
+ if (name.includes("sail") || name.includes("glass") || name.includes("roof")) {
6612
+ return material.color;
6613
+ }
6614
+ const tintAmount = name.includes("hull") ? 0.54 : name.includes("trim") ? 0.22 : name.includes("deck") ? 0.12 : 0;
6615
+ if (tintAmount <= 0) {
6616
+ return material.color;
6617
+ }
6618
+ return mixColor(material.color, multiplyColor(material.color, colorOverride), tintAmount);
6619
+ }
6497
6620
  function projectShadowPoint(point, lightDir, planeY) {
6498
6621
  const shadowDir = scaleVec3(lightDir, -1);
6499
6622
  if (Math.abs(shadowDir.y) < 1e-4) {
@@ -6514,6 +6637,50 @@ function shadeColor(base, normal, lightDir, heightBias = 0, accent = 0) {
6514
6637
  b: clamp(base.b * (brightness + 0.03), 0, 1)
6515
6638
  };
6516
6639
  }
6640
+ function getMaterialSeed(materialName) {
6641
+ let seed = 0;
6642
+ for (let index = 0; index < materialName.length; index += 1) {
6643
+ seed += materialName.charCodeAt(index) * (index + 1);
6644
+ }
6645
+ return seed;
6646
+ }
6647
+ function getMaterialDetailStrength(material, surfaceType) {
6648
+ const name = String(material?.name ?? "").toLowerCase();
6649
+ if (surfaceType === "water" || name.includes("glass")) {
6650
+ return 0.018;
6651
+ }
6652
+ if (name.includes("wood") || name.includes("timber") || name.includes("plank")) {
6653
+ return 0.13;
6654
+ }
6655
+ if (name.includes("stone") || name.includes("concrete") || name.includes("plaster")) {
6656
+ return 0.1;
6657
+ }
6658
+ if (name.includes("roof") || name.includes("crate")) {
6659
+ return 0.09;
6660
+ }
6661
+ if (name.includes("paint")) {
6662
+ return 0.045;
6663
+ }
6664
+ if (name.includes("metal")) {
6665
+ return 0.035;
6666
+ }
6667
+ return 0.04;
6668
+ }
6669
+ function applyMaterialDetail(color, material, worldCenter, normal, surfaceType) {
6670
+ const materialName = String(material?.name ?? surfaceType ?? "material");
6671
+ const detailStrength = getMaterialDetailStrength(material, surfaceType);
6672
+ const sample = worldCenter.x * 3.17 + worldCenter.y * 5.29 + worldCenter.z * 7.83 + getMaterialSeed(materialName) * 0.013;
6673
+ const grain = (pseudoRandom(sample) - 0.5) * detailStrength;
6674
+ const lowerSurface = smoothstep(7.5, -0.8, worldCenter.y);
6675
+ const verticalSurface = 1 - clamp(Math.abs(normal.y), 0, 1);
6676
+ const materialLowerWear = /stone|concrete|plaster|paint|wood|timber|plank|crate/.test(materialName.toLowerCase()) ? lowerSurface * verticalSurface * 0.055 : 0;
6677
+ const wetlineWear = surfaceType === "ship" && worldCenter.y < 0.72 ? smoothstep(0.72, -0.1, worldCenter.y) * 0.05 : 0;
6678
+ return {
6679
+ r: clamp(color.r * (1 + grain) - materialLowerWear - wetlineWear, 0, 1),
6680
+ g: clamp(color.g * (1 + grain * 0.82) - materialLowerWear * 0.9 - wetlineWear, 0, 1),
6681
+ b: clamp(color.b * (1 + grain * 0.62) - materialLowerWear * 0.68 - wetlineWear * 0.75, 0, 1)
6682
+ };
6683
+ }
6517
6684
  function buildCamera(state, canvas) {
6518
6685
  const preset = CAMERA_PRESETS[state.focus] ?? CAMERA_PRESETS.integrated;
6519
6686
  const yaw = state.camera.yaw ?? preset.yaw;
@@ -6538,44 +6705,131 @@ function buildCamera(state, canvas) {
6538
6705
  aspect: canvas.width / canvas.height
6539
6706
  };
6540
6707
  }
6541
- function buildTrianglesFromMesh(mesh, transform, baseColor, camera, viewport, triangles, accent = 0) {
6542
- for (let index = 0; index < mesh.indices.length; index += 3) {
6543
- const aIndex = mesh.indices[index] * 3;
6544
- const bIndex = mesh.indices[index + 1] * 3;
6545
- const cIndex = mesh.indices[index + 2] * 3;
6546
- const a = transformPoint(
6547
- vec3(mesh.positions[aIndex], mesh.positions[aIndex + 1], mesh.positions[aIndex + 2]),
6548
- transform
6549
- );
6550
- const b = transformPoint(
6551
- vec3(mesh.positions[bIndex], mesh.positions[bIndex + 1], mesh.positions[bIndex + 2]),
6552
- transform
6553
- );
6554
- const c = transformPoint(
6555
- vec3(mesh.positions[cIndex], mesh.positions[cIndex + 1], mesh.positions[cIndex + 2]),
6556
- transform
6557
- );
6558
- const ab = subVec3(b, a);
6559
- const ac = subVec3(c, a);
6560
- const normal = normalizeVec3(crossVec3(ab, ac));
6561
- const viewDir = normalizeVec3(subVec3(camera.eye, a));
6562
- if (dotVec3(normal, viewDir) <= 0) {
6563
- continue;
6564
- }
6565
- const projected = [projectPoint(a, camera, viewport), projectPoint(b, camera, viewport), projectPoint(c, camera, viewport)];
6566
- if (projected.some((value) => value === null)) {
6567
- continue;
6708
+ function buildTrianglesFromMesh(mesh, transform, colorOverride, camera, viewport, triangles, options = {}) {
6709
+ const primitives = getMeshPrimitives(mesh);
6710
+ for (const primitive of primitives) {
6711
+ const resolvedColor = tintPrimitiveColor(primitive.material, colorOverride);
6712
+ for (let index = 0; index < primitive.indices.length; index += 3) {
6713
+ const aIndex = primitive.indices[index] * 3;
6714
+ const bIndex = primitive.indices[index + 1] * 3;
6715
+ const cIndex = primitive.indices[index + 2] * 3;
6716
+ const a = transformPoint(
6717
+ vec3(
6718
+ primitive.positions[aIndex],
6719
+ primitive.positions[aIndex + 1],
6720
+ primitive.positions[aIndex + 2]
6721
+ ),
6722
+ transform
6723
+ );
6724
+ const b = transformPoint(
6725
+ vec3(
6726
+ primitive.positions[bIndex],
6727
+ primitive.positions[bIndex + 1],
6728
+ primitive.positions[bIndex + 2]
6729
+ ),
6730
+ transform
6731
+ );
6732
+ const c = transformPoint(
6733
+ vec3(
6734
+ primitive.positions[cIndex],
6735
+ primitive.positions[cIndex + 1],
6736
+ primitive.positions[cIndex + 2]
6737
+ ),
6738
+ transform
6739
+ );
6740
+ const ab = subVec3(b, a);
6741
+ const ac = subVec3(c, a);
6742
+ const faceNormal = normalizeVec3(crossVec3(ab, ac));
6743
+ let normal = faceNormal;
6744
+ if (Array.isArray(primitive.normals)) {
6745
+ const aNormal = transformDirection(
6746
+ vec3(
6747
+ primitive.normals[aIndex],
6748
+ primitive.normals[aIndex + 1],
6749
+ primitive.normals[aIndex + 2]
6750
+ ),
6751
+ transform
6752
+ );
6753
+ const bNormal = transformDirection(
6754
+ vec3(
6755
+ primitive.normals[bIndex],
6756
+ primitive.normals[bIndex + 1],
6757
+ primitive.normals[bIndex + 2]
6758
+ ),
6759
+ transform
6760
+ );
6761
+ const cNormal = transformDirection(
6762
+ vec3(
6763
+ primitive.normals[cIndex],
6764
+ primitive.normals[cIndex + 1],
6765
+ primitive.normals[cIndex + 2]
6766
+ ),
6767
+ transform
6768
+ );
6769
+ normal = normalizeVec3(
6770
+ scaleVec3(addVec3(addVec3(aNormal, bNormal), cNormal), 1 / 3)
6771
+ );
6772
+ }
6773
+ const viewDir = normalizeVec3(subVec3(camera.eye, a));
6774
+ if (dotVec3(faceNormal, viewDir) <= 0) {
6775
+ continue;
6776
+ }
6777
+ const projected = [
6778
+ projectPoint(a, camera, viewport),
6779
+ projectPoint(b, camera, viewport),
6780
+ projectPoint(c, camera, viewport)
6781
+ ];
6782
+ if (projected.some((value) => value === null)) {
6783
+ continue;
6784
+ }
6785
+ triangles.push({
6786
+ points: projected,
6787
+ depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
6788
+ worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
6789
+ normal,
6790
+ baseColor: resolvedColor,
6791
+ accent: options.accent ?? 0,
6792
+ material: primitive.material,
6793
+ reflection: options.reflection ?? 0,
6794
+ surfaceType: options.surfaceType ?? "solid"
6795
+ });
6568
6796
  }
6569
- triangles.push({
6570
- points: projected,
6571
- depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
6572
- worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
6573
- normal,
6574
- baseColor,
6575
- accent
6576
- });
6577
6797
  }
6578
6798
  }
6799
+ async function loadShowcaseAssetCatalog() {
6800
+ const [brigantine, cutter, lighthouse, harborDock] = await Promise.all([
6801
+ loadGltfModel(resolveShowcaseAssetUrl("brigantine")),
6802
+ loadGltfModel(resolveShowcaseAssetUrl("cutter")),
6803
+ loadGltfModel(resolveShowcaseAssetUrl("lighthouse")),
6804
+ loadGltfModel(resolveShowcaseAssetUrl("harbor-dock"))
6805
+ ]);
6806
+ return Object.freeze({
6807
+ primaryShipKey: "brigantine",
6808
+ ships: Object.freeze({
6809
+ brigantine,
6810
+ cutter
6811
+ }),
6812
+ environment: Object.freeze({
6813
+ lighthouse,
6814
+ "harbor-dock": harborDock
6815
+ })
6816
+ });
6817
+ }
6818
+ function createLegacyShowcaseAssetCatalog() {
6819
+ const brigantine = loadGltfModel(resolveShowcaseAssetUrl("brigantine"));
6820
+ return Promise.resolve(brigantine).then(
6821
+ (primary) => Object.freeze({
6822
+ primaryShipKey: "brigantine",
6823
+ ships: Object.freeze({
6824
+ brigantine: primary
6825
+ }),
6826
+ environment: Object.freeze({})
6827
+ })
6828
+ );
6829
+ }
6830
+ function resolveShipModel(state, ship, fallbackModel = null) {
6831
+ return state.assetCatalog?.ships?.[ship.modelKey ?? state.assetCatalog?.primaryShipKey ?? "brigantine"] ?? fallbackModel ?? state.shipModel;
6832
+ }
6579
6833
  function createPerformanceGovernor() {
6580
6834
  const fluidDetail = createQualityLadderAdapter({
6581
6835
  id: "fluid-detail",
@@ -6627,6 +6881,7 @@ function createPerformanceGovernor() {
6627
6881
  return { governor, fluidDetail, clothDetail, lightingDetail };
6628
6882
  }
6629
6883
  function buildDemoDom(root, options) {
6884
+ const t = options.translate;
6630
6885
  root.innerHTML = `
6631
6886
  <main class="plasius-demo">
6632
6887
  <section class="plasius-demo__hero">
@@ -6636,56 +6891,52 @@ function buildDemoDom(root, options) {
6636
6891
  <p class="plasius-demo__lead">${options.subtitle}</p>
6637
6892
  </section>
6638
6893
  <section class="plasius-panel plasius-demo__status">
6639
- <p id="demoStatus" class="plasius-demo__status-badge">Booting 3D scene\u2026</p>
6894
+ <p id="demoStatus" class="plasius-demo__status-badge">${t(gpuSharedTranslationKeys.statusBooting)}</p>
6640
6895
  <p id="demoDetails" class="plasius-demo__status-text">
6641
- Preparing a moonlit harbor scene, GLTF hull data, cloth and fluid continuity plans, and adaptive quality metadata.
6896
+ ${t(gpuSharedTranslationKeys.detailsBooting)}
6642
6897
  </p>
6643
6898
  </section>
6644
6899
  </section>
6645
6900
  <section class="plasius-demo__layout">
6646
6901
  <section class="plasius-panel plasius-demo__canvas-panel">
6647
- <canvas id="demoCanvas" class="plasius-demo__canvas" width="1280" height="720"></canvas>
6902
+ <canvas id="demoCanvas" class="plasius-demo__canvas" width="${DEFAULT_CANVAS_WIDTH}" height="${DEFAULT_CANVAS_HEIGHT}"></canvas>
6648
6903
  <div class="plasius-demo__toolbar">
6649
- <button id="pauseButton" type="button">Pause</button>
6904
+ <button id="pauseButton" type="button">${t(gpuSharedTranslationKeys.pause)}</button>
6650
6905
  <label class="plasius-toggle">
6651
6906
  <input id="stressToggle" type="checkbox" />
6652
- Stress mode
6907
+ ${t(gpuSharedTranslationKeys.stressMode)}
6653
6908
  </label>
6654
6909
  <label class="plasius-toggle">
6655
- Focus
6910
+ ${t(gpuSharedTranslationKeys.focus)}
6656
6911
  <select id="focusMode">
6657
- <option value="integrated">integrated</option>
6658
- <option value="lighting">lighting</option>
6659
- <option value="cloth">cloth</option>
6660
- <option value="fluid">fluid</option>
6661
- <option value="physics">physics</option>
6662
- <option value="performance">performance</option>
6663
- <option value="debug">debug</option>
6912
+ ${showcaseFocusModes.map(
6913
+ (mode) => `<option value="${mode}">${t(FOCUS_MODE_TRANSLATION_KEYS[mode])}</option>`
6914
+ ).join("")}
6664
6915
  </select>
6665
6916
  </label>
6666
6917
  </div>
6667
6918
  <div class="plasius-demo__legend">
6668
- <strong>Scene</strong>
6669
- GLTF ships carry hull mass and damping metadata.<br />
6670
- Lanterns and torches warm the moonlit harbor.<br />
6671
- Mass-aware collisions stay authoritative near the camera.
6919
+ <strong>${t(gpuSharedTranslationKeys.legendTitle)}</strong>
6920
+ ${t(gpuSharedTranslationKeys.legendShipMetadata)}<br />
6921
+ ${t(gpuSharedTranslationKeys.legendLighting)}<br />
6922
+ ${t(gpuSharedTranslationKeys.legendCollisions)}
6672
6923
  </div>
6673
6924
  </section>
6674
6925
  <aside class="plasius-demo__sidebar">
6675
6926
  <section class="plasius-panel plasius-demo__card">
6676
- <h2>Scene State</h2>
6927
+ <h2>${t(gpuSharedTranslationKeys.sceneState)}</h2>
6677
6928
  <ul id="sceneMetrics" class="plasius-demo__metrics"></ul>
6678
6929
  </section>
6679
6930
  <section class="plasius-panel plasius-demo__card">
6680
- <h2>Quality + Budgets</h2>
6931
+ <h2>${t(gpuSharedTranslationKeys.qualityBudgets)}</h2>
6681
6932
  <ul id="qualityMetrics" class="plasius-demo__metrics"></ul>
6682
6933
  </section>
6683
6934
  <section class="plasius-panel plasius-demo__card">
6684
- <h2>Debug Telemetry</h2>
6935
+ <h2>${t(gpuSharedTranslationKeys.debugTelemetry)}</h2>
6685
6936
  <ul id="debugMetrics" class="plasius-demo__metrics"></ul>
6686
6937
  </section>
6687
6938
  <section class="plasius-panel plasius-demo__card">
6688
- <h2>Notes</h2>
6939
+ <h2>${t(gpuSharedTranslationKeys.notes)}</h2>
6689
6940
  <ul id="sceneNotes" class="plasius-demo__metrics"></ul>
6690
6941
  </section>
6691
6942
  </aside>
@@ -6709,6 +6960,11 @@ function buildDemoDom(root, options) {
6709
6960
  };
6710
6961
  }
6711
6962
  function buildSceneSnapshot(state, shipModel) {
6963
+ const shipPhysics = Object.freeze(
6964
+ Object.fromEntries(
6965
+ state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
6966
+ )
6967
+ );
6712
6968
  return Object.freeze({
6713
6969
  focus: state.focus,
6714
6970
  frame: state.frame,
@@ -6730,6 +6986,7 @@ function buildSceneSnapshot(state, shipModel) {
6730
6986
  state.ships.map(
6731
6987
  (ship) => Object.freeze({
6732
6988
  id: ship.id,
6989
+ modelKey: ship.modelKey ?? "brigantine",
6733
6990
  position: Object.freeze({ ...ship.position }),
6734
6991
  velocity: Object.freeze({ ...ship.velocity }),
6735
6992
  rotationY: ship.rotationY,
@@ -6750,12 +7007,13 @@ function buildSceneSnapshot(state, shipModel) {
6750
7007
  )
6751
7008
  ),
6752
7009
  shipPhysics: shipModel?.physics ?? null,
7010
+ shipModels: shipPhysics,
6753
7011
  physics: Object.freeze({
6754
7012
  profile: state.physics.profile,
6755
7013
  plan: state.physics.plan,
6756
7014
  manifest: state.physics.manifest,
6757
7015
  snapshot: state.physics.snapshot,
6758
- shipPhysics: shipModel?.physics ?? null
7016
+ shipPhysics
6759
7017
  })
6760
7018
  });
6761
7019
  }
@@ -6790,6 +7048,61 @@ function normalizeColorOverride(color, fallback) {
6790
7048
  function readVisualNumber(value, fallback) {
6791
7049
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
6792
7050
  }
7051
+ function readPositiveNumber3(value, fallback) {
7052
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
7053
+ }
7054
+ function isTruthyCaptureValue(value) {
7055
+ return value === "1" || value === "true" || value === "scene" || value === "video";
7056
+ }
7057
+ function resolveCaptureSettings(options) {
7058
+ const explicitCaptureMode = typeof options.captureMode === "boolean" ? options.captureMode : void 0;
7059
+ let captureMode = explicitCaptureMode ?? false;
7060
+ let renderScale = readPositiveNumber3(options.renderScale, void 0);
7061
+ try {
7062
+ const params = new URLSearchParams(window.location.search);
7063
+ if (explicitCaptureMode === void 0) {
7064
+ captureMode = isTruthyCaptureValue(params.get("capture")) || params.get("presentation") === "capture";
7065
+ }
7066
+ renderScale = readPositiveNumber3(Number(params.get("renderScale")), renderScale);
7067
+ } catch {
7068
+ }
7069
+ return {
7070
+ captureMode,
7071
+ renderScale
7072
+ };
7073
+ }
7074
+ function getCanvasDisplaySize(canvas) {
7075
+ const rect = typeof canvas.getBoundingClientRect === "function" ? canvas.getBoundingClientRect() : null;
7076
+ const width = Math.round(
7077
+ readPositiveNumber3(rect?.width, readPositiveNumber3(canvas.clientWidth, canvas.width))
7078
+ );
7079
+ const height = Math.round(
7080
+ readPositiveNumber3(rect?.height, readPositiveNumber3(canvas.clientHeight, canvas.height))
7081
+ );
7082
+ return {
7083
+ width: Math.max(1, width || DEFAULT_CANVAS_WIDTH),
7084
+ height: Math.max(1, height || DEFAULT_CANVAS_HEIGHT)
7085
+ };
7086
+ }
7087
+ function resizeCanvasToDisplaySize(canvas, state) {
7088
+ const { width, height } = getCanvasDisplaySize(canvas);
7089
+ const deviceScale = readPositiveNumber3(globalThis.devicePixelRatio, 1);
7090
+ const requestedScale = readPositiveNumber3(state.renderScale, deviceScale);
7091
+ const maxScale = state.captureMode ? 2 : 1.5;
7092
+ let scale = clamp(requestedScale, 1, maxScale);
7093
+ const pixelBudget = state.captureMode ? CAPTURE_CANVAS_PIXEL_BUDGET : DEFAULT_CANVAS_WIDTH * DEFAULT_CANVAS_HEIGHT * 1.5;
7094
+ const projectedPixels = width * height * scale * scale;
7095
+ if (projectedPixels > pixelBudget) {
7096
+ scale = Math.sqrt(pixelBudget / Math.max(1, width * height));
7097
+ }
7098
+ const targetWidth = Math.max(1, Math.round(width * scale));
7099
+ const targetHeight = Math.max(1, Math.round(height * scale));
7100
+ if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
7101
+ canvas.width = targetWidth;
7102
+ canvas.height = targetHeight;
7103
+ }
7104
+ state.renderScale = scale;
7105
+ }
6793
7106
  function resolveClothPresentation(state, meshDetail) {
6794
7107
  const clothPlan = createClothRepresentationPlan({
6795
7108
  garmentId: "shore-flag",
@@ -7191,6 +7504,12 @@ function sampleWave(state, x, z, time) {
7191
7504
  const base = Math.sin(along * 0.22 - time * 1.12 * phaseSpeed) * 0.42 + Math.cos(along * 0.11 + cross * 0.07 - time * 0.78 * phaseSpeed) * 0.26 + Math.sin(cross * 0.19 - time * 1.34 * phaseSpeed) * 0.16;
7192
7505
  return base * amplitude + sampleShipWake(state, x, z, time) + sampleWaveImpulses(state, x, z, time);
7193
7506
  }
7507
+ function resolveFluidBandContinuity(continuity, band) {
7508
+ if (continuity?.bands && continuity.bands[band]) {
7509
+ return continuity.bands[band];
7510
+ }
7511
+ return continuity ?? { amplitudeFloor: 1, frequencyFloor: 1 };
7512
+ }
7194
7513
  function buildWaterMotionEffects(state) {
7195
7514
  const wakeTrails = [];
7196
7515
  const rippleRings = state.waveImpulses.map((impulse) => {
@@ -7264,6 +7583,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
7264
7583
  for (const bandSpec of bandExtents) {
7265
7584
  const representation = fluidPlan.representations.find((entry) => entry.band === bandSpec.band) ?? fluidPlan.representations[0];
7266
7585
  const continuity = createFluidContinuityEnvelope({ fluidBodyId: "harbor" });
7586
+ const bandContinuity = resolveFluidBandContinuity(continuity, bandSpec.band);
7267
7587
  const bandResolution = bandSpec.band === "near" ? fluidDetail.nearResolution : bandSpec.band === "mid" ? fluidDetail.midResolution : bandSpec.band === "far" ? 5 : 3;
7268
7588
  const cols = Math.max(4, bandResolution * 2);
7269
7589
  const rows = Math.max(4, bandResolution + 2);
@@ -7277,7 +7597,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
7277
7597
  const v = row / (rows - 1);
7278
7598
  const x = originX + bandSpec.width * u;
7279
7599
  const z = originZ + bandSpec.depth * v;
7280
- const y = bandSpec.y + sampleWave(state, x, z, state.time) * continuity.amplitudeFloor * (bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
7600
+ const y = bandSpec.y + sampleWave(state, x, z, state.time) * bandContinuity.amplitudeFloor * (bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
7281
7601
  positions.push(vec3(x, y, z));
7282
7602
  }
7283
7603
  }
@@ -7293,7 +7613,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
7293
7613
  bandMeshes.push({
7294
7614
  band: bandSpec.band,
7295
7615
  representation,
7296
- continuity,
7616
+ continuity: bandContinuity,
7297
7617
  rows,
7298
7618
  cols,
7299
7619
  positions,
@@ -7312,6 +7632,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
7312
7632
  return { fluidPlan, bandMeshes };
7313
7633
  }
7314
7634
  function createSceneState(options) {
7635
+ const translate = options.translate;
7315
7636
  const { governor, fluidDetail, clothDetail, lightingDetail } = createPerformanceGovernor();
7316
7637
  const physicsProfile = defaultPhysicsWorkerProfile;
7317
7638
  const physicsPlan = createPhysicsSimulationPlan(physicsProfile);
@@ -7319,7 +7640,7 @@ function createSceneState(options) {
7319
7640
  const debugSession = createGpuDebugSession({
7320
7641
  enabled: true,
7321
7642
  adapter: {
7322
- label: "3D showcase",
7643
+ label: translate(gpuSharedTranslationKeys.debugAdapterShowcase),
7323
7644
  memoryCapacityHintBytes: 6 * 1024 * 1024 * 1024,
7324
7645
  coreCountHint: 12
7325
7646
  }
@@ -7329,22 +7650,26 @@ function createSceneState(options) {
7329
7650
  owner: "renderer",
7330
7651
  category: "texture",
7331
7652
  sizeBytes: 1280 * 720 * 4,
7332
- label: "Main color buffer"
7653
+ label: translate(gpuSharedTranslationKeys.debugMainColorBuffer)
7333
7654
  });
7334
7655
  debugSession.trackAllocation({
7335
7656
  id: "showcase.shadow-impression",
7336
7657
  owner: "lighting",
7337
7658
  category: "texture",
7338
7659
  sizeBytes: 12 * 1024 * 1024,
7339
- label: "Shadow impression atlas"
7660
+ label: translate(gpuSharedTranslationKeys.debugShadowImpressionAtlas)
7340
7661
  });
7341
7662
  return {
7663
+ translate,
7342
7664
  focus: options.focus,
7343
7665
  governor,
7344
7666
  fluidDetail,
7345
7667
  clothDetail,
7346
7668
  lightingDetail,
7347
7669
  debugSession,
7670
+ showcaseRealisticModelsEnabled: options.realisticModelsEnabled !== false,
7671
+ captureMode: options.captureMode === true,
7672
+ renderScale: readPositiveNumber3(options.renderScale, void 0),
7348
7673
  packageState: void 0,
7349
7674
  demoDescription: null,
7350
7675
  demoVisuals: null,
@@ -7359,6 +7684,7 @@ function createSceneState(options) {
7359
7684
  ships: [
7360
7685
  {
7361
7686
  id: "northwind",
7687
+ modelKey: "brigantine",
7362
7688
  position: vec3(-5.2, 0, 7.2),
7363
7689
  velocity: vec3(2.35, 0, -1.08),
7364
7690
  rotationY: 0.58,
@@ -7369,17 +7695,18 @@ function createSceneState(options) {
7369
7695
  throttleResponse: 0.46,
7370
7696
  rudderResponse: 0.54,
7371
7697
  wanderPhase: 0.35,
7372
- lanterns: SHIP_LANTERNS,
7698
+ lanterns: CUTTER_LANTERNS,
7373
7699
  lanternStrength: 1.06,
7374
7700
  collisionRadiusScale: 1.04
7375
7701
  },
7376
7702
  {
7377
7703
  id: "tidecaller",
7704
+ modelKey: "cutter",
7378
7705
  position: vec3(4.8, 0, 4.4),
7379
7706
  velocity: vec3(-2.15, 0, 1.74),
7380
7707
  rotationY: -2.48,
7381
7708
  angularVelocity: -0.2,
7382
- tint: { r: 0.48, g: 0.28, b: 0.19 },
7709
+ tint: { r: 0.58, g: 0.24, b: 0.16 },
7383
7710
  massScale: 0.84,
7384
7711
  cruiseSpeed: 2.68,
7385
7712
  throttleResponse: 0.7,
@@ -7403,6 +7730,7 @@ function createSceneState(options) {
7403
7730
  manifest: physicsManifest,
7404
7731
  snapshot: null
7405
7732
  },
7733
+ assetCatalog: null,
7406
7734
  shipModel: null
7407
7735
  };
7408
7736
  }
@@ -7478,10 +7806,38 @@ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, s
7478
7806
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7479
7807
  }
7480
7808
  }
7481
- function drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength) {
7809
+ function resolveLocalLightContribution(triangle, lightSources) {
7810
+ const contribution = { r: 0, g: 0, b: 0 };
7811
+ if (!Array.isArray(lightSources) || triangle.surfaceType === "water") {
7812
+ return contribution;
7813
+ }
7814
+ const normal = normalizeVec3(triangle.normal);
7815
+ for (const source of lightSources.slice(0, 8)) {
7816
+ const delta = subVec3(source.point, triangle.worldCenter);
7817
+ const distance = lengthVec3(delta);
7818
+ const attenuation = (source.glowScale ?? 1) / Math.max(1, 0.68 + distance * distance * 0.2);
7819
+ if (attenuation < 0.012) {
7820
+ continue;
7821
+ }
7822
+ const lightDir = normalizeVec3(delta);
7823
+ const facing = clamp(dotVec3(normal, lightDir), 0, 1);
7824
+ const response = attenuation * (0.18 + facing * 0.82);
7825
+ const glowColor = source.glowColor ?? source.coreColor ?? { r: 1, g: 0.72, b: 0.4 };
7826
+ contribution.r += glowColor.r * response * 0.32;
7827
+ contribution.g += glowColor.g * response * 0.26;
7828
+ contribution.b += glowColor.b * response * 0.18;
7829
+ }
7830
+ return contribution;
7831
+ }
7832
+ function drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength, localLights = []) {
7482
7833
  triangles.sort((left, right) => right.depth - left.depth);
7483
7834
  for (const triangle of triangles) {
7484
7835
  const surfaceNormal = normalizeVec3(triangle.normal);
7836
+ const material = triangle.material ?? {
7837
+ roughness: 0.88,
7838
+ metallic: 0.08,
7839
+ emissive: { r: 0, g: 0, b: 0 }
7840
+ };
7485
7841
  const shaded = shadeColor(
7486
7842
  triangle.baseColor,
7487
7843
  surfaceNormal,
@@ -7489,19 +7845,41 @@ function drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, sha
7489
7845
  clamp((triangle.worldCenter.y + 3) / 10, 0, 1),
7490
7846
  triangle.accent
7491
7847
  );
7492
- const reflection = triangle.worldCenter.y < 0.8 ? reflectionStrength : 0;
7848
+ const reflection = reflectionStrength * (triangle.reflection ?? 0);
7493
7849
  const viewDir = normalizeVec3(subVec3(camera.eye, triangle.worldCenter));
7494
7850
  const reflectedLight = reflectVec3(scaleVec3(lightDir, -1), surfaceNormal);
7495
- const gloss = triangle.worldCenter.y < 0.9 ? 1 : triangle.accent > 0.05 ? 0.55 : 0.3;
7496
- const specular = Math.pow(clamp(dotVec3(reflectedLight, viewDir), 0, 1), triangle.worldCenter.y < 0.9 ? 18 : 12) * gloss;
7497
- const occlusion = triangle.worldCenter.y < 0.9 ? shadowStrength * 0.035 : 0;
7498
- const fill = colorToRgba(
7851
+ const gloss = mix(0.78, 0.14, clamp(material.roughness ?? 0.88, 0, 1)) + (material.metallic ?? 0) * 0.18;
7852
+ const specularPower = mix(26, 7, clamp(material.roughness ?? 0.88, 0, 1));
7853
+ const specular = Math.pow(clamp(dotVec3(reflectedLight, viewDir), 0, 1), specularPower) * gloss;
7854
+ const emissive = material.emissive ?? { r: 0, g: 0, b: 0 };
7855
+ const localLight = resolveLocalLightContribution(triangle, localLights);
7856
+ const occlusion = triangle.surfaceType === "water" ? shadowStrength * 0.018 : shadowStrength * 0.04;
7857
+ const detailed = applyMaterialDetail(
7499
7858
  {
7500
- r: clamp(shaded.r + reflection * 0.08 + specular * 0.14 - occlusion, 0, 1),
7501
- g: clamp(shaded.g + reflection * 0.08 + specular * 0.15 - occlusion, 0, 1),
7502
- b: clamp(shaded.b + reflection * 0.16 + specular * 0.2 - occlusion * 0.5, 0, 1)
7859
+ r: clamp(
7860
+ shaded.r + reflection * 0.08 + specular * 0.16 + emissive.r * 0.42 + localLight.r - occlusion,
7861
+ 0,
7862
+ 1
7863
+ ),
7864
+ g: clamp(
7865
+ shaded.g + reflection * 0.08 + specular * 0.16 + emissive.g * 0.42 + localLight.g - occlusion,
7866
+ 0,
7867
+ 1
7868
+ ),
7869
+ b: clamp(
7870
+ shaded.b + reflection * 0.16 + specular * 0.22 + emissive.b * 0.46 + localLight.b - occlusion * 0.5,
7871
+ 0,
7872
+ 1
7873
+ )
7503
7874
  },
7504
- 0.98
7875
+ material,
7876
+ triangle.worldCenter,
7877
+ surfaceNormal,
7878
+ triangle.surfaceType
7879
+ );
7880
+ const fill = colorToRgba(
7881
+ detailed,
7882
+ triangle.baseColor.a ?? 0.98
7505
7883
  );
7506
7884
  ctx.fillStyle = fill;
7507
7885
  ctx.beginPath();
@@ -7532,74 +7910,100 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
7532
7910
  ctx.fill();
7533
7911
  ctx.restore();
7534
7912
  }
7535
- function pushHarborGeometry(camera, viewport, triangles, visuals) {
7536
- const harborObjects = [
7537
- {
7538
- position: vec3(-8.2, 1.1, -0.9),
7539
- rotationY: -0.16,
7540
- scale: { x: 5.4, y: 2.4, z: 4.2 },
7541
- color: visuals.harborWall,
7542
- accent: 0.06
7543
- },
7544
- {
7545
- position: vec3(-5.7, 0.45, 1.4),
7546
- rotationY: -0.08,
7547
- scale: { x: 6.8, y: 0.3, z: 2.1 },
7548
- color: visuals.harborDeck,
7549
- accent: 0.04
7550
- },
7551
- {
7552
- position: vec3(-10.4, 0.28, 0.8),
7553
- rotationY: 0.22,
7554
- scale: { x: 1.2, y: 0.9, z: 1.2 },
7555
- color: visuals.harborTower,
7556
- accent: 0.02
7913
+ function pushHarborGeometry(camera, viewport, triangles, state) {
7914
+ if (!state.showcaseRealisticModelsEnabled) {
7915
+ for (const object of LEGACY_HARBOR_LAYOUT) {
7916
+ buildTrianglesFromMesh(
7917
+ { positions: [object], indices: [0], normals: null, colors: null, material: createLegacyMeshPrimitive({})?.material, bounds: null, name: "legacy-structure" },
7918
+ {
7919
+ position: object.position,
7920
+ rotationY: object.rotationY,
7921
+ scale: object.scale
7922
+ },
7923
+ object.color,
7924
+ camera,
7925
+ viewport,
7926
+ triangles,
7927
+ {
7928
+ accent: object.accent,
7929
+ reflection: 0,
7930
+ surfaceType: "structure"
7931
+ }
7932
+ );
7933
+ }
7934
+ return;
7935
+ }
7936
+ for (const placement of SHOWCASE_ENVIRONMENT_LAYOUT) {
7937
+ const mesh = state.assetCatalog?.environment?.[placement.assetKey] ?? null;
7938
+ if (!mesh) {
7939
+ continue;
7557
7940
  }
7558
- ];
7559
- for (const object of harborObjects) {
7560
7941
  buildTrianglesFromMesh(
7561
- UNIT_BOX_MESH,
7942
+ mesh,
7562
7943
  {
7563
- position: object.position,
7564
- rotationY: object.rotationY,
7565
- scale: object.scale
7944
+ position: vec3(placement.position.x, placement.position.y, placement.position.z),
7945
+ rotationY: placement.rotationY,
7946
+ scale: placement.scale
7566
7947
  },
7567
- object.color,
7948
+ null,
7568
7949
  camera,
7569
7950
  viewport,
7570
7951
  triangles,
7571
- object.accent
7952
+ {
7953
+ accent: placement.accent,
7954
+ reflection: 0,
7955
+ surfaceType: "structure"
7956
+ }
7572
7957
  );
7573
7958
  }
7574
7959
  }
7575
7960
  function renderShipRigging(ctx, ship, camera, viewport) {
7576
7961
  const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
7577
- const mastBase = transformPoint(vec3(0, 0.38, -0.4), transform);
7578
- const mastTop = transformPoint(vec3(0, 3.8, -0.2), transform);
7579
- const aftBase = transformPoint(vec3(-0.25, 0.32, -1.9), transform);
7580
- const aftTop = transformPoint(vec3(-0.15, 2.7, -1.75), transform);
7581
- const sailA = transformPoint(vec3(0.08, 3.2, -0.2), transform);
7582
- const sailB = transformPoint(vec3(0.12, 1.2, -0.5), transform);
7583
- const sailC = transformPoint(vec3(2.25, 2.25, 0.15), transform);
7584
- const projected = [mastBase, mastTop, aftBase, aftTop, sailA, sailB, sailC].map(
7585
- (point) => projectPoint(point, camera, viewport)
7586
- );
7962
+ const layout = ship.modelKey === "cutter" ? {
7963
+ lineColor: "rgba(85, 89, 97, 0.92)",
7964
+ sailColor: "rgba(218, 232, 244, 0.28)",
7965
+ points: [
7966
+ vec3(0, 0.88, -0.32),
7967
+ vec3(0, 2.4, -0.28),
7968
+ vec3(0.1, 1.92, -0.3),
7969
+ vec3(1.18, 1.72, -0.18),
7970
+ vec3(1.04, 1.08, -0.12)
7971
+ ],
7972
+ mastPairs: [[0, 1], [2, 3]],
7973
+ sailTriangle: [2, 3, 4]
7974
+ } : {
7975
+ lineColor: "rgba(73, 54, 45, 0.94)",
7976
+ sailColor: "rgba(238, 232, 214, 0.88)",
7977
+ points: [
7978
+ vec3(0, 0.38, -0.4),
7979
+ vec3(0, 3.8, -0.2),
7980
+ vec3(-0.25, 0.32, -1.9),
7981
+ vec3(-0.15, 2.7, -1.75),
7982
+ vec3(0.08, 3.2, -0.2),
7983
+ vec3(0.12, 1.2, -0.5),
7984
+ vec3(2.25, 2.25, 0.15)
7985
+ ],
7986
+ mastPairs: [[0, 1], [2, 3]],
7987
+ sailTriangle: [4, 5, 6]
7988
+ };
7989
+ const projected = layout.points.map((point) => transformPoint(point, transform)).map((point) => projectPoint(point, camera, viewport));
7587
7990
  if (projected.some((value) => value === null)) {
7588
7991
  return;
7589
7992
  }
7590
- ctx.strokeStyle = "rgba(73, 54, 45, 0.94)";
7591
- ctx.lineWidth = 3.5;
7993
+ ctx.strokeStyle = layout.lineColor;
7994
+ ctx.lineWidth = ship.modelKey === "cutter" ? 2.2 : 3.5;
7592
7995
  ctx.beginPath();
7593
- ctx.moveTo(projected[0].x, projected[0].y);
7594
- ctx.lineTo(projected[1].x, projected[1].y);
7595
- ctx.moveTo(projected[2].x, projected[2].y);
7596
- ctx.lineTo(projected[3].x, projected[3].y);
7996
+ for (const [from, to] of layout.mastPairs) {
7997
+ ctx.moveTo(projected[from].x, projected[from].y);
7998
+ ctx.lineTo(projected[to].x, projected[to].y);
7999
+ }
7597
8000
  ctx.stroke();
7598
- ctx.fillStyle = "rgba(238, 232, 214, 0.88)";
8001
+ const [a, b, c] = layout.sailTriangle;
8002
+ ctx.fillStyle = layout.sailColor;
7599
8003
  ctx.beginPath();
7600
- ctx.moveTo(projected[4].x, projected[4].y);
7601
- ctx.lineTo(projected[5].x, projected[5].y);
7602
- ctx.lineTo(projected[6].x, projected[6].y);
8004
+ ctx.moveTo(projected[a].x, projected[a].y);
8005
+ ctx.lineTo(projected[b].x, projected[b].y);
8006
+ ctx.lineTo(projected[c].x, projected[c].y);
7603
8007
  ctx.closePath();
7604
8008
  ctx.fill();
7605
8009
  }
@@ -7817,10 +8221,10 @@ function resolveBoundaryCollision(ship, state, shipModel) {
7817
8221
  }
7818
8222
  }
7819
8223
  }
7820
- function resolveShipCollision(state, a, b, shipModel) {
8224
+ function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
7821
8225
  const delta = subVec3(b.position, a.position);
7822
- const radiusA = getShipCollisionRadius(a, shipModel);
7823
- const radiusB = getShipCollisionRadius(b, shipModel);
8226
+ const radiusA = getShipCollisionRadius(a, shipModelA);
8227
+ const radiusB = getShipCollisionRadius(b, shipModelB);
7824
8228
  const distance = Math.hypot(delta.x, delta.z);
7825
8229
  const minDistance = radiusA + radiusB;
7826
8230
  if (distance >= minDistance) {
@@ -7829,15 +8233,15 @@ function resolveShipCollision(state, a, b, shipModel) {
7829
8233
  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)));
7830
8234
  const tangent = vec3(-normal.z, 0, normal.x);
7831
8235
  const penetration = minDistance - distance;
7832
- const invMassA = getShipInverseMass(a, shipModel);
7833
- const invMassB = getShipInverseMass(b, shipModel);
8236
+ const invMassA = getShipInverseMass(a, shipModelA);
8237
+ const invMassB = getShipInverseMass(b, shipModelB);
7834
8238
  const invMassSum = invMassA + invMassB;
7835
8239
  const correction = scaleVec3(normal, penetration / Math.max(1e-4, invMassSum) * 0.72);
7836
8240
  a.position = subVec3(a.position, scaleVec3(correction, invMassA));
7837
8241
  b.position = addVec3(b.position, scaleVec3(correction, invMassB));
7838
8242
  const relativeVelocity = subVec3(b.velocity, a.velocity);
7839
8243
  const velocityAlongNormal = dotVec3(relativeVelocity, normal);
7840
- const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.88;
8244
+ const restitution = (readPhysicsNumber(shipModelA.physics, "restitution", 0.22) + readPhysicsNumber(shipModelB.physics, "restitution", 0.22)) / 2 * 0.88;
7841
8245
  if (velocityAlongNormal < 0) {
7842
8246
  const impulseMagnitude = -(1 + restitution) * velocityAlongNormal / Math.max(1e-4, invMassSum);
7843
8247
  const impulse = scaleVec3(normal, impulseMagnitude);
@@ -7852,8 +8256,8 @@ function resolveShipCollision(state, a, b, shipModel) {
7852
8256
  const frictionImpulse = scaleVec3(tangent, frictionMagnitude);
7853
8257
  a.velocity = subVec3(a.velocity, scaleVec3(frictionImpulse, invMassA));
7854
8258
  b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
7855
- a.angularVelocity -= tangentSpeed * radiusA * getShipInverseInertia(a, shipModel) * 0.2 + impulseMagnitude * 24e-5;
7856
- b.angularVelocity += tangentSpeed * radiusB * getShipInverseInertia(b, shipModel) * 0.2 + impulseMagnitude * 24e-5;
8259
+ a.angularVelocity -= tangentSpeed * radiusA * getShipInverseInertia(a, shipModelA) * 0.2 + impulseMagnitude * 24e-5;
8260
+ b.angularVelocity += tangentSpeed * radiusB * getShipInverseInertia(b, shipModelB) * 0.2 + impulseMagnitude * 24e-5;
7857
8261
  const impactSpeed = Math.abs(velocityAlongNormal);
7858
8262
  if (impactSpeed > 0.18 && Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0) {
7859
8263
  const contactPoint = vec3(
@@ -7885,12 +8289,17 @@ function updateShips(state, dt, shipModel) {
7885
8289
  let collided = false;
7886
8290
  state.contactCount = 0;
7887
8291
  for (const ship of state.ships) {
7888
- updateShipMotion(state, ship, dt, shipModel);
7889
- resolveBoundaryCollision(ship, state, shipModel);
8292
+ const activeShipModel = resolveShipModel(state, ship, shipModel);
8293
+ updateShipMotion(state, ship, dt, activeShipModel);
8294
+ resolveBoundaryCollision(ship, state, activeShipModel);
7890
8295
  }
7891
8296
  for (let index = 0; index < state.ships.length; index += 1) {
7892
8297
  for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
7893
- collided = resolveShipCollision(state, state.ships[index], state.ships[otherIndex], shipModel) || collided;
8298
+ const shipA = state.ships[index];
8299
+ const shipB = state.ships[otherIndex];
8300
+ const shipModelA = resolveShipModel(state, shipA, shipModel);
8301
+ const shipModelB = resolveShipModel(state, shipB, shipModel);
8302
+ collided = resolveShipCollision(state, shipA, shipB, shipModelA, shipModelB) || collided;
7894
8303
  }
7895
8304
  }
7896
8305
  state.collisionFlash = collided ? Math.max(0.12, state.collisionFlash) : Math.max(0, state.collisionFlash - dt * 1.3);
@@ -8154,6 +8563,101 @@ function renderWaterLightReflection(ctx, source, state, camera, viewport) {
8154
8563
  ctx.fill();
8155
8564
  ctx.restore();
8156
8565
  }
8566
+ function renderLighthouseBeam(ctx, state, camera, viewport, visuals) {
8567
+ const lighthousePlacement = SHOWCASE_ENVIRONMENT_LAYOUT.find(
8568
+ (placement) => placement.assetKey === "lighthouse"
8569
+ );
8570
+ if (!lighthousePlacement || !state.showcaseRealisticModelsEnabled) {
8571
+ return;
8572
+ }
8573
+ const source = transformPoint(
8574
+ vec3(0, 11.34, 0),
8575
+ {
8576
+ position: vec3(
8577
+ lighthousePlacement.position.x,
8578
+ lighthousePlacement.position.y,
8579
+ lighthousePlacement.position.z
8580
+ ),
8581
+ rotationY: lighthousePlacement.rotationY,
8582
+ scale: lighthousePlacement.scale
8583
+ }
8584
+ );
8585
+ const sweep = state.time * 0.22 + 0.8;
8586
+ const direction = normalizeVec3(vec3(Math.sin(sweep), -0.07, Math.cos(sweep)));
8587
+ const spread = perpendicularOnWater(direction);
8588
+ const farCenter = addVec3(source, scaleVec3(direction, 34));
8589
+ const left = addVec3(farCenter, scaleVec3(spread, 7.4));
8590
+ const right = addVec3(farCenter, scaleVec3(spread, -7.4));
8591
+ const projectedSource = projectPoint(source, camera, viewport);
8592
+ const projectedLeft = projectPoint(left, camera, viewport);
8593
+ const projectedRight = projectPoint(right, camera, viewport);
8594
+ if (!projectedSource || !projectedLeft || !projectedRight) {
8595
+ return;
8596
+ }
8597
+ const pulse = 0.72 + Math.sin(state.time * 1.7) * 0.08;
8598
+ ctx.save();
8599
+ ctx.globalCompositeOperation = "screen";
8600
+ ctx.fillStyle = colorToRgba(visuals.torchCore, 0.055 * pulse);
8601
+ ctx.beginPath();
8602
+ ctx.moveTo(projectedSource.x, projectedSource.y);
8603
+ ctx.lineTo(projectedLeft.x, projectedLeft.y);
8604
+ ctx.lineTo(projectedRight.x, projectedRight.y);
8605
+ ctx.closePath();
8606
+ ctx.fill();
8607
+ const beamLength = Math.hypot(
8608
+ projectedLeft.x - projectedSource.x,
8609
+ projectedLeft.y - projectedSource.y
8610
+ );
8611
+ const core = ctx.createRadialGradient(
8612
+ projectedSource.x,
8613
+ projectedSource.y,
8614
+ 2,
8615
+ projectedSource.x,
8616
+ projectedSource.y,
8617
+ clamp(beamLength * 0.22, 18, 80)
8618
+ );
8619
+ core.addColorStop(0, colorToRgba(visuals.torchCore, 0.58));
8620
+ core.addColorStop(0.5, colorToRgba(visuals.torchGlow, 0.18));
8621
+ core.addColorStop(1, colorToRgba(visuals.torchGlow, 0));
8622
+ ctx.fillStyle = core;
8623
+ ctx.beginPath();
8624
+ ctx.arc(projectedSource.x, projectedSource.y, clamp(beamLength * 0.18, 14, 64), 0, Math.PI * 2);
8625
+ ctx.fill();
8626
+ ctx.restore();
8627
+ }
8628
+ function renderAtmosphericGrade(ctx, canvas, state, visuals) {
8629
+ const vignette = ctx.createRadialGradient(
8630
+ canvas.width * 0.5,
8631
+ canvas.height * 0.48,
8632
+ canvas.width * 0.2,
8633
+ canvas.width * 0.5,
8634
+ canvas.height * 0.5,
8635
+ canvas.width * 0.72
8636
+ );
8637
+ vignette.addColorStop(0, "rgba(0, 0, 0, 0)");
8638
+ vignette.addColorStop(0.68, "rgba(0, 0, 0, 0.08)");
8639
+ vignette.addColorStop(1, "rgba(0, 0, 0, 0.32)");
8640
+ ctx.fillStyle = vignette;
8641
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
8642
+ const seaHaze = ctx.createLinearGradient(0, canvas.height * 0.34, 0, canvas.height);
8643
+ seaHaze.addColorStop(0, "rgba(0, 0, 0, 0)");
8644
+ seaHaze.addColorStop(0.5, visuals.ambientMist);
8645
+ seaHaze.addColorStop(1, "rgba(3, 8, 16, 0.18)");
8646
+ ctx.fillStyle = seaHaze;
8647
+ ctx.fillRect(0, canvas.height * 0.34, canvas.width, canvas.height * 0.66);
8648
+ if (state.captureMode) {
8649
+ ctx.save();
8650
+ ctx.globalCompositeOperation = "screen";
8651
+ for (let index = 0; index < 70; index += 1) {
8652
+ const x = pseudoRandom(index * 19 + 3) * canvas.width;
8653
+ const y = pseudoRandom(index * 23 + 7) * canvas.height;
8654
+ const alpha = 8e-3 + pseudoRandom(index * 31 + 11) * 0.012;
8655
+ ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
8656
+ ctx.fillRect(x, y, 1.1, 1.1);
8657
+ }
8658
+ ctx.restore();
8659
+ }
8660
+ }
8157
8661
  function renderWaterMotionEffects(ctx, effects, camera, viewport) {
8158
8662
  ctx.save();
8159
8663
  ctx.globalCompositeOperation = "screen";
@@ -8272,13 +8776,22 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8272
8776
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
8273
8777
  normal,
8274
8778
  baseColor: bandMesh.color,
8275
- accent: bandAccent
8779
+ accent: bandAccent,
8780
+ material: {
8781
+ name: "water-surface",
8782
+ color: bandMesh.color,
8783
+ roughness: 0.2,
8784
+ metallic: 0,
8785
+ emissive: { r: 0, g: 0, b: 0 }
8786
+ },
8787
+ reflection: 1,
8788
+ surfaceType: "water"
8276
8789
  });
8277
8790
  }
8278
8791
  }
8279
8792
  const waterMotionEffects = buildWaterMotionEffects(state);
8280
8793
  const lightSources = collectSceneLightSources(state, visuals);
8281
- pushHarborGeometry(camera, viewport, sceneTriangles, visuals);
8794
+ pushHarborGeometry(camera, viewport, sceneTriangles, state);
8282
8795
  const cloth = buildClothSurface(
8283
8796
  state,
8284
8797
  state,
@@ -8300,23 +8813,46 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8300
8813
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
8301
8814
  normal,
8302
8815
  baseColor: cloth.color,
8303
- accent: cloth.band === "near" ? 0.1 : 0.04
8816
+ accent: cloth.band === "near" ? 0.1 : 0.04,
8817
+ material: {
8818
+ name: "flag-cloth",
8819
+ color: cloth.color,
8820
+ roughness: 0.94,
8821
+ metallic: 0,
8822
+ emissive: { r: 0, g: 0, b: 0 }
8823
+ },
8824
+ reflection: 0,
8825
+ surfaceType: "cloth"
8304
8826
  });
8305
8827
  }
8306
8828
  for (const ship of state.ships) {
8829
+ const activeShipModel = resolveShipModel(state, ship, shipModel);
8307
8830
  buildTrianglesFromMesh(
8308
- shipModel,
8831
+ activeShipModel,
8309
8832
  { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
8310
8833
  ship.tint,
8311
8834
  camera,
8312
8835
  viewport,
8313
8836
  sceneTriangles,
8314
- nearLighting.rtParticipation.directShadows === "premium" ? 0.08 : 0.02
8837
+ {
8838
+ accent: nearLighting.rtParticipation.directShadows === "premium" ? 0.08 : 0.02,
8839
+ reflection: 0,
8840
+ surfaceType: "ship"
8841
+ }
8315
8842
  );
8316
8843
  }
8317
8844
  drawTriangles(ctx, waterTriangles, lightDir, reflectionStrength, camera, shadowStrength);
8318
8845
  for (const ship of state.ships) {
8319
- renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength);
8846
+ renderShipShadow(
8847
+ ctx,
8848
+ resolveShipModel(state, ship, shipModel),
8849
+ ship,
8850
+ state,
8851
+ camera,
8852
+ viewport,
8853
+ lightDir,
8854
+ shadowStrength
8855
+ );
8320
8856
  }
8321
8857
  renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
8322
8858
  for (const source of lightSources.reflectionLights) {
@@ -8324,9 +8860,18 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8324
8860
  }
8325
8861
  renderWaterMotionEffects(ctx, waterMotionEffects, camera, viewport);
8326
8862
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
8327
- drawTriangles(ctx, sceneTriangles, lightDir, reflectionStrength, camera, shadowStrength);
8863
+ drawTriangles(
8864
+ ctx,
8865
+ sceneTriangles,
8866
+ lightDir,
8867
+ reflectionStrength,
8868
+ camera,
8869
+ shadowStrength,
8870
+ lightSources.directLights
8871
+ );
8328
8872
  renderFlagPole(ctx, camera, viewport);
8329
8873
  renderClothAccent(ctx, cloth, camera, viewport);
8874
+ renderLighthouseBeam(ctx, state, camera, viewport, visuals);
8330
8875
  for (const source of lightSources.directLights) {
8331
8876
  renderDirectLightGlow(ctx, source, camera, viewport);
8332
8877
  }
@@ -8334,6 +8879,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8334
8879
  renderShipRigging(ctx, ship, camera, viewport);
8335
8880
  }
8336
8881
  renderSprays(ctx, state.sprays, camera, viewport);
8882
+ renderAtmosphericGrade(ctx, canvas, state, visuals);
8337
8883
  const debugSnapshot = state.debugSession.getSnapshot();
8338
8884
  const quality = {
8339
8885
  fluid: state.fluidDetail.getSnapshot(),
@@ -8342,11 +8888,11 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8342
8888
  };
8343
8889
  const sceneMetrics = [
8344
8890
  `focus: ${state.focus}`,
8345
- `ships: ${state.ships.length} active GLTF hulls`,
8346
- `moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.length * SHIP_LANTERNS.length} warm deck and harbor lights`,
8891
+ `ships: ${state.ships.length} active GLTF hulls across ${new Set(state.ships.map((ship) => ship.modelKey)).size} model families`,
8892
+ `moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.reduce((total, ship) => total + (Array.isArray(ship.lanterns) ? ship.lanterns.length : 0), 0)} warm deck and harbor lights`,
8347
8893
  `physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
8348
8894
  `physics contacts: ${state.contactCount}`,
8349
- `mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, shipModel) / 1e3).toFixed(1)}t`).join(" \xB7 ")}`,
8895
+ `mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, resolveShipModel(state, ship, shipModel)) / 1e3).toFixed(1)}t`).join(" \xB7 ")}`,
8350
8896
  `cloth band: ${cloth.band} -> ${cloth.representation.output}`,
8351
8897
  `fluid near band: ${water.bandMeshes[0].representation.output}`,
8352
8898
  `lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`
@@ -8367,11 +8913,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8367
8913
  `pipeline samples: ${debugSnapshot.pipeline.sampleCount}`,
8368
8914
  `tracked memory: ${(debugSnapshot.memory.totalTrackedBytes / (1024 * 1024)).toFixed(1)} MB`
8369
8915
  ];
8370
- const sceneNotes = state.focus === "physics" ? [
8371
- "Stable world snapshots are taken after the authoritative rigid-body commit and before visual follow-up work.",
8372
- "The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
8373
- "Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water."
8374
- ] : SCENE_NOTES;
8916
+ const sceneNotes = state.focus === "physics" ? PHYSICS_SCENE_NOTE_KEYS.map((key) => state.translate(key)) : SCENE_NOTE_KEYS.map((key) => state.translate(key));
8375
8917
  const custom = state.demoDescription ?? null;
8376
8918
  setListContent(
8377
8919
  dom.sceneMetrics,
@@ -8386,8 +8928,16 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8386
8928
  Array.isArray(custom?.debugMetrics) ? custom.debugMetrics : debugMetrics
8387
8929
  );
8388
8930
  setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
8389
- dom.status.textContent = typeof custom?.status === "string" ? custom.status : `3D scene live \xB7 ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
8390
- 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}.`;
8931
+ dom.status.textContent = typeof custom?.status === "string" ? custom.status : state.translate(gpuSharedTranslationKeys.statusLive, {
8932
+ fps: state.lastDecision.metrics.fps.toFixed(1)
8933
+ });
8934
+ dom.details.textContent = typeof custom?.details === "string" ? custom.details : state.focus === "physics" ? state.translate(gpuSharedTranslationKeys.detailsPhysics, {
8935
+ snapshotStageId: state.physics.plan.snapshotStageId
8936
+ }) : state.showcaseRealisticModelsEnabled ? state.translate(gpuSharedTranslationKeys.detailsRealistic, {
8937
+ pressureLevel: state.lastDecision.pressureLevel
8938
+ }) : state.translate(gpuSharedTranslationKeys.detailsLegacy, {
8939
+ pressureLevel: state.lastDecision.pressureLevel
8940
+ });
8391
8941
  }
8392
8942
  function updateSceneState(state, dt, shipModel) {
8393
8943
  updateShips(state, dt, shipModel);
@@ -8417,15 +8967,18 @@ function syncTextState(state, shipModel) {
8417
8967
  stress: state.stress,
8418
8968
  ships: state.ships.map((ship) => ({
8419
8969
  id: ship.id,
8970
+ modelKey: ship.modelKey ?? "brigantine",
8420
8971
  x: Number(ship.position.x.toFixed(2)),
8421
8972
  y: Number(ship.position.y.toFixed(2)),
8422
8973
  z: Number(ship.position.z.toFixed(2)),
8423
8974
  vx: Number(ship.velocity.x.toFixed(2)),
8424
8975
  vz: Number(ship.velocity.z.toFixed(2)),
8425
- massKg: Math.round(getShipMass(ship, shipModel)),
8976
+ massKg: Math.round(getShipMass(ship, resolveShipModel(state, ship, shipModel))),
8426
8977
  lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0
8427
8978
  })),
8428
- shipPhysics: shipModel.physics,
8979
+ shipPhysics: Object.fromEntries(
8980
+ state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
8981
+ ),
8429
8982
  sprays: state.sprays.length,
8430
8983
  waveImpulses: state.waveImpulses.length,
8431
8984
  pressure: state.lastDecision?.pressureLevel ?? "stable",
@@ -8448,22 +9001,36 @@ function syncTextState(state, shipModel) {
8448
9001
  }
8449
9002
  };
8450
9003
  }
8451
- async function mountGpuShowcase(options = {}) {
9004
+ async function mountGpuShowcase(options = {}, featureFlags = null) {
8452
9005
  injectStyles();
8453
9006
  const root = options.root ?? document.body;
8454
9007
  root.classList?.add?.(ROOT_CLASS);
9008
+ const captureSettings = resolveCaptureSettings(options);
9009
+ if (captureSettings.captureMode) {
9010
+ root.classList?.add?.(CAPTURE_CLASS);
9011
+ }
8455
9012
  const previousMarkup = root.innerHTML;
8456
9013
  const previousRenderGameToText = window.render_game_to_text;
8457
9014
  const previousAdvanceTime = window.advanceTime;
8458
9015
  const focus = options.focus ?? new URLSearchParams(window.location.search).get("focus") ?? "integrated";
9016
+ const translate = createGpuSharedTranslator(options.translate);
8459
9017
  const dom = buildDemoDom(root, {
8460
9018
  packageName: options.packageName ?? "@plasius/gpu-demo-viewer",
8461
- title: options.title ?? DEFAULT_TITLE,
8462
- subtitle: options.subtitle ?? DEFAULT_SUBTITLE
9019
+ title: options.title ?? translate(gpuSharedTranslationKeys.showcaseTitle),
9020
+ subtitle: options.subtitle ?? translate(gpuSharedTranslationKeys.showcaseSubtitle),
9021
+ translate
8463
9022
  });
8464
9023
  dom.focusMode.value = focus;
8465
- const state = createSceneState({ focus });
8466
- const shipModel = await loadGltfModel(resolveShowcaseAssetUrl());
9024
+ const state = createSceneState({
9025
+ focus,
9026
+ translate,
9027
+ realisticModelsEnabled: isFeatureEnabled(featureFlags, GPU_SHOWCASE_REALISTIC_MODELS_FEATURE, true),
9028
+ captureMode: captureSettings.captureMode,
9029
+ renderScale: captureSettings.renderScale
9030
+ });
9031
+ const assetCatalog = await (state.showcaseRealisticModelsEnabled ? loadShowcaseAssetCatalog() : createLegacyShowcaseAssetCatalog());
9032
+ const shipModel = assetCatalog.ships[assetCatalog.primaryShipKey];
9033
+ state.assetCatalog = assetCatalog;
8467
9034
  state.shipModel = shipModel;
8468
9035
  state.packageState = typeof options.createState === "function" ? options.createState() : void 0;
8469
9036
  updatePhysicsSnapshot(state, shipModel);
@@ -8474,6 +9041,8 @@ async function mountGpuShowcase(options = {}) {
8474
9041
  if (!ctx) {
8475
9042
  throw new Error("2D canvas context is required for the shared showcase.");
8476
9043
  }
9044
+ ctx.imageSmoothingEnabled = true;
9045
+ ctx.imageSmoothingQuality = "high";
8477
9046
  let animationFrameId = 0;
8478
9047
  let destroyed = false;
8479
9048
  const renderFrame = (nowMs) => {
@@ -8494,13 +9063,14 @@ async function mountGpuShowcase(options = {}) {
8494
9063
  state.lastDecision = recordTelemetry(state, syntheticFrame);
8495
9064
  }
8496
9065
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
9066
+ resizeCanvasToDisplaySize(dom.canvas, state);
8497
9067
  renderScene(ctx, dom.canvas, state, shipModel, dom);
8498
9068
  syncTextState(state, shipModel);
8499
9069
  animationFrameId = requestAnimationFrame(renderFrame);
8500
9070
  };
8501
9071
  const handlePauseClick = () => {
8502
9072
  state.paused = !state.paused;
8503
- dom.pauseButton.textContent = state.paused ? "Resume" : "Pause";
9073
+ dom.pauseButton.textContent = state.paused ? state.translate(gpuSharedTranslationKeys.resume) : state.translate(gpuSharedTranslationKeys.pause);
8504
9074
  };
8505
9075
  const handleStressChange = () => {
8506
9076
  state.stress = dom.stressToggle.checked;
@@ -8535,6 +9105,7 @@ async function mountGpuShowcase(options = {}) {
8535
9105
  state.packageState = void 0;
8536
9106
  }
8537
9107
  root.classList?.remove?.(ROOT_CLASS);
9108
+ root.classList?.remove?.(CAPTURE_CLASS);
8538
9109
  root.innerHTML = previousMarkup;
8539
9110
  if (typeof previousRenderGameToText === "function") {
8540
9111
  window.render_game_to_text = previousRenderGameToText;
@@ -8555,6 +9126,12 @@ async function mountGpuShowcase(options = {}) {
8555
9126
  };
8556
9127
  }
8557
9128
  function updatePhysicsSnapshot(state, shipModel) {
9129
+ const rigidBodyShapes = Object.fromEntries(
9130
+ state.ships.map((ship) => [
9131
+ ship.id,
9132
+ resolveShipModel(state, ship, shipModel)?.physics?.shape ?? "box"
9133
+ ])
9134
+ );
8558
9135
  state.physics.snapshot = createPhysicsWorldSnapshot({
8559
9136
  frameId: `showcase-${state.frame}`,
8560
9137
  tick: state.frame,
@@ -8570,16 +9147,18 @@ function updatePhysicsSnapshot(state, shipModel) {
8570
9147
  collisionCount: state.collisionCount,
8571
9148
  contactCount: state.contactCount,
8572
9149
  snapshotStageId: state.physics.plan.snapshotStageId,
8573
- rigidBodyShape: shipModel.physics.shape ?? "box"
9150
+ rigidBodyShape: shipModel.physics.shape ?? "box",
9151
+ rigidBodyShapes
8574
9152
  }
8575
9153
  });
8576
9154
  }
8577
9155
  export {
8578
9156
  advanceShowcaseClothSimulationState as __testOnlyAdvanceShowcaseClothSimulationState,
9157
+ buildWaterBands as __testOnlyBuildWaterBands,
8579
9158
  buildWaterMotionEffects as __testOnlyBuildWaterMotionEffects,
8580
9159
  collectSceneLightSources as __testOnlyCollectSceneLightSources,
8581
9160
  createShowcaseClothSimulationState as __testOnlyCreateShowcaseClothSimulationState,
8582
9161
  mountGpuShowcase,
8583
9162
  showcaseFocusModes
8584
9163
  };
8585
- //# sourceMappingURL=showcase-runtime-55OVDYWT.js.map
9164
+ //# sourceMappingURL=showcase-runtime-PN7N3FZY.js.map