@plasius/gpu-shared 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +36 -3
  2. package/README.md +55 -1
  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 +1255 -279
  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-2ZNPKD7D.js → showcase-runtime-PN7N3FZY.js} +808 -237
  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 +912 -188
  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-2ZNPKD7D.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",
@@ -7319,6 +7632,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
7319
7632
  return { fluidPlan, bandMeshes };
7320
7633
  }
7321
7634
  function createSceneState(options) {
7635
+ const translate = options.translate;
7322
7636
  const { governor, fluidDetail, clothDetail, lightingDetail } = createPerformanceGovernor();
7323
7637
  const physicsProfile = defaultPhysicsWorkerProfile;
7324
7638
  const physicsPlan = createPhysicsSimulationPlan(physicsProfile);
@@ -7326,7 +7640,7 @@ function createSceneState(options) {
7326
7640
  const debugSession = createGpuDebugSession({
7327
7641
  enabled: true,
7328
7642
  adapter: {
7329
- label: "3D showcase",
7643
+ label: translate(gpuSharedTranslationKeys.debugAdapterShowcase),
7330
7644
  memoryCapacityHintBytes: 6 * 1024 * 1024 * 1024,
7331
7645
  coreCountHint: 12
7332
7646
  }
@@ -7336,22 +7650,26 @@ function createSceneState(options) {
7336
7650
  owner: "renderer",
7337
7651
  category: "texture",
7338
7652
  sizeBytes: 1280 * 720 * 4,
7339
- label: "Main color buffer"
7653
+ label: translate(gpuSharedTranslationKeys.debugMainColorBuffer)
7340
7654
  });
7341
7655
  debugSession.trackAllocation({
7342
7656
  id: "showcase.shadow-impression",
7343
7657
  owner: "lighting",
7344
7658
  category: "texture",
7345
7659
  sizeBytes: 12 * 1024 * 1024,
7346
- label: "Shadow impression atlas"
7660
+ label: translate(gpuSharedTranslationKeys.debugShadowImpressionAtlas)
7347
7661
  });
7348
7662
  return {
7663
+ translate,
7349
7664
  focus: options.focus,
7350
7665
  governor,
7351
7666
  fluidDetail,
7352
7667
  clothDetail,
7353
7668
  lightingDetail,
7354
7669
  debugSession,
7670
+ showcaseRealisticModelsEnabled: options.realisticModelsEnabled !== false,
7671
+ captureMode: options.captureMode === true,
7672
+ renderScale: readPositiveNumber3(options.renderScale, void 0),
7355
7673
  packageState: void 0,
7356
7674
  demoDescription: null,
7357
7675
  demoVisuals: null,
@@ -7366,6 +7684,7 @@ function createSceneState(options) {
7366
7684
  ships: [
7367
7685
  {
7368
7686
  id: "northwind",
7687
+ modelKey: "brigantine",
7369
7688
  position: vec3(-5.2, 0, 7.2),
7370
7689
  velocity: vec3(2.35, 0, -1.08),
7371
7690
  rotationY: 0.58,
@@ -7376,17 +7695,18 @@ function createSceneState(options) {
7376
7695
  throttleResponse: 0.46,
7377
7696
  rudderResponse: 0.54,
7378
7697
  wanderPhase: 0.35,
7379
- lanterns: SHIP_LANTERNS,
7698
+ lanterns: CUTTER_LANTERNS,
7380
7699
  lanternStrength: 1.06,
7381
7700
  collisionRadiusScale: 1.04
7382
7701
  },
7383
7702
  {
7384
7703
  id: "tidecaller",
7704
+ modelKey: "cutter",
7385
7705
  position: vec3(4.8, 0, 4.4),
7386
7706
  velocity: vec3(-2.15, 0, 1.74),
7387
7707
  rotationY: -2.48,
7388
7708
  angularVelocity: -0.2,
7389
- tint: { r: 0.48, g: 0.28, b: 0.19 },
7709
+ tint: { r: 0.58, g: 0.24, b: 0.16 },
7390
7710
  massScale: 0.84,
7391
7711
  cruiseSpeed: 2.68,
7392
7712
  throttleResponse: 0.7,
@@ -7410,6 +7730,7 @@ function createSceneState(options) {
7410
7730
  manifest: physicsManifest,
7411
7731
  snapshot: null
7412
7732
  },
7733
+ assetCatalog: null,
7413
7734
  shipModel: null
7414
7735
  };
7415
7736
  }
@@ -7485,10 +7806,38 @@ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, s
7485
7806
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7486
7807
  }
7487
7808
  }
7488
- 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 = []) {
7489
7833
  triangles.sort((left, right) => right.depth - left.depth);
7490
7834
  for (const triangle of triangles) {
7491
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
+ };
7492
7841
  const shaded = shadeColor(
7493
7842
  triangle.baseColor,
7494
7843
  surfaceNormal,
@@ -7496,19 +7845,41 @@ function drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, sha
7496
7845
  clamp((triangle.worldCenter.y + 3) / 10, 0, 1),
7497
7846
  triangle.accent
7498
7847
  );
7499
- const reflection = triangle.worldCenter.y < 0.8 ? reflectionStrength : 0;
7848
+ const reflection = reflectionStrength * (triangle.reflection ?? 0);
7500
7849
  const viewDir = normalizeVec3(subVec3(camera.eye, triangle.worldCenter));
7501
7850
  const reflectedLight = reflectVec3(scaleVec3(lightDir, -1), surfaceNormal);
7502
- const gloss = triangle.worldCenter.y < 0.9 ? 1 : triangle.accent > 0.05 ? 0.55 : 0.3;
7503
- const specular = Math.pow(clamp(dotVec3(reflectedLight, viewDir), 0, 1), triangle.worldCenter.y < 0.9 ? 18 : 12) * gloss;
7504
- const occlusion = triangle.worldCenter.y < 0.9 ? shadowStrength * 0.035 : 0;
7505
- 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(
7506
7858
  {
7507
- r: clamp(shaded.r + reflection * 0.08 + specular * 0.14 - occlusion, 0, 1),
7508
- g: clamp(shaded.g + reflection * 0.08 + specular * 0.15 - occlusion, 0, 1),
7509
- 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
+ )
7510
7874
  },
7511
- 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
7512
7883
  );
7513
7884
  ctx.fillStyle = fill;
7514
7885
  ctx.beginPath();
@@ -7539,74 +7910,100 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
7539
7910
  ctx.fill();
7540
7911
  ctx.restore();
7541
7912
  }
7542
- function pushHarborGeometry(camera, viewport, triangles, visuals) {
7543
- const harborObjects = [
7544
- {
7545
- position: vec3(-8.2, 1.1, -0.9),
7546
- rotationY: -0.16,
7547
- scale: { x: 5.4, y: 2.4, z: 4.2 },
7548
- color: visuals.harborWall,
7549
- accent: 0.06
7550
- },
7551
- {
7552
- position: vec3(-5.7, 0.45, 1.4),
7553
- rotationY: -0.08,
7554
- scale: { x: 6.8, y: 0.3, z: 2.1 },
7555
- color: visuals.harborDeck,
7556
- accent: 0.04
7557
- },
7558
- {
7559
- position: vec3(-10.4, 0.28, 0.8),
7560
- rotationY: 0.22,
7561
- scale: { x: 1.2, y: 0.9, z: 1.2 },
7562
- color: visuals.harborTower,
7563
- 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;
7564
7940
  }
7565
- ];
7566
- for (const object of harborObjects) {
7567
7941
  buildTrianglesFromMesh(
7568
- UNIT_BOX_MESH,
7942
+ mesh,
7569
7943
  {
7570
- position: object.position,
7571
- rotationY: object.rotationY,
7572
- scale: object.scale
7944
+ position: vec3(placement.position.x, placement.position.y, placement.position.z),
7945
+ rotationY: placement.rotationY,
7946
+ scale: placement.scale
7573
7947
  },
7574
- object.color,
7948
+ null,
7575
7949
  camera,
7576
7950
  viewport,
7577
7951
  triangles,
7578
- object.accent
7952
+ {
7953
+ accent: placement.accent,
7954
+ reflection: 0,
7955
+ surfaceType: "structure"
7956
+ }
7579
7957
  );
7580
7958
  }
7581
7959
  }
7582
7960
  function renderShipRigging(ctx, ship, camera, viewport) {
7583
7961
  const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
7584
- const mastBase = transformPoint(vec3(0, 0.38, -0.4), transform);
7585
- const mastTop = transformPoint(vec3(0, 3.8, -0.2), transform);
7586
- const aftBase = transformPoint(vec3(-0.25, 0.32, -1.9), transform);
7587
- const aftTop = transformPoint(vec3(-0.15, 2.7, -1.75), transform);
7588
- const sailA = transformPoint(vec3(0.08, 3.2, -0.2), transform);
7589
- const sailB = transformPoint(vec3(0.12, 1.2, -0.5), transform);
7590
- const sailC = transformPoint(vec3(2.25, 2.25, 0.15), transform);
7591
- const projected = [mastBase, mastTop, aftBase, aftTop, sailA, sailB, sailC].map(
7592
- (point) => projectPoint(point, camera, viewport)
7593
- );
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));
7594
7990
  if (projected.some((value) => value === null)) {
7595
7991
  return;
7596
7992
  }
7597
- ctx.strokeStyle = "rgba(73, 54, 45, 0.94)";
7598
- ctx.lineWidth = 3.5;
7993
+ ctx.strokeStyle = layout.lineColor;
7994
+ ctx.lineWidth = ship.modelKey === "cutter" ? 2.2 : 3.5;
7599
7995
  ctx.beginPath();
7600
- ctx.moveTo(projected[0].x, projected[0].y);
7601
- ctx.lineTo(projected[1].x, projected[1].y);
7602
- ctx.moveTo(projected[2].x, projected[2].y);
7603
- 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
+ }
7604
8000
  ctx.stroke();
