@plasius/gpu-shared 0.1.7 → 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.
@@ -6212,6 +6212,12 @@ var HARBOR_TORCHES = Object.freeze([
6212
6212
  Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
6213
6213
  Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 })
6214
6214
  ]);
6215
+ var FLAG_LAYOUT = Object.freeze({
6216
+ origin: Object.freeze({ x: -3.5, y: 5.9, z: 2.4 }),
6217
+ width: 4.8,
6218
+ height: 2.7,
6219
+ mastOffsetX: 1.8
6220
+ });
6215
6221
  function injectStyles() {
6216
6222
  if (document.getElementById(STYLE_ID)) {
6217
6223
  return;
@@ -6784,6 +6790,250 @@ function normalizeColorOverride(color, fallback) {
6784
6790
  function readVisualNumber(value, fallback) {
6785
6791
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
6786
6792
  }
6793
+ function resolveClothPresentation(state, meshDetail) {
6794
+ const clothPlan = createClothRepresentationPlan({
6795
+ garmentId: "shore-flag",
6796
+ kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
6797
+ profile: state.focus === "cloth" ? "cinematic" : clothProfileNames[0],
6798
+ supportsRayTracing: true,
6799
+ nearFieldMaxMeters: 18,
6800
+ midFieldMaxMeters: 55,
6801
+ farFieldMaxMeters: 180
6802
+ });
6803
+ const preset = CAMERA_PRESETS[state.focus] ?? CAMERA_PRESETS.integrated;
6804
+ const fallbackEye = state.camera.eye ? state.camera.eye : addVec3(
6805
+ state.camera.target,
6806
+ vec3(
6807
+ Math.sin(state.camera.yaw ?? preset.yaw) * Math.cos(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance),
6808
+ Math.sin(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance),
6809
+ Math.cos(state.camera.yaw ?? preset.yaw) * Math.cos(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance)
6810
+ )
6811
+ );
6812
+ const cameraDistance = lengthVec3(subVec3(state.camera.target, fallbackEye));
6813
+ const band = selectClothRepresentationBand(cameraDistance, clothPlan.thresholds);
6814
+ const representation = clothPlan.representations.find((entry) => entry.band === band) ?? clothPlan.representations[0];
6815
+ return {
6816
+ clothPlan,
6817
+ band,
6818
+ continuity: representation.continuity,
6819
+ representation
6820
+ };
6821
+ }
6822
+ function getFlagRestPosition(rows, cols, row, column) {
6823
+ const u = cols <= 1 ? 0 : column / (cols - 1);
6824
+ const v = rows <= 1 ? 0 : row / (rows - 1);
6825
+ return vec3(
6826
+ FLAG_LAYOUT.origin.x + u * FLAG_LAYOUT.mastOffsetX,
6827
+ FLAG_LAYOUT.origin.y - FLAG_LAYOUT.height * v - u * u * 0.08,
6828
+ FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * u
6829
+ );
6830
+ }
6831
+ function buildClothConstraints(rows, cols, restPositions) {
6832
+ const constraints = [];
6833
+ const indexFor = (row, column) => row * cols + column;
6834
+ const pushConstraint = (a, b, stiffness) => {
6835
+ constraints.push(
6836
+ Object.freeze({
6837
+ a,
6838
+ b,
6839
+ restLength: lengthVec3(subVec3(restPositions[a], restPositions[b])),
6840
+ stiffness
6841
+ })
6842
+ );
6843
+ };
6844
+ for (let row = 0; row < rows; row += 1) {
6845
+ for (let column = 0; column < cols; column += 1) {
6846
+ const index = indexFor(row, column);
6847
+ if (column + 1 < cols) {
6848
+ pushConstraint(index, indexFor(row, column + 1), 0.92);
6849
+ }
6850
+ if (row + 1 < rows) {
6851
+ pushConstraint(index, indexFor(row + 1, column), 0.9);
6852
+ }
6853
+ if (column + 1 < cols && row + 1 < rows) {
6854
+ pushConstraint(index, indexFor(row + 1, column + 1), 0.66);
6855
+ }
6856
+ if (column - 1 >= 0 && row + 1 < rows) {
6857
+ pushConstraint(index, indexFor(row + 1, column - 1), 0.66);
6858
+ }
6859
+ if (column + 2 < cols) {
6860
+ pushConstraint(index, indexFor(row, column + 2), 0.22);
6861
+ }
6862
+ if (row + 2 < rows) {
6863
+ pushConstraint(index, indexFor(row + 2, column), 0.18);
6864
+ }
6865
+ }
6866
+ }
6867
+ return Object.freeze(constraints);
6868
+ }
6869
+ function createShowcaseClothSimulationState(options = {}) {
6870
+ const rows = Math.max(4, options.rows ?? 11);
6871
+ const cols = Math.max(4, options.cols ?? 16);
6872
+ const continuity = options.continuity ?? {
6873
+ broadMotionFloor: 0.72,
6874
+ wrinkleFloor: 0.56
6875
+ };
6876
+ const representation = options.representation ?? {
6877
+ mesh: {
6878
+ solverIterations: 6,
6879
+ wrinkleLayers: 2
6880
+ }
6881
+ };
6882
+ const restPositions = [];
6883
+ const positions = [];
6884
+ const previousPositions = [];
6885
+ const uvs = [];
6886
+ const phaseOffsets = [];
6887
+ const pinned = [];
6888
+ for (let row = 0; row < rows; row += 1) {
6889
+ for (let column = 0; column < cols; column += 1) {
6890
+ const index = row * cols + column;
6891
+ const u = cols <= 1 ? 0 : column / (cols - 1);
6892
+ const v = rows <= 1 ? 0 : row / (rows - 1);
6893
+ const rest = getFlagRestPosition(rows, cols, row, column);
6894
+ const preload = vec3(
6895
+ u * 0.04,
6896
+ Math.sin(v * Math.PI) * 0.02 * continuity.wrinkleFloor,
6897
+ -u * 0.12
6898
+ );
6899
+ const pinnedPoint = column === 0;
6900
+ restPositions.push(rest);
6901
+ positions.push(pinnedPoint ? vec3(rest.x, rest.y, rest.z) : addVec3(rest, preload));
6902
+ previousPositions.push(
6903
+ pinnedPoint ? vec3(rest.x, rest.y, rest.z) : addVec3(rest, scaleVec3(preload, 0.35))
6904
+ );
6905
+ uvs.push(Object.freeze({ u, v }));
6906
+ phaseOffsets.push(pseudoRandom(index + 17) * Math.PI * 2);
6907
+ pinned.push(pinnedPoint);
6908
+ }
6909
+ }
6910
+ return {
6911
+ rows,
6912
+ cols,
6913
+ continuity,
6914
+ representation,
6915
+ restPositions,
6916
+ positions,
6917
+ previousPositions,
6918
+ constraints: buildClothConstraints(rows, cols, restPositions),
6919
+ indices: Object.freeze(
6920
+ Array.from({ length: (rows - 1) * (cols - 1) * 6 }, (_, listIndex) => listIndex).map((_, listIndex, source) => {
6921
+ if (listIndex >= source.length) {
6922
+ return 0;
6923
+ }
6924
+ const quadIndex = Math.floor(listIndex / 6);
6925
+ const quadColumn = quadIndex % (cols - 1);
6926
+ const quadRow = Math.floor(quadIndex / (cols - 1));
6927
+ const base = quadRow * cols + quadColumn;
6928
+ return [base, base + 1, base + cols + 1, base, base + cols + 1, base + cols][listIndex % 6];
6929
+ })
6930
+ ),
6931
+ uvs,
6932
+ phaseOffsets,
6933
+ pinned
6934
+ };
6935
+ }
6936
+ function resetPinnedClothPoints(clothState) {
6937
+ for (let index = 0; index < clothState.positions.length; index += 1) {
6938
+ if (!clothState.pinned[index]) {
6939
+ continue;
6940
+ }
6941
+ const anchor = clothState.restPositions[index];
6942
+ clothState.positions[index] = vec3(anchor.x, anchor.y, anchor.z);
6943
+ clothState.previousPositions[index] = vec3(anchor.x, anchor.y, anchor.z);
6944
+ }
6945
+ }
6946
+ function satisfyClothConstraint(clothState, constraint) {
6947
+ const a = clothState.positions[constraint.a];
6948
+ const b = clothState.positions[constraint.b];
6949
+ const delta = subVec3(b, a);
6950
+ const distance = lengthVec3(delta);
6951
+ if (distance <= 1e-4) {
6952
+ return;
6953
+ }
6954
+ const correctionScale = (distance - constraint.restLength) / distance * 0.5 * constraint.stiffness;
6955
+ const correction = scaleVec3(delta, correctionScale);
6956
+ if (!clothState.pinned[constraint.a]) {
6957
+ clothState.positions[constraint.a] = addVec3(a, correction);
6958
+ }
6959
+ if (!clothState.pinned[constraint.b]) {
6960
+ clothState.positions[constraint.b] = subVec3(b, correction);
6961
+ }
6962
+ }
6963
+ function advanceShowcaseClothSimulationState(clothState, options = {}) {
6964
+ const dt = clamp(options.dt ?? 1 / 60, 1 / 240, 1 / 18);
6965
+ const time = readVisualNumber(options.time, 0);
6966
+ const flagMotion = readVisualNumber(options.flagMotion, 0.92);
6967
+ const waveInfluence = readVisualNumber(options.waveInfluence, 0);
6968
+ const wrinkleLayers = Math.max(1, clothState.representation.mesh?.wrinkleLayers ?? 2);
6969
+ const solverIterations = clamp(
6970
+ Math.round(clothState.representation.mesh?.solverIterations ?? 6),
6971
+ 2,
6972
+ 10
6973
+ );
6974
+ for (let index = 0; index < clothState.positions.length; index += 1) {
6975
+ if (clothState.pinned[index]) {
6976
+ continue;
6977
+ }
6978
+ const current = clothState.positions[index];
6979
+ const previous = clothState.previousPositions[index];
6980
+ const { u, v } = clothState.uvs[index];
6981
+ const phase = clothState.phaseOffsets[index];
6982
+ const broadMotion = clothState.continuity.broadMotionFloor;
6983
+ const wrinkleMotion = clothState.continuity.wrinkleFloor;
6984
+ const gustPhase = time * 2.1 + phase + u * 4.4 + v * 2.3;
6985
+ const wrinklePhase = time * 5.3 + phase * 0.72 + u * 9.6 + v * 7.1;
6986
+ const windDirection = normalizeVec3(
6987
+ vec3(
6988
+ 0.18 + Math.sin(gustPhase) * (0.12 + broadMotion * 0.09),
6989
+ Math.cos(time * 1.4 + phase + v * 4.8) * 0.06 * wrinkleMotion,
6990
+ 1 + Math.sin(gustPhase * 0.74) * 0.18
6991
+ )
6992
+ );
6993
+ const windStrength = (1.6 + broadMotion * 1.25 + wrinkleLayers * 0.12) * flagMotion * (0.44 + u * 1.14);
6994
+ const wrinkleForce = vec3(
6995
+ Math.sin(wrinklePhase) * 0.22 * wrinkleMotion * flagMotion,
6996
+ Math.cos(wrinklePhase * 0.7) * 0.08 * wrinkleMotion,
6997
+ Math.cos(wrinklePhase) * 0.14 * broadMotion * flagMotion
6998
+ );
6999
+ const acceleration = addVec3(
7000
+ vec3(0, -0.48 - u * 0.08, 0),
7001
+ addVec3(
7002
+ scaleVec3(windDirection, windStrength),
7003
+ addVec3(
7004
+ wrinkleForce,
7005
+ vec3(waveInfluence * (0.04 + u * 0.08), 0, waveInfluence * 0.16)
7006
+ )
7007
+ )
7008
+ );
7009
+ const inertia = scaleVec3(subVec3(current, previous), 0.987);
7010
+ const next = addVec3(addVec3(current, inertia), scaleVec3(acceleration, dt * dt));
7011
+ clothState.previousPositions[index] = vec3(current.x, current.y, current.z);
7012
+ clothState.positions[index] = next;
7013
+ }
7014
+ resetPinnedClothPoints(clothState);
7015
+ for (let iteration = 0; iteration < solverIterations; iteration += 1) {
7016
+ for (const constraint of clothState.constraints) {
7017
+ satisfyClothConstraint(clothState, constraint);
7018
+ }
7019
+ resetPinnedClothPoints(clothState);
7020
+ }
7021
+ return clothState;
7022
+ }
7023
+ function ensureShowcaseClothState(state, meshDetail, clothPresentation) {
7024
+ if (!state.clothState || state.clothState.rows !== meshDetail.rows || state.clothState.cols !== meshDetail.cols) {
7025
+ state.clothState = createShowcaseClothSimulationState({
7026
+ rows: meshDetail.rows,
7027
+ cols: meshDetail.cols,
7028
+ continuity: clothPresentation.continuity,
7029
+ representation: clothPresentation.representation
7030
+ });
7031
+ } else {
7032
+ state.clothState.continuity = clothPresentation.continuity;
7033
+ state.clothState.representation = clothPresentation.representation;
7034
+ }
7035
+ return state.clothState;
7036
+ }
6787
7037
  function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
6788
7038
  const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6789
7039
  const defaults = {
@@ -6867,58 +7117,17 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
6867
7117
  };
6868
7118
  }
6869
7119
  function buildClothSurface(model, state, meshDetail, visuals) {
6870
- const clothPlan = createClothRepresentationPlan({
6871
- garmentId: "shore-flag",
6872
- kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
6873
- profile: state.focus === "cloth" ? "cinematic" : clothProfileNames[0],
6874
- supportsRayTracing: true,
6875
- nearFieldMaxMeters: 18,
6876
- midFieldMaxMeters: 55,
6877
- farFieldMaxMeters: 180
6878
- });
6879
- const cameraDistance = lengthVec3(subVec3(state.camera.target, state.camera.eye ?? vec3(...CAMERA_PRESETS[state.focus].target)));
6880
- const band = selectClothRepresentationBand(cameraDistance, clothPlan.thresholds);
6881
- const representation = clothPlan.representations.find((entry) => entry.band === band) ?? clothPlan.representations[0];
6882
- const continuity = createClothContinuityEnvelope({ garmentId: "shore-flag" });
6883
- const cols = meshDetail.cols;
6884
- const rows = meshDetail.rows;
6885
- const origin = vec3(-3.5, 5.9, 2.4);
6886
- const width = 4.8;
6887
- const height = 2.7;
6888
- const positions = [];
6889
- const indices = [];
6890
- const time = state.time;
6891
- for (let row = 0; row < rows; row += 1) {
6892
- for (let column = 0; column < cols; column += 1) {
6893
- const u = column / (cols - 1);
6894
- const v = row / (rows - 1);
6895
- const gust = Math.sin(time * 1.9 + v * 3.2 + u * 2.1) * continuity.broadMotionFloor * visuals.flagMotion;
6896
- const wrinkle = Math.sin(time * 4.4 + u * 9.2 + v * 5.6) * continuity.wrinkleFloor * 0.22 * Math.max(0.55, visuals.flagMotion);
6897
- const x = origin.x + u * 1.8 + gust * 0.55 * (u * 0.9);
6898
- const y = origin.y - height * v + wrinkle * 0.2;
6899
- const z = origin.z + width * u + gust * 0.72 * (u * 0.85);
6900
- const flap = Math.cos(time * 2.7 + u * 7.4 + v * 3.8) * continuity.broadMotionFloor * 0.28 * visuals.flagMotion;
6901
- positions.push(vec3(x + flap, y, z));
6902
- }
6903
- }
6904
- for (let row = 0; row < rows - 1; row += 1) {
6905
- for (let column = 0; column < cols - 1; column += 1) {
6906
- const a = row * cols + column;
6907
- const b = a + 1;
6908
- const c = a + cols + 1;
6909
- const d = a + cols;
6910
- indices.push(a, b, c, a, c, d);
6911
- }
6912
- }
7120
+ const clothPresentation = resolveClothPresentation(state, meshDetail);
7121
+ const clothState = ensureShowcaseClothState(state, meshDetail, clothPresentation);
6913
7122
  return {
6914
- clothPlan,
6915
- band,
6916
- representation,
6917
- continuity,
7123
+ clothPlan: clothPresentation.clothPlan,
7124
+ band: clothPresentation.band,
7125
+ representation: clothPresentation.representation,
7126
+ continuity: clothPresentation.continuity,
6918
7127
  color: visuals.flagColor,
6919
- positions,
6920
- indices,
6921
- grid: { rows, cols }
7128
+ positions: clothState.positions.map((point) => vec3(point.x, point.y, point.z)),
7129
+ indices: clothState.indices,
7130
+ grid: { rows: clothState.rows, cols: clothState.cols }
6922
7131
  };
6923
7132
  }
6924
7133
  function resolveWaveDirection(state) {
@@ -6982,6 +7191,59 @@ function sampleWave(state, x, z, time) {
6982
7191
  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;
6983
7192
  return base * amplitude + sampleShipWake(state, x, z, time) + sampleWaveImpulses(state, x, z, time);
6984
7193
  }
7194
+ function buildWaterMotionEffects(state) {
7195
+ const wakeTrails = [];
7196
+ const rippleRings = state.waveImpulses.map((impulse) => {
7197
+ const radius = impulse.radius + (1 - impulse.life) * 4.8;
7198
+ return Object.freeze({
7199
+ center: vec3(
7200
+ impulse.x,
7201
+ sampleWave(state, impulse.x, impulse.z, state.time) * 0.24 + 0.06,
7202
+ impulse.z
7203
+ ),
7204
+ radius,
7205
+ opacity: clamp(impulse.life * 0.28, 0.08, 0.3)
7206
+ });
7207
+ });
7208
+ for (const ship of state.ships) {
7209
+ const speed = Math.hypot(ship.velocity.x, ship.velocity.z);
7210
+ if (speed <= 0.18) {
7211
+ continue;
7212
+ }
7213
+ const direction = normalizeVec3(vec3(ship.velocity.x, 0, ship.velocity.z));
7214
+ const behind = scaleVec3(direction, -1);
7215
+ const lateral = vec3(-direction.z, 0, direction.x);
7216
+ const points = [];
7217
+ for (let sampleIndex = 0; sampleIndex < 6; sampleIndex += 1) {
7218
+ const along = 1 + sampleIndex * 1.45;
7219
+ const lateralOffset = Math.sin(state.time * 1.2 + sampleIndex * 0.8 + readVisualNumber(ship.wanderPhase, 0)) * 0.12;
7220
+ const worldPoint = addVec3(
7221
+ ship.position,
7222
+ addVec3(scaleVec3(behind, along), scaleVec3(lateral, lateralOffset))
7223
+ );
7224
+ points.push(
7225
+ Object.freeze({
7226
+ center: vec3(
7227
+ worldPoint.x,
7228
+ sampleWave(state, worldPoint.x, worldPoint.z, state.time) * 0.24 + 0.04,
7229
+ worldPoint.z
7230
+ ),
7231
+ width: 0.34 + sampleIndex * 0.13
7232
+ })
7233
+ );
7234
+ }
7235
+ wakeTrails.push(
7236
+ Object.freeze({
7237
+ opacity: clamp(0.18 + speed * 0.09, 0.22, 0.46),
7238
+ points: Object.freeze(points)
7239
+ })
7240
+ );
7241
+ }
7242
+ return Object.freeze({
7243
+ wakeTrails: Object.freeze(wakeTrails),
7244
+ rippleRings: Object.freeze(rippleRings)
7245
+ });
7246
+ }
6985
7247
  function buildWaterBands(state, fluidDetail, visuals) {
6986
7248
  const fluidPlan = createFluidRepresentationPlan({
6987
7249
  fluidBodyId: "harbor",
@@ -7134,6 +7396,7 @@ function createSceneState(options) {
7134
7396
  contactCount: 0,
7135
7397
  collisionCount: 0,
7136
7398
  collisionFlash: 0,
7399
+ clothState: null,
7137
7400
  physics: {
7138
7401
  profile: physicsProfile,
7139
7402
  plan: physicsPlan,
@@ -7769,16 +8032,73 @@ function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength
7769
8032
  blur: 12 + shadowStrength * 20
7770
8033
  });
7771
8034
  }
7772
- function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glowScale, reflectionStrength = 0, state = null) {
7773
- const projected = projectPoint(point, camera, viewport);
8035
+ function collectSceneLightSources(state, visuals) {
8036
+ const directLights = [];
8037
+ const reflectionLights = [];
8038
+ const pushLight = (point, glowScale, reflectionStrength, coreColor, glowColor) => {
8039
+ directLights.push(
8040
+ Object.freeze({
8041
+ pass: "direct-glow",
8042
+ point,
8043
+ coreColor,
8044
+ glowColor,
8045
+ glowScale
8046
+ })
8047
+ );
8048
+ if (reflectionStrength > 0) {
8049
+ reflectionLights.push(
8050
+ Object.freeze({
8051
+ pass: "water-reflection",
8052
+ point,
8053
+ coreColor,
8054
+ glowColor,
8055
+ glowScale,
8056
+ reflectionStrength
8057
+ })
8058
+ );
8059
+ }
8060
+ };
8061
+ for (const torch of HARBOR_TORCHES) {
8062
+ pushLight(
8063
+ vec3(torch.x, torch.y, torch.z),
8064
+ torch.glow,
8065
+ visuals.lanternReflectionStrength * 0.55,
8066
+ visuals.torchCore,
8067
+ visuals.torchGlow
8068
+ );
8069
+ }
8070
+ for (const ship of state.ships) {
8071
+ const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
8072
+ const strength = readVisualNumber(ship.lanternStrength, 1);
8073
+ for (const lantern of lanterns) {
8074
+ const point = transformPoint(
8075
+ vec3(lantern.x, lantern.y, lantern.z),
8076
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
8077
+ );
8078
+ pushLight(
8079
+ point,
8080
+ lantern.glow * strength,
8081
+ visuals.lanternReflectionStrength,
8082
+ visuals.lanternCore,
8083
+ visuals.lanternGlow
8084
+ );
8085
+ }
8086
+ }
8087
+ return Object.freeze({
8088
+ directLights: Object.freeze(directLights),
8089
+ reflectionLights: Object.freeze(reflectionLights)
8090
+ });
8091
+ }
8092
+ function renderDirectLightGlow(ctx, source, camera, viewport) {
8093
+ const projected = projectPoint(source.point, camera, viewport);
7774
8094
  if (!projected) {
7775
8095
  return;
7776
8096
  }
7777
- const radius = clamp(1 / projected.depth * 420 * glowScale, 4, 34);
8097
+ const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
7778
8098
  const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
7779
- halo.addColorStop(0, colorToRgba(coreColor, 0.98));
7780
- halo.addColorStop(0.5, colorToRgba(glowColor, 0.42));
7781
- halo.addColorStop(1, colorToRgba(glowColor, 0));
8099
+ halo.addColorStop(0, colorToRgba(source.coreColor, 0.98));
8100
+ halo.addColorStop(0.5, colorToRgba(source.glowColor, 0.42));
8101
+ halo.addColorStop(1, colorToRgba(source.glowColor, 0));
7782
8102
  ctx.save();
7783
8103
  ctx.globalCompositeOperation = "screen";
7784
8104
  ctx.fillStyle = halo;
@@ -7786,79 +8106,119 @@ function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glo
7786
8106
  ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
7787
8107
  ctx.fill();
7788
8108
  ctx.restore();
7789
- ctx.fillStyle = colorToRgba(coreColor, 0.98);
8109
+ ctx.fillStyle = colorToRgba(source.coreColor, 0.98);
7790
8110
  ctx.beginPath();
7791
8111
  ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
7792
8112
  ctx.fill();
7793
- if (state && reflectionStrength > 0) {
7794
- const waterline = sampleWave(state, point.x, point.z, state.time) * 0.22;
7795
- const reflectedPoint = vec3(point.x, waterline - (point.y - waterline) * 0.58, point.z + 0.08);
7796
- const reflected = projectPoint(reflectedPoint, camera, viewport);
7797
- if (reflected) {
7798
- const reflectionRadius = radius * 0.72;
7799
- const glow = ctx.createRadialGradient(
7800
- reflected.x,
7801
- reflected.y,
7802
- reflectionRadius * 0.1,
7803
- reflected.x,
7804
- reflected.y,
7805
- reflectionRadius
7806
- );
7807
- glow.addColorStop(0, colorToRgba(coreColor, reflectionStrength * 0.34));
7808
- glow.addColorStop(1, colorToRgba(glowColor, 0));
7809
- ctx.save();
7810
- ctx.globalCompositeOperation = "screen";
7811
- ctx.fillStyle = glow;
8113
+ }
8114
+ function renderWaterLightReflection(ctx, source, state, camera, viewport) {
8115
+ const projected = projectPoint(source.point, camera, viewport);
8116
+ if (!projected) {
8117
+ return;
8118
+ }
8119
+ const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
8120
+ const waterline = sampleWave(state, source.point.x, source.point.z, state.time) * 0.22;
8121
+ const reflectedPoint = vec3(
8122
+ source.point.x,
8123
+ waterline - (source.point.y - waterline) * 0.58,
8124
+ source.point.z + 0.08
8125
+ );
8126
+ const reflected = projectPoint(reflectedPoint, camera, viewport);
8127
+ if (!reflected) {
8128
+ return;
8129
+ }
8130
+ const reflectionRadius = radius * 0.72;
8131
+ const glow = ctx.createRadialGradient(
8132
+ reflected.x,
8133
+ reflected.y,
8134
+ reflectionRadius * 0.1,
8135
+ reflected.x,
8136
+ reflected.y,
8137
+ reflectionRadius
8138
+ );
8139
+ glow.addColorStop(0, colorToRgba(source.coreColor, source.reflectionStrength * 0.34));
8140
+ glow.addColorStop(1, colorToRgba(source.glowColor, 0));
8141
+ ctx.save();
8142
+ ctx.globalCompositeOperation = "screen";
8143
+ ctx.fillStyle = glow;
8144
+ ctx.beginPath();
8145
+ ctx.ellipse(
8146
+ reflected.x,
8147
+ reflected.y,
8148
+ reflectionRadius * 0.34,
8149
+ reflectionRadius,
8150
+ 0,
8151
+ 0,
8152
+ Math.PI * 2
8153
+ );
8154
+ ctx.fill();
8155
+ ctx.restore();
8156
+ }
8157
+ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
8158
+ ctx.save();
8159
+ ctx.globalCompositeOperation = "screen";
8160
+ for (const wake of effects.wakeTrails) {
8161
+ const projected = wake.points.map((point) => ({
8162
+ projected: projectPoint(point.center, camera, viewport),
8163
+ width: point.width
8164
+ })).filter((entry) => entry.projected);
8165
+ if (projected.length < 2) {
8166
+ continue;
8167
+ }
8168
+ const averageDepth = projected.reduce((total, entry) => total + entry.projected.depth, 0) / projected.length;
8169
+ const averageWidth = projected.reduce((total, entry) => total + entry.width, 0) / projected.length;
8170
+ const baseWidth = clamp(averageWidth / Math.max(0.25, averageDepth) * 180, 1.6, 5.4);
8171
+ ctx.strokeStyle = `rgba(146, 194, 236, ${wake.opacity * 0.52})`;
8172
+ ctx.lineWidth = baseWidth * 1.9;
8173
+ ctx.lineCap = "round";
8174
+ ctx.lineJoin = "round";
8175
+ ctx.beginPath();
8176
+ ctx.moveTo(projected[0].projected.x, projected[0].projected.y);
8177
+ for (let index = 1; index < projected.length; index += 1) {
8178
+ ctx.lineTo(projected[index].projected.x, projected[index].projected.y);
8179
+ }
8180
+ ctx.stroke();
8181
+ ctx.strokeStyle = `rgba(234, 247, 255, ${wake.opacity})`;
8182
+ ctx.lineWidth = baseWidth;
8183
+ ctx.lineCap = "round";
8184
+ ctx.lineJoin = "round";
8185
+ ctx.beginPath();
8186
+ ctx.moveTo(projected[0].projected.x, projected[0].projected.y);
8187
+ for (let index = 1; index < projected.length; index += 1) {
8188
+ ctx.lineTo(projected[index].projected.x, projected[index].projected.y);
8189
+ }
8190
+ ctx.stroke();
8191
+ for (const entry of projected.slice(1, 5)) {
8192
+ ctx.fillStyle = `rgba(239, 248, 255, ${wake.opacity * 0.76})`;
7812
8193
  ctx.beginPath();
7813
8194
  ctx.ellipse(
7814
- reflected.x,
7815
- reflected.y,
7816
- reflectionRadius * 0.34,
7817
- reflectionRadius,
8195
+ entry.projected.x,
8196
+ entry.projected.y,
8197
+ baseWidth * 0.72,
8198
+ baseWidth * 0.44,
7818
8199
  0,
7819
8200
  0,
7820
8201
  Math.PI * 2
7821
8202
  );
7822
8203
  ctx.fill();
7823
- ctx.restore();
7824
8204
  }
7825
8205
  }
7826
- }
7827
- function renderShipLanterns(ctx, ship, state, camera, viewport, visuals) {
7828
- const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
7829
- const strength = readVisualNumber(ship.lanternStrength, 1);
7830
- for (const lantern of lanterns) {
7831
- const position = transformPoint(
7832
- vec3(lantern.x, lantern.y, lantern.z),
7833
- { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
7834
- );
7835
- renderGlowLight(
7836
- ctx,
7837
- position,
7838
- camera,
7839
- viewport,
7840
- visuals.lanternCore,
7841
- visuals.lanternGlow,
7842
- lantern.glow * strength,
7843
- visuals.lanternReflectionStrength,
7844
- state
7845
- );
7846
- }
7847
- }
7848
- function renderHarborTorches(ctx, state, camera, viewport, visuals) {
7849
- for (const torch of HARBOR_TORCHES) {
7850
- renderGlowLight(
7851
- ctx,
7852
- vec3(torch.x, torch.y, torch.z),
7853
- camera,
7854
- viewport,
7855
- visuals.torchCore,
7856
- visuals.torchGlow,
7857
- torch.glow,
7858
- visuals.lanternReflectionStrength * 0.55,
7859
- state
7860
- );
8206
+ for (const ring of effects.rippleRings) {
8207
+ const center = projectPoint(ring.center, camera, viewport);
8208
+ const xAxis = projectPoint(addVec3(ring.center, vec3(ring.radius, 0, 0)), camera, viewport);
8209
+ const zAxis = projectPoint(addVec3(ring.center, vec3(0, 0, ring.radius)), camera, viewport);
8210
+ if (!center || !xAxis || !zAxis) {
8211
+ continue;
8212
+ }
8213
+ const radiusX = Math.hypot(xAxis.x - center.x, xAxis.y - center.y);
8214
+ const radiusY = Math.hypot(zAxis.x - center.x, zAxis.y - center.y);
8215
+ ctx.strokeStyle = `rgba(216, 235, 255, ${ring.opacity})`;
8216
+ ctx.lineWidth = clamp((radiusX + radiusY) * 0.02, 1, 3.1);
8217
+ ctx.beginPath();
8218
+ ctx.ellipse(center.x, center.y, radiusX, radiusY, 0, 0, Math.PI * 2);
8219
+ ctx.stroke();
7861
8220
  }
8221
+ ctx.restore();
7862
8222
  }
7863
8223
  function renderScene(ctx, canvas, state, shipModel, dom) {
7864
8224
  const viewport = { width: canvas.width, height: canvas.height };
@@ -7888,8 +8248,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7888
8248
  shadowStrength,
7889
8249
  visuals
7890
8250
  );
7891
- const triangles = [];
7892
- pushHarborGeometry(camera, viewport, triangles, visuals);
8251
+ const waterTriangles = [];
8252
+ const sceneTriangles = [];
7893
8253
  const water = buildWaterBands(
7894
8254
  state,
7895
8255
  state.fluidDetail.getSnapshot().currentLevel.config,
@@ -7906,7 +8266,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7906
8266
  if (projected.some((value) => value === null)) {
7907
8267
  continue;
7908
8268
  }
7909
- triangles.push({
8269
+ waterTriangles.push({
7910
8270
  points: projected,
7911
8271
  depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
7912
8272
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
@@ -7916,6 +8276,9 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7916
8276
  });
7917
8277
  }
7918
8278
  }
8279
+ const waterMotionEffects = buildWaterMotionEffects(state);
8280
+ const lightSources = collectSceneLightSources(state, visuals);
8281
+ pushHarborGeometry(camera, viewport, sceneTriangles, visuals);
7919
8282
  const cloth = buildClothSurface(
7920
8283
  state,
7921
8284
  state,
@@ -7931,7 +8294,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7931
8294
  if (projected.some((value) => value === null)) {
7932
8295
  continue;
7933
8296
  }
7934
- triangles.push({
8297
+ sceneTriangles.push({
7935
8298
  points: projected,
7936
8299
  depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
7937
8300
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
@@ -7947,22 +8310,28 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7947
8310
  ship.tint,
7948
8311
  camera,
7949
8312
  viewport,
7950
- triangles,
8313
+ sceneTriangles,
7951
8314
  nearLighting.rtParticipation.directShadows === "premium" ? 0.08 : 0.02
7952
8315
  );
7953
8316
  }
8317
+ drawTriangles(ctx, waterTriangles, lightDir, reflectionStrength, camera, shadowStrength);
7954
8318
  for (const ship of state.ships) {
7955
8319
  renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength);
7956
8320
  }
7957
8321
  renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
7958
- drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength);
8322
+ for (const source of lightSources.reflectionLights) {
8323
+ renderWaterLightReflection(ctx, source, state, camera, viewport);
8324
+ }
8325
+ renderWaterMotionEffects(ctx, waterMotionEffects, camera, viewport);
7959
8326
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
7960
- renderHarborTorches(ctx, state, camera, viewport, visuals);
8327
+ drawTriangles(ctx, sceneTriangles, lightDir, reflectionStrength, camera, shadowStrength);
7961
8328
  renderFlagPole(ctx, camera, viewport);
7962
8329
  renderClothAccent(ctx, cloth, camera, viewport);
8330
+ for (const source of lightSources.directLights) {
8331
+ renderDirectLightGlow(ctx, source, camera, viewport);
8332
+ }
7963
8333
  for (const ship of state.ships) {
7964
8334
  renderShipRigging(ctx, ship, camera, viewport);
7965
- renderShipLanterns(ctx, ship, state, camera, viewport, visuals);
7966
8335
  }
7967
8336
  renderSprays(ctx, state.sprays, camera, viewport);
7968
8337
  const debugSnapshot = state.debugSession.getSnapshot();
@@ -8024,6 +8393,21 @@ function updateSceneState(state, dt, shipModel) {
8024
8393
  updateShips(state, dt, shipModel);
8025
8394
  updateWaveImpulses(state, dt);
8026
8395
  updateSpray(state, dt);
8396
+ const clothPresentation = resolveClothPresentation(
8397
+ state,
8398
+ state.clothDetail.getSnapshot().currentLevel.config
8399
+ );
8400
+ const clothState = ensureShowcaseClothState(
8401
+ state,
8402
+ state.clothDetail.getSnapshot().currentLevel.config,
8403
+ clothPresentation
8404
+ );
8405
+ advanceShowcaseClothSimulationState(clothState, {
8406
+ dt,
8407
+ time: state.time,
8408
+ flagMotion: readVisualNumber(state.demoVisuals?.flagMotion, 0.92),
8409
+ waveInfluence: sampleWave(state, FLAG_LAYOUT.origin.x + FLAG_LAYOUT.width * 0.55, FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * 0.48, state.time)
8410
+ });
8027
8411
  updatePhysicsSnapshot(state, shipModel);
8028
8412
  }
8029
8413
  function syncTextState(state, shipModel) {
@@ -8191,7 +8575,11 @@ function updatePhysicsSnapshot(state, shipModel) {
8191
8575
  });
8192
8576
  }
8193
8577
  export {
8578
+ advanceShowcaseClothSimulationState as __testOnlyAdvanceShowcaseClothSimulationState,
8579
+ buildWaterMotionEffects as __testOnlyBuildWaterMotionEffects,
8580
+ collectSceneLightSources as __testOnlyCollectSceneLightSources,
8581
+ createShowcaseClothSimulationState as __testOnlyCreateShowcaseClothSimulationState,
8194
8582
  mountGpuShowcase,
8195
8583
  showcaseFocusModes
8196
8584
  };
8197
- //# sourceMappingURL=showcase-runtime-AIPDRK7G.js.map
8585
+ //# sourceMappingURL=showcase-runtime-55OVDYWT.js.map