@plasius/gpu-shared 0.1.7 → 0.1.10

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
@@ -6342,6 +6342,10 @@ var init_browser = __esm({
6342
6342
  // src/showcase-runtime.js
6343
6343
  var showcase_runtime_exports = {};
6344
6344
  __export(showcase_runtime_exports, {
6345
+ __testOnlyAdvanceShowcaseClothSimulationState: () => advanceShowcaseClothSimulationState,
6346
+ __testOnlyBuildWaterMotionEffects: () => buildWaterMotionEffects,
6347
+ __testOnlyCollectSceneLightSources: () => collectSceneLightSources,
6348
+ __testOnlyCreateShowcaseClothSimulationState: () => createShowcaseClothSimulationState,
6345
6349
  mountGpuShowcase: () => mountGpuShowcase,
6346
6350
  showcaseFocusModes: () => showcaseFocusModes
6347
6351
  });
@@ -6917,6 +6921,250 @@ function normalizeColorOverride(color, fallback) {
6917
6921
  function readVisualNumber(value, fallback) {
6918
6922
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
6919
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
+ }
6920
7168
  function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
6921
7169
  const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6922
7170
  const defaults = {
@@ -7000,58 +7248,17 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
7000
7248
  };
7001
7249
  }
7002
7250
  function buildClothSurface(model, state, meshDetail, visuals) {
7003
- const clothPlan = createClothRepresentationPlan({
7004
- garmentId: "shore-flag",
7005
- kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
7006
- profile: state.focus === "cloth" ? "cinematic" : clothProfileNames[0],
7007
- supportsRayTracing: true,
7008
- nearFieldMaxMeters: 18,
7009
- midFieldMaxMeters: 55,
7010
- farFieldMaxMeters: 180
7011
- });
7012
- const cameraDistance = lengthVec3(subVec3(state.camera.target, state.camera.eye ?? vec3(...CAMERA_PRESETS[state.focus].target)));
7013
- const band = selectClothRepresentationBand(cameraDistance, clothPlan.thresholds);
7014
- const representation = clothPlan.representations.find((entry) => entry.band === band) ?? clothPlan.representations[0];
7015
- const continuity = createClothContinuityEnvelope({ garmentId: "shore-flag" });
7016
- const cols = meshDetail.cols;
7017
- const rows = meshDetail.rows;
7018
- const origin = vec3(-3.5, 5.9, 2.4);
7019
- const width = 4.8;
7020
- const height = 2.7;
7021
- const positions = [];
7022
- const indices = [];
7023
- const time = state.time;
7024
- for (let row = 0; row < rows; row += 1) {
7025
- for (let column = 0; column < cols; column += 1) {
7026
- const u = column / (cols - 1);
7027
- const v = row / (rows - 1);
7028
- const gust = Math.sin(time * 1.9 + v * 3.2 + u * 2.1) * continuity.broadMotionFloor * visuals.flagMotion;
7029
- const wrinkle = Math.sin(time * 4.4 + u * 9.2 + v * 5.6) * continuity.wrinkleFloor * 0.22 * Math.max(0.55, visuals.flagMotion);
7030
- const x = origin.x + u * 1.8 + gust * 0.55 * (u * 0.9);
7031
- const y = origin.y - height * v + wrinkle * 0.2;
7032
- const z = origin.z + width * u + gust * 0.72 * (u * 0.85);
7033
- const flap = Math.cos(time * 2.7 + u * 7.4 + v * 3.8) * continuity.broadMotionFloor * 0.28 * visuals.flagMotion;
7034
- positions.push(vec3(x + flap, y, z));
7035
- }
7036
- }
7037
- for (let row = 0; row < rows - 1; row += 1) {
7038
- for (let column = 0; column < cols - 1; column += 1) {
7039
- const a = row * cols + column;
7040
- const b = a + 1;
7041
- const c = a + cols + 1;
7042
- const d = a + cols;
7043
- indices.push(a, b, c, a, c, d);
7044
- }
7045
- }
7251
+ const clothPresentation = resolveClothPresentation(state, meshDetail);
7252
+ const clothState = ensureShowcaseClothState(state, meshDetail, clothPresentation);
7046
7253
  return {
7047
- clothPlan,
7048
- band,
7049
- representation,
7050
- continuity,
7254
+ clothPlan: clothPresentation.clothPlan,
7255
+ band: clothPresentation.band,
7256
+ representation: clothPresentation.representation,
7257
+ continuity: clothPresentation.continuity,
7051
7258
  color: visuals.flagColor,
7052
- positions,
7053
- indices,
7054
- 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 }
7055
7262
  };
7056
7263
  }
7057
7264
  function resolveWaveDirection(state) {
@@ -7115,6 +7322,59 @@ function sampleWave(state, x, z, time) {
7115
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;
7116
7323
  return base * amplitude + sampleShipWake(state, x, z, time) + sampleWaveImpulses(state, x, z, time);
7117
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
+ }
7118
7378
  function buildWaterBands(state, fluidDetail, visuals) {
7119
7379
  const fluidPlan = createFluidRepresentationPlan({
7120
7380
  fluidBodyId: "harbor",
@@ -7267,6 +7527,7 @@ function createSceneState(options) {
7267
7527
  contactCount: 0,
7268
7528
  collisionCount: 0,
7269
7529
  collisionFlash: 0,
7530
+ clothState: null,
7270
7531
  physics: {
7271
7532
  profile: physicsProfile,
7272
7533
  plan: physicsPlan,
@@ -7902,16 +8163,73 @@ function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength
7902
8163
  blur: 12 + shadowStrength * 20
7903
8164
  });
7904
8165
  }
7905
- function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glowScale, reflectionStrength = 0, state = null) {
7906
- 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);
7907
8225
  if (!projected) {
7908
8226
  return;
7909
8227
  }
7910
- const radius = clamp(1 / projected.depth * 420 * glowScale, 4, 34);
8228
+ const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
7911
8229
  const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
7912
- halo.addColorStop(0, colorToRgba(coreColor, 0.98));
7913
- halo.addColorStop(0.5, colorToRgba(glowColor, 0.42));
7914
- halo.addColorStop(1, colorToRgba(glowColor, 0));
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));
7915
8233
  ctx.save();
7916
8234
  ctx.globalCompositeOperation = "screen";
7917
8235
  ctx.fillStyle = halo;
@@ -7919,79 +8237,119 @@ function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glo
7919
8237
  ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
7920
8238
  ctx.fill();
7921
8239
  ctx.restore();
7922
- ctx.fillStyle = colorToRgba(coreColor, 0.98);
8240
+ ctx.fillStyle = colorToRgba(source.coreColor, 0.98);
7923
8241
  ctx.beginPath();
7924
8242
  ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
7925
8243
  ctx.fill();
7926
- if (state && reflectionStrength > 0) {
7927
- const waterline = sampleWave(state, point.x, point.z, state.time) * 0.22;
7928
- const reflectedPoint = vec3(point.x, waterline - (point.y - waterline) * 0.58, point.z + 0.08);
7929
- const reflected = projectPoint(reflectedPoint, camera, viewport);
7930
- if (reflected) {
7931
- const reflectionRadius = radius * 0.72;
7932
- const glow = ctx.createRadialGradient(
7933
- reflected.x,
7934
- reflected.y,
7935
- reflectionRadius * 0.1,
7936
- reflected.x,
7937
- reflected.y,
7938
- reflectionRadius
7939
- );
7940
- glow.addColorStop(0, colorToRgba(coreColor, reflectionStrength * 0.34));
7941
- glow.addColorStop(1, colorToRgba(glowColor, 0));
7942
- ctx.save();
7943
- ctx.globalCompositeOperation = "screen";
7944
- ctx.fillStyle = glow;
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})`;
7945
8324
  ctx.beginPath();
7946
8325
  ctx.ellipse(
7947
- reflected.x,
7948
- reflected.y,
7949
- reflectionRadius * 0.34,
7950
- reflectionRadius,
8326
+ entry.projected.x,
8327
+ entry.projected.y,
8328
+ baseWidth * 0.72,
8329
+ baseWidth * 0.44,
7951
8330
  0,
7952
8331
  0,
7953
8332
  Math.PI * 2
7954
8333
  );
7955
8334
  ctx.fill();
7956
- ctx.restore();
7957
8335
  }
7958
8336
  }
7959
- }
7960
- function renderShipLanterns(ctx, ship, state, camera, viewport, visuals) {
7961
- const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
7962
- const strength = readVisualNumber(ship.lanternStrength, 1);
7963
- for (const lantern of lanterns) {
7964
- const position = transformPoint(
7965
- vec3(lantern.x, lantern.y, lantern.z),
7966
- { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
7967
- );
7968
- renderGlowLight(
7969
- ctx,
7970
- position,
7971
- camera,
7972
- viewport,
7973
- visuals.lanternCore,
7974
- visuals.lanternGlow,
7975
- lantern.glow * strength,
7976
- visuals.lanternReflectionStrength,
7977
- state
7978
- );
7979
- }
7980
- }
7981
- function renderHarborTorches(ctx, state, camera, viewport, visuals) {
7982
- for (const torch of HARBOR_TORCHES) {
7983
- renderGlowLight(
7984
- ctx,
7985
- vec3(torch.x, torch.y, torch.z),
7986
- camera,
7987
- viewport,
7988
- visuals.torchCore,
7989
- visuals.torchGlow,
7990
- torch.glow,
7991
- visuals.lanternReflectionStrength * 0.55,
7992
- state
7993
- );
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();
7994
8351
  }
8352
+ ctx.restore();
7995
8353
  }
7996
8354
  function renderScene(ctx, canvas, state, shipModel, dom) {
7997
8355
  const viewport = { width: canvas.width, height: canvas.height };
@@ -8021,8 +8379,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8021
8379
  shadowStrength,
8022
8380
  visuals
8023
8381
  );
8024
- const triangles = [];
8025
- pushHarborGeometry(camera, viewport, triangles, visuals);
8382
+ const waterTriangles = [];
8383
+ const sceneTriangles = [];
8026
8384
  const water = buildWaterBands(
8027
8385
  state,
8028
8386
  state.fluidDetail.getSnapshot().currentLevel.config,
@@ -8039,7 +8397,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8039
8397
  if (projected.some((value) => value === null)) {
8040
8398
  continue;
8041
8399
  }
8042
- triangles.push({
8400
+ waterTriangles.push({
8043
8401
  points: projected,
8044
8402
  depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
8045
8403
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
@@ -8049,6 +8407,9 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8049
8407
  });
8050
8408
  }
8051
8409
  }
8410
+ const waterMotionEffects = buildWaterMotionEffects(state);
8411
+ const lightSources = collectSceneLightSources(state, visuals);
8412
+ pushHarborGeometry(camera, viewport, sceneTriangles, visuals);
8052
8413
  const cloth = buildClothSurface(
8053
8414
  state,
8054
8415
  state,
@@ -8064,7 +8425,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8064
8425
  if (projected.some((value) => value === null)) {
8065
8426
  continue;
8066
8427
  }
8067
- triangles.push({
8428
+ sceneTriangles.push({
8068
8429
  points: projected,
8069
8430
  depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
8070
8431
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
@@ -8080,22 +8441,28 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
8080
8441
  ship.tint,
8081
8442
  camera,
8082
8443
  viewport,
8083
- triangles,
8444
+ sceneTriangles,
8084
8445
  nearLighting.rtParticipation.directShadows === "premium" ? 0.08 : 0.02
8085
8446
  );
8086
8447
  }
8448
+ drawTriangles(ctx, waterTriangles, lightDir, reflectionStrength, camera, shadowStrength);
8087
8449
  for (const ship of state.ships) {
8088
8450
  renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength);
8089
8451
  }
8090
8452
  renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
8091
- 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);
8092
8457
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
8093
- renderHarborTorches(ctx, state, camera, viewport, visuals);
8458
+ drawTriangles(ctx, sceneTriangles, lightDir, reflectionStrength, camera, shadowStrength);
8094
8459
  renderFlagPole(ctx, camera, viewport);
8095
8460
  renderClothAccent(ctx, cloth, camera, viewport);
8461
+ for (const source of lightSources.directLights) {
8462
+ renderDirectLightGlow(ctx, source, camera, viewport);
8463
+ }
8096
8464
  for (const ship of state.ships) {
8097
8465
  renderShipRigging(ctx, ship, camera, viewport);
8098
- renderShipLanterns(ctx, ship, state, camera, viewport, visuals);
8099
8466
  }
8100
8467
  renderSprays(ctx, state.sprays, camera, viewport);
8101
8468
  const debugSnapshot = state.debugSession.getSnapshot();
@@ -8157,6 +8524,21 @@ function updateSceneState(state, dt, shipModel) {
8157
8524
  updateShips(state, dt, shipModel);
8158
8525
  updateWaveImpulses(state, dt);
8159
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
+ });
8160
8542
  updatePhysicsSnapshot(state, shipModel);
8161
8543
  }
8162
8544
  function syncTextState(state, shipModel) {
@@ -8323,7 +8705,7 @@ function updatePhysicsSnapshot(state, shipModel) {
8323
8705
  }
8324
8706
  });
8325
8707
  }
8326
- var STYLE_ID, ROOT_CLASS, DEFAULT_TITLE, DEFAULT_SUBTITLE, SHIP_SCALE, HARBOR_BOUNDS, CAMERA_PRESETS, showcaseFocusModes, SCENE_NOTES, UNIT_BOX_MESH, SHIP_LANTERNS, HARBOR_TORCHES;
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;
8327
8709
  var init_showcase_runtime = __esm({
8328
8710
  "src/showcase-runtime.js"() {
8329
8711
  init_dist();
@@ -8438,6 +8820,12 @@ var init_showcase_runtime = __esm({
8438
8820
  Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
8439
8821
  Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 })
8440
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
+ });
8441
8829
  }
8442
8830
  });
8443
8831