7605
- ctx.fillStyle = "rgba(238, 232, 214, 0.88)";
8001
+ const [a, b, c] = layout.sailTriangle;
8002
+ ctx.fillStyle = layout.sailColor;
7606
8003
  ctx.beginPath();
7607
- ctx.moveTo(projected[4].x, projected[4].y);
7608
- ctx.lineTo(projected[5].x, projected[5].y);
7609
- 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);
7610
8007
  ctx.closePath();
7611
8008
  ctx.fill();
7612
8009
  }
@@ -7824,10 +8221,10 @@ function resolveBoundaryCollision(ship, state, shipModel) {
7824
8221
  }
7825
8222
  }
7826
8223
  }
7827
- function resolveShipCollision(state, a, b, shipModel) {
8224
+ function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
7828
8225
  const delta = subVec3(b.position, a.position);
7829
- const radiusA = getShipCollisionRadius(a, shipModel);
7830
- const radiusB = getShipCollisionRadius(b, shipModel);
8226
+ const radiusA = getShipCollisionRadius(a, shipModelA);
8227
+ const radiusB = getShipCollisionRadius(b, shipModelB);
7831
8228
  const distance = Math.hypot(delta.x, delta.z);
7832
8229
  const minDistance = radiusA + radiusB;
7833
8230
  if (distance >= minDistance) {
@@ -7836,15 +8233,15 @@ function resolveShipCollision(state, a, b, shipModel) {
7836
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)));
7837
8234
  const tangent = vec3(-normal.z, 0, normal.x);
