@plasius/gpu-shared 0.1.6 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2220,6 +2220,9 @@ var init_dist2 = __esm({
2220
2220
  });
2221
2221
 
2222
2222
  // node_modules/@plasius/gpu-lighting/dist/index.js
2223
+ function createModuleBaseUrl(metaUrl) {
2224
+ return Reflect.construct(URL, [String(metaUrl)]);
2225
+ }
2223
2226
  function buildTechnique(name, spec) {
2224
2227
  const preludeUrl = new URL(`./techniques/${name}/${spec.prelude}`, baseUrl);
2225
2228
  const jobs = Object.entries(spec.jobs).map(([key, file]) => {
@@ -2528,7 +2531,7 @@ var init_dist3 = __esm({
2528
2531
  });
2529
2532
  baseUrl = (() => {
2530
2533
  if (typeof import_meta2.url !== "undefined") {
2531
- return new URL("./index.js", import_meta2.url);
2534
+ return createModuleBaseUrl(import_meta2.url);
2532
2535
  }
2533
2536
  if (typeof __filename !== "undefined" && typeof __require !== "undefined") {
2534
2537
  const { pathToFileURL } = __require("url");
@@ -6339,6 +6342,10 @@ var init_browser = __esm({
6339
6342
  // src/showcase-runtime.js
6340
6343
  var showcase_runtime_exports = {};
6341
6344
  __export(showcase_runtime_exports, {
6345
+ __testOnlyAdvanceShowcaseClothSimulationState: () => advanceShowcaseClothSimulationState,
6346
+ __testOnlyBuildWaterMotionEffects: () => buildWaterMotionEffects,
6347
+ __testOnlyCollectSceneLightSources: () => collectSceneLightSources,
6348
+ __testOnlyCreateShowcaseClothSimulationState: () => createShowcaseClothSimulationState,
6342
6349
  mountGpuShowcase: () => mountGpuShowcase,
6343
6350
  showcaseFocusModes: () => showcaseFocusModes
6344
6351
  });
@@ -6914,6 +6921,250 @@ function normalizeColorOverride(color, fallback) {
6914
6921
  function readVisualNumber(value, fallback) {
6915
6922
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
6916
6923
  }
6924
+ function resolveClothPresentation(state, meshDetail) {
6925
+ const clothPlan = createClothRepresentationPlan({
6926
+ garmentId: "shore-flag",
6927
+ kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
6928
+ profile: state.focus === "cloth" ? "cinematic" : clothProfileNames[0],
6929
+ supportsRayTracing: true,
6930
+ nearFieldMaxMeters: 18,
6931
+ midFieldMaxMeters: 55,
6932
+ farFieldMaxMeters: 180
6933
+ });
6934
+ const preset = CAMERA_PRESETS[state.focus] ?? CAMERA_PRESETS.integrated;
6935
+ const fallbackEye = state.camera.eye ? state.camera.eye : addVec3(
6936
+ state.camera.target,
6937
+ vec3(
6938
+ Math.sin(state.camera.yaw ?? preset.yaw) * Math.cos(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance),
6939
+ Math.sin(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance),
6940
+ Math.cos(state.camera.yaw ?? preset.yaw) * Math.cos(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance)
6941
+ )
6942
+ );
6943
+ const cameraDistance = lengthVec3(subVec3(state.camera.target, fallbackEye));
6944
+ const band = selectClothRepresentationBand(cameraDistance, clothPlan.thresholds);
6945
+ const representation = clothPlan.representations.find((entry) => entry.band === band) ?? clothPlan.representations[0];
6946
+ return {
6947
+ clothPlan,
6948
+ band,
6949
+ continuity: representation.continuity,
6950
+ representation
6951
+ };
6952
+ }
6953
+ function getFlagRestPosition(rows, cols, row, column) {
6954
+ const u = cols <= 1 ? 0 : column / (cols - 1);
6955
+ const v = rows <= 1 ? 0 : row / (rows - 1);
6956
+ return vec3(
6957
+ FLAG_LAYOUT.origin.x + u * FLAG_LAYOUT.mastOffsetX,
6958
+ FLAG_LAYOUT.origin.y - FLAG_LAYOUT.height * v - u * u * 0.08,
6959
+ FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * u
6960
+ );
6961
+ }
6962
+ function buildClothConstraints(rows, cols, restPositions) {
6963
+ const constraints = [];
6964
+ const indexFor = (row, column) => row * cols + column;
6965
+ const pushConstraint = (a, b, stiffness) => {
6966
+ constraints.push(
6967
+ Object.freeze({
6968
+ a,
6969
+ b,
6970
+ restLength: lengthVec3(subVec3(restPositions[a], restPositions[b])),
6971
+ stiffness
6972
+ })
6973
+ );
6974
+ };
6975
+ for (let row = 0; row < rows; row += 1) {
6976
+ for (let column = 0; column < cols; column += 1) {
6977
+ const index = indexFor(row, column);
6978
+ if (column + 1 < cols) {
6979
+ pushConstraint(index, indexFor(row, column + 1), 0.92);
6980
+ }
6981
+ if (row + 1 < rows) {
6982
+ pushConstraint(index, indexFor(row + 1, column), 0.9);
6983
+ }
6984
+ if (column + 1 < cols && row + 1 < rows) {
6985
+ pushConstraint(index, indexFor(row + 1, column + 1), 0.66);
6986
+ }
6987
+ if (column - 1 >= 0 && row + 1 < rows) {
6988
+ pushConstraint(index, indexFor(row + 1, column - 1), 0.66);
6989
+ }
6990
+ if (column + 2 < cols) {
6991
+ pushConstraint(index, indexFor(row, column + 2), 0.22);
6992
+ }
6993
+ if (row + 2 < rows) {
6994
+ pushConstraint(index, indexFor(row + 2, column), 0.18);
6995
+ }
6996
+ }
6997
+ }
6998
+ return Object.freeze(constraints);
6999
+ }
7000
+ function createShowcaseClothSimulationState(options = {}) {
7001
+ const rows = Math.max(4, options.rows ?? 11);
7002
+ const cols = Math.max(4, options.cols ?? 16);
7003
+ const continuity = options.continuity ?? {
7004
+ broadMotionFloor: 0.72,
7005
+ wrinkleFloor: 0.56
7006
+ };
7007
+ const representation = options.representation ?? {
7008
+ mesh: {
7009
+ solverIterations: 6,
7010
+ wrinkleLayers: 2
7011
+ }
7012
+ };
7013
+ const restPositions = [];
7014
+ const positions = [];
7015
+ const previousPositions = [];
7016
+ const uvs = [];
7017
+ const phaseOffsets = [];
7018
+ const pinned = [];
7019
+ for (let row = 0; row < rows; row += 1) {
7020
+ for (let column = 0; column < cols; column += 1) {
7021
+ const index = row * cols + column;
7022
+ const u = cols <= 1 ? 0 : column / (cols - 1);
7023
+ const v = rows <= 1 ? 0 : row / (rows - 1);
7024
+ const rest = getFlagRestPosition(rows, cols, row, column);
7025
+ const preload = vec3(
7026
+ u * 0.04,
7027
+ Math.sin(v * Math.PI) * 0.02 * continuity.wrinkleFloor,
7028
+ -u * 0.12
7029
+ );
7030
+ const pinnedPoint = column === 0;
7031
+ restPositions.push(rest);
7032
+ positions.push(pinnedPoint ? vec3(rest.x, rest.y, rest.z) : addVec3(rest, preload));
7033
+ previousPositions.push(
7034
+ pinnedPoint ? vec3(rest.x, rest.y, rest.z) : addVec3(rest, scaleVec3(preload, 0.35))
7035
+ );
7036
+ uvs.push(Object.freeze({ u, v }));
7037
+ phaseOffsets.push(pseudoRandom(index + 17) * Math.PI * 2);
7038
+ pinned.push(pinnedPoint);
7039
+ }
7040
+ }
7041
+ return {
7042
+ rows,
7043
+ cols,
7044
+ continuity,
7045
+ representation,
7046
+ restPositions,
7047
+ positions,
7048
+ previousPositions,
7049
+ constraints: buildClothConstraints(rows, cols, restPositions),
7050
+ indices: Object.freeze(
7051
+ Array.from({ length: (rows - 1) * (cols - 1) * 6 }, (_, listIndex) => listIndex).map((_, listIndex, source) => {
7052
+ if (listIndex >= source.length) {
7053
+ return 0;
7054
+ }
7055
+ const quadIndex = Math.floor(listIndex / 6);
7056
+ const quadColumn = quadIndex % (cols - 1);
7057
+ const quadRow = Math.floor(quadIndex / (cols - 1));
7058
+ const base = quadRow * cols + quadColumn;
7059
+ return [base, base + 1, base + cols + 1, base, base + cols + 1, base + cols][listIndex % 6];
7060
+ })
7061
+ ),
7062
+ uvs,
7063
+ phaseOffsets,
7064
+ pinned
7065
+ };
7066
+ }
7067
+ function resetPinnedClothPoints(clothState) {
7068
+ for (let index = 0; index < clothState.positions.length; index += 1) {
7069
+ if (!clothState.pinned[index]) {
7070
+ continue;
7071
+ }
7072
+ const anchor = clothState.restPositions[index];
7073
+ clothState.positions[index] = vec3(anchor.x, anchor.y, anchor.z);
7074
+ clothState.previousPositions[index] = vec3(anchor.x, anchor.y, anchor.z);
7075
+ }
7076
+ }
7077
+ function satisfyClothConstraint(clothState, constraint) {
7078
+ const a = clothState.positions[constraint.a];
7079
+ const b = clothState.positions[constraint.b];
7080
+ const delta = subVec3(b, a);
7081
+ const distance = lengthVec3(delta);
7082
+ if (distance <= 1e-4) {
7083
+ return;
7084
+ }
7085
+ const correctionScale = (distance - constraint.restLength) / distance * 0.5 * constraint.stiffness;
7086
+ const correction = scaleVec3(delta, correctionScale);
7087
+ if (!clothState.pinned[constraint.a]) {
7088
+ clothState.positions[constraint.a] = addVec3(a, correction);
7089
+ }
7090
+ if (!clothState.pinned[constraint.b]) {
7091
+ clothState.positions[constraint.b] = subVec3(b, correction);
7092
+ }
7093
+ }
7094
+ function advanceShowcaseClothSimulationState(clothState, options = {}) {
7095
+ const dt = clamp(options.dt ?? 1 / 60, 1 / 240, 1 / 18);
7096
+ const time = readVisualNumber(options.time, 0);
7097
+ const flagMotion = readVisualNumber(options.flagMotion, 0.92);
7098
+ const waveInfluence = readVisualNumber(options.waveInfluence, 0);
7099
+ const wrinkleLayers = Math.max(1, clothState.representation.mesh?.wrinkleLayers ?? 2);
7100
+ const solverIterations = clamp(
7101
+ Math.round(clothState.representation.mesh?.solverIterations ?? 6),
7102
+ 2,
7103
+ 10
7104
+ );
7105
+ for (let index = 0; index < clothState.positions.length; index += 1) {
7106
+ if (clothState.pinned[index]) {
7107
+ continue;
7108
+ }
7109
+ const current = clothState.positions[index];
7110
+ const previous = clothState.previousPositions[index];
7111
+ const { u, v } = clothState.uvs[index];
7112
+ const phase = clothState.phaseOffsets[index];
7113
+ const broadMotion = clothState.continuity.broadMotionFloor;
7114
+ const wrinkleMotion = clothState.continuity.wrinkleFloor;
7115
+ const gustPhase = time * 2.1 + phase + u * 4.4 + v * 2.3;
7116
+ const wrinklePhase = time * 5.3 + phase * 0.72 + u * 9.6 + v * 7.1;
7117
+ const windDirection = normalizeVec3(
7118
+ vec3(
7119
+ 0.18 + Math.sin(gustPhase) * (0.12 + broadMotion * 0.09),
7120
+ Math.cos(time * 1.4 + phase + v * 4.8) * 0.06 * wrinkleMotion,
7121
+ 1 + Math.sin(gustPhase * 0.74) * 0.18
7122
+ )
7123
+ );
7124
+ const windStrength = (1.6 + broadMotion * 1.25 + wrinkleLayers * 0.12) * flagMotion * (0.44 + u * 1.14);
7125
+ const wrinkleForce = vec3(
7126
+ Math.sin(wrinklePhase) * 0.22 * wrinkleMotion * flagMotion,
7127
+ Math.cos(wrinklePhase * 0.7) * 0.08 * wrinkleMotion,
7128
+ Math.cos(wrinklePhase) * 0.14 * broadMotion * flagMotion
7129
+ );
7130
+ const acceleration = addVec3(
7131
+ vec3(0, -0.48 - u * 0.08, 0),
7132
+ addVec3(
7133
+ scaleVec3(windDirection, windStrength),
7134
+ addVec3(
7135
+ wrinkleForce,
7136
+ vec3(waveInfluence * (0.04 + u * 0.08), 0, waveInfluence * 0.16)
7137
+ )
7138
+ )
7139
+ );
7140
+ const inertia = scaleVec3(subVec3(current, previous), 0.987);
7141
+ const next = addVec3(addVec3(current, inertia), scaleVec3(acceleration, dt * dt));
7142
+ clothState.previousPositions[index] = vec3(current.x, current.y, current.z);
7143
+ clothState.positions[index] = next;
7144
+ }
7145
+ resetPinnedClothPoints(clothState);
7146
+ for (let iteration = 0; iteration < solverIterations; iteration += 1) {
7147
+ for (const constraint of clothState.constraints) {
7148
+ satisfyClothConstraint(clothState, constraint);
7149
+ }
7150
+ resetPinnedClothPoints(clothState);
7151
+ }
7152
+ return clothState;
7153
+ }
7154
+ function ensureShowcaseClothState(state, meshDetail, clothPresentation) {
7155
+ if (!state.clothState || state.clothState.rows !== meshDetail.rows || state.clothState.cols !== meshDetail.cols) {
7156
+ state.clothState = createShowcaseClothSimulationState({
7157
+ rows: meshDetail.rows,
7158
+ cols: meshDetail.cols,
7159
+ continuity: clothPresentation.continuity,
7160
+ representation: clothPresentation.representation
7161
+ });
7162
+ } else {
7163
+ state.clothState.continuity = clothPresentation.continuity;
7164
+ state.clothState.representation = clothPresentation.representation;
7165
+ }
7166
+ return state.clothState;
7167
+ }
6917
7168
  function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
6918
7169
  const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6919
7170
  const defaults = {
@@ -6997,58 +7248,17 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
6997
7248
  };
6998
7249
  }
6999
7250
  function buildClothSurface(model, state, meshDetail, visuals) {
7000
- const clothPlan = createClothRepresentationPlan({
7001
- garmentId: "shore-flag",
7002
- kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
7003
- profile: state.focus === "cloth" ? "cinematic" : clothProfileNames[0],
7004
- supportsRayTracing: true,
7005
- nearFieldMaxMeters: 18,
7006
- midFieldMaxMeters: 55,
7007
- farFieldMaxMeters: 180
7008
- });
7009
- const cameraDistance = lengthVec3(subVec3(state.camera.target, state.camera.eye ?? vec3(...CAMERA_PRESETS[state.focus].target)));
7010
- const band = selectClothRepresentationBand(cameraDistance, clothPlan.thresholds);
7011
- const representation = clothPlan.representations.find((entry) => entry.band === band) ?? clothPlan.representations[0];
7012
- const continuity = createClothContinuityEnvelope({ garmentId: "shore-flag" });
7013
- const cols = meshDetail.cols;
7014
- const rows = meshDetail.rows;
7015
- const origin = vec3(-3.5, 5.9, 2.4);
7016
- const width = 4.8;
7017
- const height = 2.7;
7018
- const positions = [];
7019
- const indices = [];
7020
- const time = state.time;
7021
- for (let row = 0; row < rows; row += 1) {
7022
- for (let column = 0; column < cols; column += 1) {
7023
- const u = column / (cols - 1);
7024
- const v = row / (rows - 1);
7025
- const gust = Math.sin(time * 1.9 + v * 3.2 + u * 2.1) * continuity.broadMotionFloor * visuals.flagMotion;
7026
- const wrinkle = Math.sin(time * 4.4 + u * 9.2 + v * 5.6) * continuity.wrinkleFloor * 0.22 * Math.max(0.55, visuals.flagMotion);
7027
- const x = origin.x + u * 1.8 + gust * 0.55 * (u * 0.9);
7028
- const y = origin.y - height * v + wrinkle * 0.2;
7029
- const z = origin.z + width * u + gust * 0.72 * (u * 0.85);
7030
- const flap = Math.cos(time * 2.7 + u * 7.4 + v * 3.8) * continuity.broadMotionFloor * 0.28 * visuals.flagMotion;
7031
- positions.push(vec3(x + flap, y, z));
7032
- }
7033
- }
7034
- for (let row = 0; row < rows - 1; row += 1) {
7035
- for (let column = 0; column < cols - 1; column += 1) {
7036
- const a = row * cols + column;
7037
- const b = a + 1;
7038
- const c = a + cols + 1;
7039
- const d = a + cols;
7040
- indices.push(a, b, c, a, c, d);
7041
- }
7042
- }
7251
+ const clothPresentation = resolveClothPresentation(state, meshDetail);
7252
+ const clothState = ensureShowcaseClothState(state, meshDetail, clothPresentation);
7043
7253
  return {
7044
- clothPlan,
7045
- band,
7046
- representation,
7047
- continuity,
7254
+ clothPlan: clothPresentation.clothPlan,
7255
+ band: clothPresentation.band,
7256
+ representation: clothPresentation.representation,
7257
+ continuity: clothPresentation.continuity,
7048
7258
  color: visuals.flagColor,
7049
- positions,
7050
- indices,
7051
- grid: { rows, cols }
7259
+ positions: clothState.positions.map((point) => vec3(point.x, point.y, point.z)),
7260
+ indices: clothState.indices,
7261
+ grid: { rows: clothState.rows, cols: clothState.cols }
7052
7262
  };
7053
7263
  }
7054
7264
  function resolveWaveDirection(state) {
@@ -7112,6 +7322,59 @@ function sampleWave(state, x, z, time) {
7112
7322
  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;
7113
7323
  return base * amplitude + sampleShipWake(state, x, z, time) + sampleWaveImpulses(state, x, z, time);
7114
7324
  }
7325
+ function buildWaterMotionEffects(state) {
7326
+ const wakeTrails = [];
7327
+ const rippleRings = state.waveImpulses.map((impulse) => {
7328
+ const radius = impulse.radius + (1 - impulse.life) * 4.8;
7329
+ return Object.freeze({
7330
+ center: vec3(
7331
+ impulse.x,
7332
+ sampleWave(state, impulse.x, impulse.z, state.time) * 0.24 + 0.06,
7333
+ impulse.z
7334
+ ),
7335
+ radius,
7336
+ opacity: clamp(impulse.life * 0.28, 0.08, 0.3)
7337
+ });
7338
+ });
7339
+ for (const ship of state.ships) {
7340
+ const speed = Math.hypot(ship.velocity.x, ship.velocity.z);
7341
+ if (speed <= 0.18) {
7342
+ continue;
7343
+ }
7344
+ const direction = normalizeVec3(vec3(ship.velocity.x, 0, ship.velocity.z));
7345
+ const behind = scaleVec3(direction, -1);
7346
+ const lateral = vec3(-direction.z, 0, direction.x);
7347
+ const points = [];
7348
+ for (let sampleIndex = 0; sampleIndex < 6; sampleIndex += 1) {
7349
+ const along = 1 + sampleIndex * 1.45;
7350
+ const lateralOffset = Math.sin(state.time * 1.2 + sampleIndex * 0.8 + readVisualNumber(ship.wanderPhase, 0)) * 0.12;
7351
+ const worldPoint = addVec3(
7352
+ ship.position,
7353
+ addVec3(scaleVec3(behind, along), scaleVec3(lateral, lateralOffset))
7354
+ );
7355
+ points.push(
7356
+ Object.freeze({
7357
+ center: vec3(
7358
+ worldPoint.x,
7359
+ sampleWave(state, worldPoint.x, worldPoint.z, state.time) * 0.24 + 0.04,
7360
+ worldPoint.z
7361
+ ),
7362
+ width: 0.34 + sampleIndex * 0.13
7363
+ })
7364
+ );
7365
+ }
7366
+ wakeTrails.push(
7367
+ Object.freeze({
7368
+ opacity: clamp(0.18 + speed * 0.09, 0.22, 0.46),
7369
+ points: Object.freeze(points)
7370
+ })
7371
+ );
7372
+ }
7373
+ return Object.freeze({
7374
+ wakeTrails: Object.freeze(wakeTrails),
7375
+ rippleRings: Object.freeze(rippleRings)
7376
+ });
7377
+ }
7115
7378
  function buildWaterBands(state, fluidDetail, visuals) {
7116
7379
  const fluidPlan = createFluidRepresentationPlan({
7117
7380
  fluidBodyId: "harbor",
@@ -7264,6 +7527,7 @@ function createSceneState(options) {
7264
7527
  contactCount: 0,
7265
7528
  collisionCount: 0,
7266
7529
  collisionFlash: 0,
7530
+ clothState: null,
7267
7531
  physics: {
7268
7532
  profile: physicsProfile,
7269
7533
  plan: physicsPlan,
@@ -7899,16 +8163,73 @@ function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength
7899
8163
  blur: 12 + shadowStrength * 20
7900
8164
  });
7901
8165
  }
7902
- function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glowScale, reflectionStrength = 0, state = null) {
7903
- const projected = projectPoint(point, camera, viewport);
8166
+ function collectSceneLightSources(state, visuals) {
8167
+ const directLights = [];
8168
+ const reflectionLights = [];
8169
+ const pushLight = (point, glowScale, reflectionStrength, coreColor, glowColor) => {
8170
+ directLights.push(
8171
+ Object.freeze({
8172
+ pass: "direct-glow",
8173
+ point,
8174
+ coreColor,
8175
+ glowColor,
8176
+ glowScale
8177
+ })
8178
+ );
8179
+ if (reflectionStrength > 0) {
8180
+ reflectionLights.push(
8181
+ Object.freeze({
8182
+ pass: "water-reflection",
8183
+ point,
8184
+ coreColor,
8185
+ glowColor,
8186
+ glowScale,
8187
+ reflectionStrength
8188
+ })
8189
+ );
8190
+ }
8191
+ };
8192
+ for (const torch of HARBOR_TORCHES) {
8193
+ pushLight(
8194
+ vec3(torch.x, torch.y, torch.z),
8195
+ torch.glow,
8196
+ visuals.lanternReflectionStrength * 0.55,
8197
+ visuals.torchCore,
8198
+ visuals.torchGlow
8199
+ );
8200
+ }
8201
+ for (const ship of state.ships) {
8202
+ const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
8203
+ const strength = readVisualNumber(ship.lanternStrength, 1);
8204
+ for (const lantern of lanterns) {
8205
+ const point = transformPoint(
8206
+ vec3(lantern.x, lantern.y, lantern.z),
8207
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
8208
+ );
8209
+ pushLight(
8210
+ point,
8211
+ lantern.glow * strength,
8212
+ visuals.lanternReflectionStrength,
8213
+ visuals.lanternCore,
8214
+ visuals.lanternGlow
8215
+ );
8216
+ }
8217
+ }
8218
+ return Object.freeze({
8219
+ directLights: Object.freeze(directLights),
8220
+ reflectionLights: Object.freeze(reflectionLights)
8221
+ });
8222
+ }
8223
+ function renderDirectLightGlow(ctx, source, camera, viewport) {
8224
+ const projected = projectPoint(source.point, camera, viewport);
7904
8225
  if (!projected) {
7905
8226
  return;
7906
8227
  }
7907
- const radius = clamp(1 / projected.depth * 420 * glowScale, 4, 34);
8228
+ const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
7908
8229
  const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
7909
- halo.addColorStop(0, colorToRgba(coreColor, 0.98));
7910
- halo.addColorStop(0.5, colorToRgba(glowColor, 0.42));
7911
- halo.addColorStop(1, colorToRgba(glowColor, 0));
8230
+ halo.addColorStop(0, colorToRgba(source.coreColor, 0.98));
8231
+ halo.addColorStop(0.5, colorToRgba(source.glowColor, 0.42));
8232
+ halo.addColorStop(1, colorToRgba(source.glowColor, 0));
7912
8233
  ctx.save();
7913
8234
  ctx.globalCompositeOperation = "screen";
7914
8235
  ctx.fillStyle = halo;
@@ -7916,79 +8237,119 @@ function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glo
7916
8237
  ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
7917
8238
  ctx.fill();
7918
8239
  ctx.restore();
7919
- ctx.fillStyle = colorToRgba(coreColor, 0.98);
8240
+ ctx.fillStyle = colorToRgba(source.coreColor, 0.98);
7920
8241
  ctx.beginPath();
7921
8242
  ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
7922
8243
  ctx.fill();
7923
- if (state && reflectionStrength > 0) {
7924
- const waterline = sampleWave(state, point.x, point.z, state.time) * 0.22;
7925
- const reflectedPoint = vec3(point.x, waterline - (point.y - waterline) * 0.58, point.z + 0.08);
7926
- const reflected = projectPoint(reflectedPoint, camera, viewport);
7927
- if (reflected) {
7928
- const reflectionRadius = radius * 0.72;
7929
- const glow = ctx.createRadialGradient(
7930
- reflected.x,
7931
- reflected.y,
7932
- reflectionRadius * 0.1,
7933
- reflected.x,
7934
- reflected.y,
7935
- reflectionRadius
7936
- );
7937
- glow.addColorStop(0, colorToRgba(coreColor, reflectionStrength * 0.34));
7938
- glow.addColorStop(1, colorToRgba(glowColor, 0));
7939
- ctx.save();
7940
- ctx.globalCompositeOperation = "screen";
7941
- ctx.fillStyle = glow;
8244
+ }
8245
+ function renderWaterLightReflection(ctx, source, state, camera, viewport) {
8246
+ const projected = projectPoint(source.point, camera, viewport);
8247
+ if (!projected) {
8248
+ return;
8249
+ }
8250
+ const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
8251
+ const waterline = sampleWave(state, source.point.x, source.point.z, state.time) * 0.22;
8252
+ const reflectedPoint = vec3(
8253
+ source.point.x,
8254
+ waterline - (source.point.y - waterline) * 0.58,
8255
+ source.point.z + 0.08
8256
+ );
8257
+ const reflected = projectPoint(reflectedPoint, camera, viewport);
8258
+ if (!reflected) {
8259
+ return;
8260
+ }
8261
+ const reflectionRadius = radius * 0.72;
8262
+ const glow = ctx.createRadialGradient(
8263
+ reflected.x,
8264
+ reflected.y,
8265
+ reflectionRadius * 0.1,
8266
+ reflected.x,
8267
+ reflected.y,
8268
+ reflectionRadius
8269
+ );
8270
+ glow.addColorStop(0, colorToRgba(source.coreColor, source.reflectionStrength * 0.34));
8271
+ glow.addColorStop(1, colorToRgba(source.glowColor, 0));
8272
+ ctx.save();
8273
+ ctx.globalCompositeOperation = "screen";
8274
+ ctx.fillStyle = glow;
8275
+ ctx.beginPath();
8276
+ ctx.ellipse(
8277
+ reflected.x,
8278
+ reflected.y,
8279
+ reflectionRadius * 0.34,
8280
+ reflectionRadius,
8281
+ 0,
8282
+ 0,
8283
+ Math.PI * 2
8284
+ );
8285
+ ctx.fill();
8286
+ ctx.restore();
8287
+ }
8288
+ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
8289
+ ctx.save();
8290
+ ctx.globalCompositeOperation = "screen";
8291
+ for (const wake of effects.wakeTrails) {
8292
+ const projected = wake.points.map((point) => ({
8293
+ projected: projectPoint(point.center, camera, viewport),
8294
+ width: point.width
8295
+ })).filter((entry) => entry.projected);
8296
+ if (projected.length < 2) {
8297
+ continue;
8298
+ }
8299
+ const averageDepth = projected.reduce((total, entry) => total + entry.projected.depth, 0) / projected.length;
8300
+ const averageWidth = projected.reduce((total, entry) => total + entry.width, 0) / projected.length;
8301
+ const baseWidth = clamp(averageWidth / Math.max(0.25, averageDepth) * 180, 1.6, 5.4);
8302
+ ctx.strokeStyle = `rgba(146, 194, 236, ${wake.opacity * 0.52})`;
8303
+ ctx.lineWidth = baseWidth * 1.9;
8304
+ ctx.lineCap = "round";
8305
+ ctx.lineJoin = "round";
8306
+ ctx.beginPath();
8307
+ ctx.moveTo(projected[0].projected.x, projected[0].projected.y);
8308
+ for (let index = 1; index < projected.length; index += 1) {
8309
+ ctx.lineTo(projected[index].projected.x, projected[index].projected.y);
8310
+ }
8311
+ ctx.stroke();
8312
+ ctx.strokeStyle = `rgba(234, 247, 255, ${wake.opacity})`;
8313
+ ctx.lineWidth = baseWidth;
8314
+ ctx.lineCap = "round";
8315
+ ctx.lineJoin = "round";
8316
+ ctx.beginPath();
8317
+ ctx.moveTo(projected[0].projected.x, projected[0].projected.y);
8318
+ for (let index = 1; index < projected.length; index += 1) {
8319
+ ctx.lineTo(projected[index].projected.x, projected[index].projected.y);
8320
+ }
8321
+ ctx.stroke();
8322
+ for (const entry of projected.slice(1, 5)) {
8323
+ ctx.fillStyle = `rgba(239, 248, 255, ${wake.opacity * 0.76})`;
7942
8324
  ctx.beginPath();
7943
8325
  ctx.ellipse(
7944
- reflected.x,
7945
- reflected.y,
7946
- reflectionRadius * 0.34,
7947
- reflectionRadius,
8326
+ entry.projected.x,
8327
+ entry.projected.y,
8328
+ baseWidth * 0.72,
8329
+ baseWidth * 0.44,
7948
8330
  0,
7949
8331
  0,
7950
8332
  Math.PI * 2
7951
8333
  );
7952
8334
  ctx.fill();
7953
- ctx.restore();
7954
8335
  }
7955
8336
  }
7956
- }
7957
- function renderShipLanterns(ctx, ship, state, camera, viewport, visuals) {
7958
- const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
7959
- const strength = readVisualNumber(ship.lanternStrength, 1);
7960
- for (const lantern of lanterns) {
7961
- const position = transformPoint(
7962
- vec3(lantern.x, lantern.y, lantern.z),
7963
- { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
7964
- );
7965
- renderGlowLight(
7966
- ctx,
7967
- position,
7968
- camera,
7969
- viewport,
7970
- visuals.lanternCore,
7971
- visuals.lanternGlow,
7972
- lantern.glow * strength,
7973
- visuals.lanternReflectionStrength,
7974
- state
7975
- );
7976
- }
7977
- }
7978
- function renderHarborTorches(ctx, state, camera, viewport, visuals) {
7979
- for (const torch of HARBOR_TORCHES) {
7980
- renderGlowLight(
7981
- ctx,
7982
- vec3(torch.x, torch.y, torch.z),
7983
- camera,
7984
- viewport,
7985
- visuals.torchCore,
7986
- visuals.torchGlow,
7987
- torch.glow,
7988
- visuals.lanternReflectionStrength * 0.55,
7989
- state
7990
- );
8337
+ for (const ring of effects.rippleRings) {
8338
+ const center = projectPoint(ring.center, camera, viewport);
8339
+ const xAxis = projectPoint(addVec3(ring.center, vec3(ring.radius, 0, 0)), camera, viewport);
8340
+ const zAxis = projectPoint(addVec3(ring.center, vec3(0, 0, ring.radius)), camera, viewport);
8341
+ if (!center || !xAxis || !zAxis) {
8342
+ continue;
8343
+ }
8344
+ const radiusX = Math.hypot(xAxis.x - center.x, xAxis.y - center.y);
8345
+ const radiusY = Math.hypot(zAxis.x - center.x, zAxis.y - center.y);
8346
+ ctx.strokeStyle = `rgba(216, 235, 255, ${ring.opacity})`;
8347
+ ctx.lineWidth = clamp((radiusX + radiusY) * 0.02, 1, 3.1);
8348
+ ctx.beginPath();
8349
+ ctx.ellipse(center.x, center.y, radiusX, radiusY, 0, 0, Math.PI * 2);
8350
+ ctx.stroke();
7991
8351
  }
8352
+ ctx.restore();
7992
8353
  }
7993
8354
  function renderScene(ctx, canvas, state, shipModel, dom) {
7994
8355
  const viewport = { width: canvas.width, height: canvas.height };
@@ -8018,8 +8379,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8018
8379
  shadowStrength,
8019
8380
  visuals
8020
8381
  );
8021
- const triangles = [];
8022
- pushHarborGeometry(camera, viewport, triangles, visuals);
8382
+ const waterTriangles = [];
8383
+ const sceneTriangles = [];
8023
8384
  const water = buildWaterBands(
8024
8385
  state,
8025
8386
  state.fluidDetail.getSnapshot().currentLevel.config,
@@ -8036,7 +8397,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8036
8397
  if (projected.some((value) => value === null)) {
8037
8398
  continue;
8038
8399
  }
8039
- triangles.push({
8400
+ waterTriangles.push({
8040
8401
  points: projected,
8041
8402
  depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
8042
8403
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
@@ -8046,6 +8407,9 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8046
8407
  });
8047
8408
  }
8048
8409
  }
8410
+ const waterMotionEffects = buildWaterMotionEffects(state);
8411
+ const lightSources = collectSceneLightSources(state, visuals);
8412
+ pushHarborGeometry(camera, viewport, sceneTriangles, visuals);
8049
8413
  const cloth = buildClothSurface(
8050
8414
  state,
8051
8415
  state,
@@ -8061,7 +8425,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8061
8425
  if (projected.some((value) => value === null)) {
8062
8426
  continue;
8063
8427
  }
8064
- triangles.push({
8428
+ sceneTriangles.push({
8065
8429
  points: projected,
8066
8430
  depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
8067
8431
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
@@ -8077,22 +8441,28 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8077
8441
  ship.tint,
8078
8442
  camera,
8079
8443
  viewport,
8080
- triangles,
8444
+ sceneTriangles,
8081
8445
  nearLighting.rtParticipation.directShadows === "premium" ? 0.08 : 0.02
8082
8446
  );
8083
8447
  }
8448
+ drawTriangles(ctx, waterTriangles, lightDir, reflectionStrength, camera, shadowStrength);
8084
8449
  for (const ship of state.ships) {
8085
8450
  renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength);
8086
8451
  }
8087
8452
  renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
8088
- drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength);
8453
+ for (const source of lightSources.reflectionLights) {
8454
+ renderWaterLightReflection(ctx, source, state, camera, viewport);
8455
+ }
8456
+ renderWaterMotionEffects(ctx, waterMotionEffects, camera, viewport);
8089
8457
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
8090
- renderHarborTorches(ctx, state, camera, viewport, visuals);
8458
+ drawTriangles(ctx, sceneTriangles, lightDir, reflectionStrength, camera, shadowStrength);
8091
8459
  renderFlagPole(ctx, camera, viewport);
8092
8460
  renderClothAccent(ctx, cloth, camera, viewport);
8461
+ for (const source of lightSources.directLights) {
8462
+ renderDirectLightGlow(ctx, source, camera, viewport);
8463
+ }
8093
8464
  for (const ship of state.ships) {
8094
8465
  renderShipRigging(ctx, ship, camera, viewport);
8095
- renderShipLanterns(ctx, ship, state, camera, viewport, visuals);
8096
8466
  }
8097
8467
  renderSprays(ctx, state.sprays, camera, viewport);
8098
8468
  const debugSnapshot = state.debugSession.getSnapshot();
@@ -8154,6 +8524,21 @@ function updateSceneState(state, dt, shipModel) {
8154
8524
  updateShips(state, dt, shipModel);
8155
8525
  updateWaveImpulses(state, dt);
8156
8526
  updateSpray(state, dt);
8527
+ const clothPresentation = resolveClothPresentation(
8528
+ state,
8529
+ state.clothDetail.getSnapshot().currentLevel.config
8530
+ );
8531
+ const clothState = ensureShowcaseClothState(
8532
+ state,
8533
+ state.clothDetail.getSnapshot().currentLevel.config,
8534
+ clothPresentation
8535
+ );
8536
+ advanceShowcaseClothSimulationState(clothState, {
8537
+ dt,
8538
+ time: state.time,
8539
+ flagMotion: readVisualNumber(state.demoVisuals?.flagMotion, 0.92),
8540
+ waveInfluence: sampleWave(state, FLAG_LAYOUT.origin.x + FLAG_LAYOUT.width * 0.55, FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * 0.48, state.time)
8541
+ });
8157
8542
  updatePhysicsSnapshot(state, shipModel);
8158
8543
  }
8159
8544
  function syncTextState(state, shipModel) {
@@ -8320,7 +8705,7 @@ function updatePhysicsSnapshot(state, shipModel) {
8320
8705
  }
8321
8706
  });
8322
8707
  }
8323
- var STYLE_ID, ROOT_CLASS, DEFAULT_TITLE, DEFAULT_SUBTITLE, SHIP_SCALE, HARBOR_BOUNDS, CAMERA_PRESETS, showcaseFocusModes, SCENE_NOTES, UNIT_BOX_MESH, SHIP_LANTERNS, HARBOR_TORCHES;
8708
+ var STYLE_ID, ROOT_CLASS, DEFAULT_TITLE, DEFAULT_SUBTITLE, SHIP_SCALE, HARBOR_BOUNDS, CAMERA_PRESETS, showcaseFocusModes, SCENE_NOTES, UNIT_BOX_MESH, SHIP_LANTERNS, HARBOR_TORCHES, FLAG_LAYOUT;
8324
8709
  var init_showcase_runtime = __esm({
8325
8710
  "src/showcase-runtime.js"() {
8326
8711
  init_dist();
@@ -8435,6 +8820,12 @@ var init_showcase_runtime = __esm({
8435
8820
  Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
8436
8821
  Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 })
8437
8822
  ]);
8823
+ FLAG_LAYOUT = Object.freeze({
8824
+ origin: Object.freeze({ x: -3.5, y: 5.9, z: 2.4 }),
8825
+ width: 4.8,
8826
+ height: 2.7,
8827
+ mastOffsetX: 1.8
8828
+ });
8438
8829
  }
8439
8830
  });
8440
8831