7838
8235
  const penetration = minDistance - distance;
7839
- const invMassA = getShipInverseMass(a, shipModel);
7840
- const invMassB = getShipInverseMass(b, shipModel);
8236
+ const invMassA = getShipInverseMass(a, shipModelA);
8237
+ const invMassB = getShipInverseMass(b, shipModelB);
7841
8238
  const invMassSum = invMassA + invMassB;
7842
8239
  const correction = scaleVec3(normal, penetration / Math.max(1e-4, invMassSum) * 0.72);
7843
8240
  a.position = subVec3(a.position, scaleVec3(correction, invMassA));
7844
8241
  b.position = addVec3(b.position, scaleVec3(correction, invMassB));
7845
8242
  const relativeVelocity = subVec3(b.velocity, a.velocity);
7846
8243
  const velocityAlongNormal = dotVec3(relativeVelocity, normal);
7847
- 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;
7848
8245
  if (velocityAlongNormal < 0) {
7849
8246
  const impulseMagnitude = -(1 + restitution) * velocityAlongNormal / Math.max(1e-4, invMassSum);
7850
8247
  const impulse = scaleVec3(normal, impulseMagnitude);
@@ -7859,8 +8256,8 @@ function resolveShipCollision(state, a, b, shipModel) {
7859
8256
  const frictionImpulse = scaleVec3(tangent, frictionMagnitude);
7860
8257
  a.velocity = subVec3(a.velocity, scaleVec3(frictionImpulse, invMassA));
7861
8258
  b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
7862
- a.angularVelocity -= tangentSpeed * radiusA * getShipInverseInertia(a, shipModel) * 0.2 + impulseMagnitude * 24e-5;
7863
- 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;
7864
8261
  const impactSpeed = Math.abs(velocityAlongNormal);
7865
8262
  if (impactSpeed > 0.18 && Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0) {
7866
8263
  const contactPoint = vec3(
@@ -7892,12 +8289,17 @@ function updateShips(state, dt, shipModel) {
7892
8289
  let collided = false;
7893
8290
  state.contactCount = 0;
7894
8291
  for (const ship of state.ships) {
7895
- updateShipMotion(state, ship, dt, shipModel);
7896
- resolveBoundaryCollision(ship, state, shipModel);
8292
+ const activeShipModel = resolveShipModel(state, ship, shipModel);
8293
+ updateShipMotion(state, ship, dt, activeShipModel);
8294
+ resolveBoundaryCollision(ship, state, activeShipModel);
7897
8295
  }
7898
8296
  for (let index = 0; index < state.ships.length; index += 1) {
7899
8297
  for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
7900
- 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;
7901
8303
  }
7902
8304
  }
7903
8305
  state.collisionFlash = collided ? Math.max(0.12, state.collisionFlash) : Math.max(0, state.collisionFlash - dt * 1.3);
@@ -8161,6 +8563,101 @@ function renderWaterLightReflection(ctx, source, state, camera, viewport) {
8161
8563
  ctx.fill();
8162
8564
  ctx.restore();
8163
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
+ }
8164
8661
  function renderWaterMotionEffects(ctx, effects, camera, viewport) {
8165
8662
  ctx.save();
8166
8663
  ctx.globalCompositeOperation = "screen";
@@ -8279,13 +8776,22 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8279
8776
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
8280
8777
  normal,
8281
8778
  baseColor: bandMesh.color,
8282
- 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"
8283
8789
  });
8284
8790
  }
8285
8791
  }
8286
8792
  const waterMotionEffects = buildWaterMotionEffects(state);
8287
8793
  const lightSources = collectSceneLightSources(state, visuals);
8288
- pushHarborGeometry(camera, viewport, sceneTriangles, visuals);
8794
+ pushHarborGeometry(camera, viewport, sceneTriangles, state);
8289
8795
  const cloth = buildClothSurface(
8290
8796
  state,
8291
8797
  state,
@@ -8307,23 +8813,46 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8307
8813
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
8308
8814
  normal,
8309
8815
  baseColor: cloth.color,
8310
- 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"
8311
8826
  });
8312
8827
  }
8313
8828
  for (const ship of state.ships) {
8829
+ const activeShipModel = resolveShipModel(state, ship, shipModel);
8314
8830
  buildTrianglesFromMesh(
8315
- shipModel,
8831
+ activeShipModel,
8316
8832
  { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
8317
8833
  ship.tint,
8318
8834
  camera,
8319
8835
  viewport,
8320
8836
  sceneTriangles,
8321
- 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
+ }
8322
8842
  );
8323
8843
  }
8324
8844
  drawTriangles(ctx, waterTriangles, lightDir, reflectionStrength, camera, shadowStrength);
8325
8845
  for (const ship of state.ships) {
8326
- 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
+ );
8327
8856
  }
8328
8857
  renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
8329
8858
  for (const source of lightSources.reflectionLights) {
@@ -8331,9 +8860,18 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8331
8860
  }
8332
8861
  renderWaterMotionEffects(ctx, waterMotionEffects, camera, viewport);
8333
8862
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
8334
- 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
+ );
8335
8872
  renderFlagPole(ctx, camera, viewport);
8336
8873
  renderClothAccent(ctx, cloth, camera, viewport);
8874
+ renderLighthouseBeam(ctx, state, camera, viewport, visuals);
8337
8875
  for (const source of lightSources.directLights) {
8338
8876
  renderDirectLightGlow(ctx, source, camera, viewport);
8339
8877
  }
@@ -8341,6 +8879,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8341
8879
  renderShipRigging(ctx, ship, camera, viewport);
8342
8880
  }
8343
8881
  renderSprays(ctx, state.sprays, camera, viewport);
8882
+ renderAtmosphericGrade(ctx, canvas, state, visuals);
8344
8883
  const debugSnapshot = state.debugSession.getSnapshot();
8345
8884
  const quality = {
8346
8885
  fluid: state.fluidDetail.getSnapshot(),
@@ -8349,11 +8888,11 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8349
8888
  };
8350
8889
  const sceneMetrics = [
8351
8890
  `focus: ${state.focus}`,
8352
- `ships: ${state.ships.length} active GLTF hulls`,
8353
- `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`,
8354
8893
  `physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
8355
8894
  `physics contacts: ${state.contactCount}`,
8356
- `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 ")}`,
8357
8896
  `cloth band: ${cloth.band} -> ${cloth.representation.output}`,
8358
8897
  `fluid near band: ${water.bandMeshes[0].representation.output}`,
8359
8898
  `lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`
@@ -8374,11 +8913,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8374
8913
  `pipeline samples: ${debugSnapshot.pipeline.sampleCount}`,
8375
8914
  `tracked memory: ${(debugSnapshot.memory.totalTrackedBytes / (1024 * 1024)).toFixed(1)} MB`
8376
8915
  ];
8377
- const sceneNotes = state.focus === "physics" ? [
8378
- "Stable world snapshots are taken after the authoritative rigid-body commit and before visual follow-up work.",
8379
- "The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
8380
- "Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water."
8381
- ] : 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));
8382
8917
  const custom = state.demoDescription ?? null;
8383
8918
  setListContent(
8384
8919
  dom.sceneMetrics,
@@ -8393,8 +8928,16 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8393
8928
  Array.isArray(custom?.debugMetrics) ? custom.debugMetrics : debugMetrics
8394
8929
  );
8395
8930
  setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
8396
- dom.status.textContent = typeof custom?.status === "string" ? custom.status : `3D scene live \xB7 ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
8397
- 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
+ });
8398
8941
  }
8399
8942
  function updateSceneState(state, dt, shipModel) {
8400
8943
  updateShips(state, dt, shipModel);
@@ -8424,15 +8967,18 @@ function syncTextState(state, shipModel) {
8424
8967
  stress: state.stress,
8425
8968
  ships: state.ships.map((ship) => ({
8426
8969
  id: ship.id,
8970
+ modelKey: ship.modelKey ?? "brigantine",
8427
8971
  x: Number(ship.position.x.toFixed(2)),
8428
8972
  y: Number(ship.position.y.toFixed(2)),
8429
8973
  z: Number(ship.position.z.toFixed(2)),
8430
8974
  vx: Number(ship.velocity.x.toFixed(2)),
8431
8975
  vz: Number(ship.velocity.z.toFixed(2)),
8432
- massKg: Math.round(getShipMass(ship, shipModel)),
8976
+ massKg: Math.round(getShipMass(ship, resolveShipModel(state, ship, shipModel))),
8433
8977
  lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0
8434
8978
  })),
8435
- shipPhysics: shipModel.physics,
8979
+ shipPhysics: Object.fromEntries(
8980
+ state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
8981
+ ),
8436
8982
  sprays: state.sprays.length,
8437
8983
  waveImpulses: state.waveImpulses.length,
8438
8984
  pressure: state.lastDecision?.pressureLevel ?? "stable",
@@ -8455,22 +9001,36 @@ function syncTextState(state, shipModel) {
8455
9001
  }
8456
9002
  };
8457
9003
  }
8458
- async function mountGpuShowcase(options = {}) {
9004
+ async function mountGpuShowcase(options = {}, featureFlags = null) {
8459
9005
  injectStyles();
8460
9006
  const root = options.root ?? document.body;
8461
9007
  root.classList?.add?.(ROOT_CLASS);
9008
+ const captureSettings = resolveCaptureSettings(options);
9009
+ if (captureSettings.captureMode) {
9010
+ root.classList?.add?.(CAPTURE_CLASS);
9011
+ }
8462
9012
  const previousMarkup = root.innerHTML;
8463
9013
  const previousRenderGameToText = window.render_game_to_text;
8464
9014
  const previousAdvanceTime = window.advanceTime;
8465
9015
  const focus = options.focus ?? new URLSearchParams(window.location.search).get("focus") ?? "integrated";
9016
+ const translate = createGpuSharedTranslator(options.translate);
8466
9017
  const dom = buildDemoDom(root, {
8467
9018
  packageName: options.packageName ?? "@plasius/gpu-demo-viewer",
8468
- title: options.title ?? DEFAULT_TITLE,
8469
- subtitle: options.subtitle ?? DEFAULT_SUBTITLE
9019
+ title: options.title ?? translate(gpuSharedTranslationKeys.showcaseTitle),
9020
+ subtitle: options.subtitle ?? translate(gpuSharedTranslationKeys.showcaseSubtitle),
9021
+ translate
8470
9022
  });
8471
9023
  dom.focusMode.value = focus;
8472
- const state = createSceneState({ focus });
8473
- 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;
8474
9034
  state.shipModel = shipModel;
8475
9035
  state.packageState = typeof options.createState === "function" ? options.createState() : void 0;
8476
9036
  updatePhysicsSnapshot(state, shipModel);
@@ -8481,6 +9041,8 @@ async function mountGpuShowcase(options = {}) {
8481
9041
  if (!ctx) {
8482
9042
  throw new Error("2D canvas context is required for the shared showcase.");
8483
9043
  }
9044
+ ctx.imageSmoothingEnabled = true;
9045
+ ctx.imageSmoothingQuality = "high";
8484
9046
  let animationFrameId = 0;
8485
9047
  let destroyed = false;
8486
9048
  const renderFrame = (nowMs) => {
@@ -8501,13 +9063,14 @@ async function mountGpuShowcase(options = {}) {
8501
9063
  state.lastDecision = recordTelemetry(state, syntheticFrame);
8502
9064
  }
8503
9065
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
9066
+ resizeCanvasToDisplaySize(dom.canvas, state);
8504
9067
  renderScene(ctx, dom.canvas, state, shipModel, dom);
8505
9068
  syncTextState(state, shipModel);
8506
9069
  animationFrameId = requestAnimationFrame(renderFrame);
8507
9070
  };
8508
9071
  const handlePauseClick = () => {
8509
9072
  state.paused = !state.paused;
8510
- dom.pauseButton.textContent = state.paused ? "Resume" : "Pause";
9073
+ dom.pauseButton.textContent = state.paused ? state.translate(gpuSharedTranslationKeys.resume) : state.translate(gpuSharedTranslationKeys.pause);
8511
9074
  };
8512
9075
  const handleStressChange = () => {
8513
9076
  state.stress = dom.stressToggle.checked;
@@ -8542,6 +9105,7 @@ async function mountGpuShowcase(options = {}) {
8542
9105
  state.packageState = void 0;
8543
9106
  }
8544
9107
  root.classList?.remove?.(ROOT_CLASS);
9108
+ root.classList?.remove?.(CAPTURE_CLASS);
8545
9109
  root.innerHTML = previousMarkup;
8546
9110
  if (typeof previousRenderGameToText === "function") {
8547
9111
  window.render_game_to_text = previousRenderGameToText;
@@ -8562,6 +9126,12 @@ async function mountGpuShowcase(options = {}) {
8562
9126
  };
8563
9127
  }
8564
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
+ );
8565
9135
  state.physics.snapshot = createPhysicsWorldSnapshot({
8566
9136
  frameId: `showcase-${state.frame}`,
8567
9137
  tick: state.frame,
@@ -8577,7 +9147,8 @@ function updatePhysicsSnapshot(state, shipModel) {
8577
9147
  collisionCount: state.collisionCount,
8578
9148
  contactCount: state.contactCount,
8579
9149
  snapshotStageId: state.physics.plan.snapshotStageId,
8580
- rigidBodyShape: shipModel.physics.shape ?? "box"
9150
+ rigidBodyShape: shipModel.physics.shape ?? "box",
9151
+ rigidBodyShapes
8581
9152
  }
8582
9153
  });
8583
9154
  }
@@ -8590,4 +9161,4 @@ export {
8590
9161
  mountGpuShowcase,
8591
9162
  showcaseFocusModes
8592
9163
  };
8593
- //# sourceMappingURL=showcase-runtime-2ZNPKD7D.js.map
9164
+ //# sourceMappingURL=showcase-runtime-PN7N3FZY.js.map