@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.
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  clothGarmentKinds,
3
3
  clothProfileNames,
4
- createClothContinuityEnvelope,
5
4
  createClothRepresentationPlan,
6
5
  selectClothRepresentationBand,
7
6
  } from "@plasius/gpu-cloth";
@@ -97,6 +96,12 @@ const HARBOR_TORCHES = Object.freeze([
97
96
  Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
98
97
  Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 }),
99
98
  ]);
99
+ const FLAG_LAYOUT = Object.freeze({
100
+ origin: Object.freeze({ x: -3.5, y: 5.9, z: 2.4 }),
101
+ width: 4.8,
102
+ height: 2.7,
103
+ mastOffsetX: 1.8,
104
+ });
100
105
  function injectStyles() {
101
106
  if (document.getElementById(STYLE_ID)) {
102
107
  return;
@@ -722,6 +727,282 @@ function readVisualNumber(value, fallback) {
722
727
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
723
728
  }
724
729
 
730
+ function resolveClothPresentation(state, meshDetail) {
731
+ const clothPlan = createClothRepresentationPlan({
732
+ garmentId: "shore-flag",
733
+ kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
734
+ profile: state.focus === "cloth" ? "cinematic" : clothProfileNames[0],
735
+ supportsRayTracing: true,
736
+ nearFieldMaxMeters: 18,
737
+ midFieldMaxMeters: 55,
738
+ farFieldMaxMeters: 180,
739
+ });
740
+ const preset = CAMERA_PRESETS[state.focus] ?? CAMERA_PRESETS.integrated;
741
+ const fallbackEye = state.camera.eye
742
+ ? state.camera.eye
743
+ : addVec3(
744
+ state.camera.target,
745
+ vec3(
746
+ Math.sin(state.camera.yaw ?? preset.yaw) * Math.cos(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance),
747
+ Math.sin(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance),
748
+ Math.cos(state.camera.yaw ?? preset.yaw) * Math.cos(state.camera.pitch ?? preset.pitch) * (state.camera.distance ?? preset.distance)
749
+ )
750
+ );
751
+ const cameraDistance = lengthVec3(subVec3(state.camera.target, fallbackEye));
752
+ const band = selectClothRepresentationBand(cameraDistance, clothPlan.thresholds);
753
+ const representation =
754
+ clothPlan.representations.find((entry) => entry.band === band) ?? clothPlan.representations[0];
755
+ return {
756
+ clothPlan,
757
+ band,
758
+ continuity: representation.continuity,
759
+ representation,
760
+ };
761
+ }
762
+
763
+ function getFlagRestPosition(rows, cols, row, column) {
764
+ const u = cols <= 1 ? 0 : column / (cols - 1);
765
+ const v = rows <= 1 ? 0 : row / (rows - 1);
766
+ return vec3(
767
+ FLAG_LAYOUT.origin.x + u * FLAG_LAYOUT.mastOffsetX,
768
+ FLAG_LAYOUT.origin.y - FLAG_LAYOUT.height * v - u * u * 0.08,
769
+ FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * u
770
+ );
771
+ }
772
+
773
+ function buildClothConstraints(rows, cols, restPositions) {
774
+ const constraints = [];
775
+ const indexFor = (row, column) => row * cols + column;
776
+ const pushConstraint = (a, b, stiffness) => {
777
+ constraints.push(
778
+ Object.freeze({
779
+ a,
780
+ b,
781
+ restLength: lengthVec3(subVec3(restPositions[a], restPositions[b])),
782
+ stiffness,
783
+ })
784
+ );
785
+ };
786
+
787
+ for (let row = 0; row < rows; row += 1) {
788
+ for (let column = 0; column < cols; column += 1) {
789
+ const index = indexFor(row, column);
790
+ if (column + 1 < cols) {
791
+ pushConstraint(index, indexFor(row, column + 1), 0.92);
792
+ }
793
+ if (row + 1 < rows) {
794
+ pushConstraint(index, indexFor(row + 1, column), 0.9);
795
+ }
796
+ if (column + 1 < cols && row + 1 < rows) {
797
+ pushConstraint(index, indexFor(row + 1, column + 1), 0.66);
798
+ }
799
+ if (column - 1 >= 0 && row + 1 < rows) {
800
+ pushConstraint(index, indexFor(row + 1, column - 1), 0.66);
801
+ }
802
+ if (column + 2 < cols) {
803
+ pushConstraint(index, indexFor(row, column + 2), 0.22);
804
+ }
805
+ if (row + 2 < rows) {
806
+ pushConstraint(index, indexFor(row + 2, column), 0.18);
807
+ }
808
+ }
809
+ }
810
+
811
+ return Object.freeze(constraints);
812
+ }
813
+
814
+ function createShowcaseClothSimulationState(options = {}) {
815
+ const rows = Math.max(4, options.rows ?? 11);
816
+ const cols = Math.max(4, options.cols ?? 16);
817
+ const continuity = options.continuity ?? {
818
+ broadMotionFloor: 0.72,
819
+ wrinkleFloor: 0.56,
820
+ };
821
+ const representation = options.representation ?? {
822
+ mesh: {
823
+ solverIterations: 6,
824
+ wrinkleLayers: 2,
825
+ },
826
+ };
827
+ const restPositions = [];
828
+ const positions = [];
829
+ const previousPositions = [];
830
+ const uvs = [];
831
+ const phaseOffsets = [];
832
+ const pinned = [];
833
+
834
+ for (let row = 0; row < rows; row += 1) {
835
+ for (let column = 0; column < cols; column += 1) {
836
+ const index = row * cols + column;
837
+ const u = cols <= 1 ? 0 : column / (cols - 1);
838
+ const v = rows <= 1 ? 0 : row / (rows - 1);
839
+ const rest = getFlagRestPosition(rows, cols, row, column);
840
+ const preload = vec3(
841
+ u * 0.04,
842
+ Math.sin(v * Math.PI) * 0.02 * continuity.wrinkleFloor,
843
+ -u * 0.12
844
+ );
845
+ const pinnedPoint = column === 0;
846
+ restPositions.push(rest);
847
+ positions.push(pinnedPoint ? vec3(rest.x, rest.y, rest.z) : addVec3(rest, preload));
848
+ previousPositions.push(
849
+ pinnedPoint
850
+ ? vec3(rest.x, rest.y, rest.z)
851
+ : addVec3(rest, scaleVec3(preload, 0.35))
852
+ );
853
+ uvs.push(Object.freeze({ u, v }));
854
+ phaseOffsets.push(pseudoRandom(index + 17) * Math.PI * 2);
855
+ pinned.push(pinnedPoint);
856
+ }
857
+ }
858
+
859
+ return {
860
+ rows,
861
+ cols,
862
+ continuity,
863
+ representation,
864
+ restPositions,
865
+ positions,
866
+ previousPositions,
867
+ constraints: buildClothConstraints(rows, cols, restPositions),
868
+ indices: Object.freeze(
869
+ Array.from({ length: (rows - 1) * (cols - 1) * 6 }, (_, listIndex) => listIndex)
870
+ .map((_, listIndex, source) => {
871
+ if (listIndex >= source.length) {
872
+ return 0;
873
+ }
874
+ const quadIndex = Math.floor(listIndex / 6);
875
+ const quadColumn = quadIndex % (cols - 1);
876
+ const quadRow = Math.floor(quadIndex / (cols - 1));
877
+ const base = quadRow * cols + quadColumn;
878
+ return [base, base + 1, base + cols + 1, base, base + cols + 1, base + cols][listIndex % 6];
879
+ })
880
+ ),
881
+ uvs,
882
+ phaseOffsets,
883
+ pinned,
884
+ };
885
+ }
886
+
887
+ function resetPinnedClothPoints(clothState) {
888
+ for (let index = 0; index < clothState.positions.length; index += 1) {
889
+ if (!clothState.pinned[index]) {
890
+ continue;
891
+ }
892
+ const anchor = clothState.restPositions[index];
893
+ clothState.positions[index] = vec3(anchor.x, anchor.y, anchor.z);
894
+ clothState.previousPositions[index] = vec3(anchor.x, anchor.y, anchor.z);
895
+ }
896
+ }
897
+
898
+ function satisfyClothConstraint(clothState, constraint) {
899
+ const a = clothState.positions[constraint.a];
900
+ const b = clothState.positions[constraint.b];
901
+ const delta = subVec3(b, a);
902
+ const distance = lengthVec3(delta);
903
+ if (distance <= 0.0001) {
904
+ return;
905
+ }
906
+
907
+ const correctionScale =
908
+ ((distance - constraint.restLength) / distance) * 0.5 * constraint.stiffness;
909
+ const correction = scaleVec3(delta, correctionScale);
910
+ if (!clothState.pinned[constraint.a]) {
911
+ clothState.positions[constraint.a] = addVec3(a, correction);
912
+ }
913
+ if (!clothState.pinned[constraint.b]) {
914
+ clothState.positions[constraint.b] = subVec3(b, correction);
915
+ }
916
+ }
917
+
918
+ function advanceShowcaseClothSimulationState(clothState, options = {}) {
919
+ const dt = clamp(options.dt ?? 1 / 60, 1 / 240, 1 / 18);
920
+ const time = readVisualNumber(options.time, 0);
921
+ const flagMotion = readVisualNumber(options.flagMotion, 0.92);
922
+ const waveInfluence = readVisualNumber(options.waveInfluence, 0);
923
+ const wrinkleLayers = Math.max(1, clothState.representation.mesh?.wrinkleLayers ?? 2);
924
+ const solverIterations = clamp(
925
+ Math.round(clothState.representation.mesh?.solverIterations ?? 6),
926
+ 2,
927
+ 10
928
+ );
929
+
930
+ for (let index = 0; index < clothState.positions.length; index += 1) {
931
+ if (clothState.pinned[index]) {
932
+ continue;
933
+ }
934
+
935
+ const current = clothState.positions[index];
936
+ const previous = clothState.previousPositions[index];
937
+ const { u, v } = clothState.uvs[index];
938
+ const phase = clothState.phaseOffsets[index];
939
+ const broadMotion = clothState.continuity.broadMotionFloor;
940
+ const wrinkleMotion = clothState.continuity.wrinkleFloor;
941
+ const gustPhase = time * 2.1 + phase + u * 4.4 + v * 2.3;
942
+ const wrinklePhase = time * 5.3 + phase * 0.72 + u * 9.6 + v * 7.1;
943
+ const windDirection = normalizeVec3(
944
+ vec3(
945
+ 0.18 + Math.sin(gustPhase) * (0.12 + broadMotion * 0.09),
946
+ Math.cos(time * 1.4 + phase + v * 4.8) * 0.06 * wrinkleMotion,
947
+ 1 + Math.sin(gustPhase * 0.74) * 0.18
948
+ )
949
+ );
950
+ const windStrength =
951
+ (1.6 + broadMotion * 1.25 + wrinkleLayers * 0.12) *
952
+ flagMotion *
953
+ (0.44 + u * 1.14);
954
+ const wrinkleForce = vec3(
955
+ Math.sin(wrinklePhase) * 0.22 * wrinkleMotion * flagMotion,
956
+ Math.cos(wrinklePhase * 0.7) * 0.08 * wrinkleMotion,
957
+ Math.cos(wrinklePhase) * 0.14 * broadMotion * flagMotion
958
+ );
959
+ const acceleration = addVec3(
960
+ vec3(0, -0.48 - u * 0.08, 0),
961
+ addVec3(
962
+ scaleVec3(windDirection, windStrength),
963
+ addVec3(
964
+ wrinkleForce,
965
+ vec3(waveInfluence * (0.04 + u * 0.08), 0, waveInfluence * 0.16)
966
+ )
967
+ )
968
+ );
969
+ const inertia = scaleVec3(subVec3(current, previous), 0.987);
970
+ const next = addVec3(addVec3(current, inertia), scaleVec3(acceleration, dt * dt));
971
+ clothState.previousPositions[index] = vec3(current.x, current.y, current.z);
972
+ clothState.positions[index] = next;
973
+ }
974
+
975
+ resetPinnedClothPoints(clothState);
976
+ for (let iteration = 0; iteration < solverIterations; iteration += 1) {
977
+ for (const constraint of clothState.constraints) {
978
+ satisfyClothConstraint(clothState, constraint);
979
+ }
980
+ resetPinnedClothPoints(clothState);
981
+ }
982
+
983
+ return clothState;
984
+ }
985
+
986
+ function ensureShowcaseClothState(state, meshDetail, clothPresentation) {
987
+ if (
988
+ !state.clothState ||
989
+ state.clothState.rows !== meshDetail.rows ||
990
+ state.clothState.cols !== meshDetail.cols
991
+ ) {
992
+ state.clothState = createShowcaseClothSimulationState({
993
+ rows: meshDetail.rows,
994
+ cols: meshDetail.cols,
995
+ continuity: clothPresentation.continuity,
996
+ representation: clothPresentation.representation,
997
+ });
998
+ } else {
999
+ state.clothState.continuity = clothPresentation.continuity;
1000
+ state.clothState.representation = clothPresentation.representation;
1001
+ }
1002
+
1003
+ return state.clothState;
1004
+ }
1005
+
725
1006
  function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
726
1007
  const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
727
1008
  const defaults = {
@@ -831,74 +1112,18 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
831
1112
  }
832
1113
 
833
1114
  function buildClothSurface(model, state, meshDetail, visuals) {
834
- const clothPlan = createClothRepresentationPlan({
835
- garmentId: "shore-flag",
836
- kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
837
- profile: state.focus === "cloth" ? "cinematic" : clothProfileNames[0],
838
- supportsRayTracing: true,
839
- nearFieldMaxMeters: 18,
840
- midFieldMaxMeters: 55,
841
- farFieldMaxMeters: 180,
842
- });
843
- const cameraDistance = lengthVec3(subVec3(state.camera.target, state.camera.eye ?? vec3(...CAMERA_PRESETS[state.focus].target)));
844
- const band = selectClothRepresentationBand(cameraDistance, clothPlan.thresholds);
845
- const representation =
846
- clothPlan.representations.find((entry) => entry.band === band) ?? clothPlan.representations[0];
847
- const continuity = createClothContinuityEnvelope({ garmentId: "shore-flag" });
848
-
849
- const cols = meshDetail.cols;
850
- const rows = meshDetail.rows;
851
- const origin = vec3(-3.5, 5.9, 2.4);
852
- const width = 4.8;
853
- const height = 2.7;
854
- const positions = [];
855
- const indices = [];
856
- const time = state.time;
857
-
858
- for (let row = 0; row < rows; row += 1) {
859
- for (let column = 0; column < cols; column += 1) {
860
- const u = column / (cols - 1);
861
- const v = row / (rows - 1);
862
- const gust =
863
- Math.sin(time * 1.9 + v * 3.2 + u * 2.1) *
864
- continuity.broadMotionFloor *
865
- visuals.flagMotion;
866
- const wrinkle =
867
- Math.sin(time * 4.4 + u * 9.2 + v * 5.6) *
868
- continuity.wrinkleFloor *
869
- 0.22 *
870
- Math.max(0.55, visuals.flagMotion);
871
- const x = origin.x + u * 1.8 + gust * 0.55 * (u * 0.9);
872
- const y = origin.y - height * v + wrinkle * 0.2;
873
- const z = origin.z + width * u + gust * 0.72 * (u * 0.85);
874
- const flap =
875
- Math.cos(time * 2.7 + u * 7.4 + v * 3.8) *
876
- continuity.broadMotionFloor *
877
- 0.28 *
878
- visuals.flagMotion;
879
- positions.push(vec3(x + flap, y, z));
880
- }
881
- }
882
-
883
- for (let row = 0; row < rows - 1; row += 1) {
884
- for (let column = 0; column < cols - 1; column += 1) {
885
- const a = row * cols + column;
886
- const b = a + 1;
887
- const c = a + cols + 1;
888
- const d = a + cols;
889
- indices.push(a, b, c, a, c, d);
890
- }
891
- }
1115
+ const clothPresentation = resolveClothPresentation(state, meshDetail);
1116
+ const clothState = ensureShowcaseClothState(state, meshDetail, clothPresentation);
892
1117
 
893
1118
  return {
894
- clothPlan,
895
- band,
896
- representation,
897
- continuity,
1119
+ clothPlan: clothPresentation.clothPlan,
1120
+ band: clothPresentation.band,
1121
+ representation: clothPresentation.representation,
1122
+ continuity: clothPresentation.continuity,
898
1123
  color: visuals.flagColor,
899
- positions,
900
- indices,
901
- grid: { rows, cols },
1124
+ positions: clothState.positions.map((point) => vec3(point.x, point.y, point.z)),
1125
+ indices: clothState.indices,
1126
+ grid: { rows: clothState.rows, cols: clothState.cols },
902
1127
  };
903
1128
  }
904
1129
 
@@ -990,6 +1215,64 @@ function sampleWave(state, x, z, time) {
990
1215
  );
991
1216
  }
992
1217
 
1218
+ function buildWaterMotionEffects(state) {
1219
+ const wakeTrails = [];
1220
+ const rippleRings = state.waveImpulses.map((impulse) => {
1221
+ const radius = impulse.radius + (1 - impulse.life) * 4.8;
1222
+ return Object.freeze({
1223
+ center: vec3(
1224
+ impulse.x,
1225
+ sampleWave(state, impulse.x, impulse.z, state.time) * 0.24 + 0.06,
1226
+ impulse.z
1227
+ ),
1228
+ radius,
1229
+ opacity: clamp(impulse.life * 0.28, 0.08, 0.3),
1230
+ });
1231
+ });
1232
+
1233
+ for (const ship of state.ships) {
1234
+ const speed = Math.hypot(ship.velocity.x, ship.velocity.z);
1235
+ if (speed <= 0.18) {
1236
+ continue;
1237
+ }
1238
+
1239
+ const direction = normalizeVec3(vec3(ship.velocity.x, 0, ship.velocity.z));
1240
+ const behind = scaleVec3(direction, -1);
1241
+ const lateral = vec3(-direction.z, 0, direction.x);
1242
+ const points = [];
1243
+ for (let sampleIndex = 0; sampleIndex < 6; sampleIndex += 1) {
1244
+ const along = 1 + sampleIndex * 1.45;
1245
+ const lateralOffset =
1246
+ Math.sin(state.time * 1.2 + sampleIndex * 0.8 + readVisualNumber(ship.wanderPhase, 0)) * 0.12;
1247
+ const worldPoint = addVec3(
1248
+ ship.position,
1249
+ addVec3(scaleVec3(behind, along), scaleVec3(lateral, lateralOffset))
1250
+ );
1251
+ points.push(
1252
+ Object.freeze({
1253
+ center: vec3(
1254
+ worldPoint.x,
1255
+ sampleWave(state, worldPoint.x, worldPoint.z, state.time) * 0.24 + 0.04,
1256
+ worldPoint.z
1257
+ ),
1258
+ width: 0.34 + sampleIndex * 0.13,
1259
+ })
1260
+ );
1261
+ }
1262
+ wakeTrails.push(
1263
+ Object.freeze({
1264
+ opacity: clamp(0.18 + speed * 0.09, 0.22, 0.46),
1265
+ points: Object.freeze(points),
1266
+ })
1267
+ );
1268
+ }
1269
+
1270
+ return Object.freeze({
1271
+ wakeTrails: Object.freeze(wakeTrails),
1272
+ rippleRings: Object.freeze(rippleRings),
1273
+ });
1274
+ }
1275
+
993
1276
  function buildWaterBands(state, fluidDetail, visuals) {
994
1277
  const fluidPlan = createFluidRepresentationPlan({
995
1278
  fluidBodyId: "harbor",
@@ -1168,6 +1451,7 @@ function createSceneState(options) {
1168
1451
  contactCount: 0,
1169
1452
  collisionCount: 0,
1170
1453
  collisionFlash: 0,
1454
+ clothState: null,
1171
1455
  physics: {
1172
1456
  profile: physicsProfile,
1173
1457
  plan: physicsPlan,
@@ -1902,27 +2186,78 @@ function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength
1902
2186
  });
1903
2187
  }
1904
2188
 
1905
- function renderGlowLight(
1906
- ctx,
1907
- point,
1908
- camera,
1909
- viewport,
1910
- coreColor,
1911
- glowColor,
1912
- glowScale,
1913
- reflectionStrength = 0,
1914
- state = null
1915
- ) {
1916
- const projected = projectPoint(point, camera, viewport);
2189
+ function collectSceneLightSources(state, visuals) {
2190
+ const directLights = [];
2191
+ const reflectionLights = [];
2192
+ const pushLight = (point, glowScale, reflectionStrength, coreColor, glowColor) => {
2193
+ directLights.push(
2194
+ Object.freeze({
2195
+ pass: "direct-glow",
2196
+ point,
2197
+ coreColor,
2198
+ glowColor,
2199
+ glowScale,
2200
+ })
2201
+ );
2202
+ if (reflectionStrength > 0) {
2203
+ reflectionLights.push(
2204
+ Object.freeze({
2205
+ pass: "water-reflection",
2206
+ point,
2207
+ coreColor,
2208
+ glowColor,
2209
+ glowScale,
2210
+ reflectionStrength,
2211
+ })
2212
+ );
2213
+ }
2214
+ };
2215
+
2216
+ for (const torch of HARBOR_TORCHES) {
2217
+ pushLight(
2218
+ vec3(torch.x, torch.y, torch.z),
2219
+ torch.glow,
2220
+ visuals.lanternReflectionStrength * 0.55,
2221
+ visuals.torchCore,
2222
+ visuals.torchGlow
2223
+ );
2224
+ }
2225
+
2226
+ for (const ship of state.ships) {
2227
+ const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
2228
+ const strength = readVisualNumber(ship.lanternStrength, 1);
2229
+ for (const lantern of lanterns) {
2230
+ const point = transformPoint(
2231
+ vec3(lantern.x, lantern.y, lantern.z),
2232
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
2233
+ );
2234
+ pushLight(
2235
+ point,
2236
+ lantern.glow * strength,
2237
+ visuals.lanternReflectionStrength,
2238
+ visuals.lanternCore,
2239
+ visuals.lanternGlow
2240
+ );
2241
+ }
2242
+ }
2243
+
2244
+ return Object.freeze({
2245
+ directLights: Object.freeze(directLights),
2246
+ reflectionLights: Object.freeze(reflectionLights),
2247
+ });
2248
+ }
2249
+
2250
+ function renderDirectLightGlow(ctx, source, camera, viewport) {
2251
+ const projected = projectPoint(source.point, camera, viewport);
1917
2252
  if (!projected) {
1918
2253
  return;
1919
2254
  }
1920
2255
 
1921
- const radius = clamp((1 / projected.depth) * 420 * glowScale, 4, 34);
2256
+ const radius = clamp((1 / projected.depth) * 420 * source.glowScale, 4, 34);
1922
2257
  const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
1923
- halo.addColorStop(0, colorToRgba(coreColor, 0.98));
1924
- halo.addColorStop(0.5, colorToRgba(glowColor, 0.42));
1925
- halo.addColorStop(1, colorToRgba(glowColor, 0));
2258
+ halo.addColorStop(0, colorToRgba(source.coreColor, 0.98));
2259
+ halo.addColorStop(0.5, colorToRgba(source.glowColor, 0.42));
2260
+ halo.addColorStop(1, colorToRgba(source.glowColor, 0));
1926
2261
  ctx.save();
1927
2262
  ctx.globalCompositeOperation = "screen";
1928
2263
  ctx.fillStyle = halo;
@@ -1931,82 +2266,134 @@ function renderGlowLight(
1931
2266
  ctx.fill();
1932
2267
  ctx.restore();
1933
2268
 
1934
- ctx.fillStyle = colorToRgba(coreColor, 0.98);
2269
+ ctx.fillStyle = colorToRgba(source.coreColor, 0.98);
1935
2270
  ctx.beginPath();
1936
2271
  ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
1937
2272
  ctx.fill();
2273
+ }
1938
2274
 
1939
- if (state && reflectionStrength > 0) {
1940
- const waterline = sampleWave(state, point.x, point.z, state.time) * 0.22;
1941
- const reflectedPoint = vec3(point.x, waterline - (point.y - waterline) * 0.58, point.z + 0.08);
1942
- const reflected = projectPoint(reflectedPoint, camera, viewport);
1943
- if (reflected) {
1944
- const reflectionRadius = radius * 0.72;
1945
- const glow = ctx.createRadialGradient(
1946
- reflected.x,
1947
- reflected.y,
1948
- reflectionRadius * 0.1,
1949
- reflected.x,
1950
- reflected.y,
1951
- reflectionRadius
1952
- );
1953
- glow.addColorStop(0, colorToRgba(coreColor, reflectionStrength * 0.34));
1954
- glow.addColorStop(1, colorToRgba(glowColor, 0));
1955
- ctx.save();
1956
- ctx.globalCompositeOperation = "screen";
1957
- ctx.fillStyle = glow;
2275
+ function renderWaterLightReflection(ctx, source, state, camera, viewport) {
2276
+ const projected = projectPoint(source.point, camera, viewport);
2277
+ if (!projected) {
2278
+ return;
2279
+ }
2280
+
2281
+ const radius = clamp((1 / projected.depth) * 420 * source.glowScale, 4, 34);
2282
+ const waterline = sampleWave(state, source.point.x, source.point.z, state.time) * 0.22;
2283
+ const reflectedPoint = vec3(
2284
+ source.point.x,
2285
+ waterline - (source.point.y - waterline) * 0.58,
2286
+ source.point.z + 0.08
2287
+ );
2288
+ const reflected = projectPoint(reflectedPoint, camera, viewport);
2289
+ if (!reflected) {
2290
+ return;
2291
+ }
2292
+
2293
+ const reflectionRadius = radius * 0.72;
2294
+ const glow = ctx.createRadialGradient(
2295
+ reflected.x,
2296
+ reflected.y,
2297
+ reflectionRadius * 0.1,
2298
+ reflected.x,
2299
+ reflected.y,
2300
+ reflectionRadius
2301
+ );
2302
+ glow.addColorStop(0, colorToRgba(source.coreColor, source.reflectionStrength * 0.34));
2303
+ glow.addColorStop(1, colorToRgba(source.glowColor, 0));
2304
+ ctx.save();
2305
+ ctx.globalCompositeOperation = "screen";
2306
+ ctx.fillStyle = glow;
2307
+ ctx.beginPath();
2308
+ ctx.ellipse(
2309
+ reflected.x,
2310
+ reflected.y,
2311
+ reflectionRadius * 0.34,
2312
+ reflectionRadius,
2313
+ 0,
2314
+ 0,
2315
+ Math.PI * 2
2316
+ );
2317
+ ctx.fill();
2318
+ ctx.restore();
2319
+ }
2320
+
2321
+ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
2322
+ ctx.save();
2323
+ ctx.globalCompositeOperation = "screen";
2324
+
2325
+ for (const wake of effects.wakeTrails) {
2326
+ const projected = wake.points
2327
+ .map((point) => ({
2328
+ projected: projectPoint(point.center, camera, viewport),
2329
+ width: point.width,
2330
+ }))
2331
+ .filter((entry) => entry.projected);
2332
+ if (projected.length < 2) {
2333
+ continue;
2334
+ }
2335
+
2336
+ const averageDepth =
2337
+ projected.reduce((total, entry) => total + entry.projected.depth, 0) / projected.length;
2338
+ const averageWidth =
2339
+ projected.reduce((total, entry) => total + entry.width, 0) / projected.length;
2340
+ const baseWidth = clamp((averageWidth / Math.max(0.25, averageDepth)) * 180, 1.6, 5.4);
2341
+ ctx.strokeStyle = `rgba(146, 194, 236, ${wake.opacity * 0.52})`;
2342
+ ctx.lineWidth = baseWidth * 1.9;
2343
+ ctx.lineCap = "round";
2344
+ ctx.lineJoin = "round";
2345
+ ctx.beginPath();
2346
+ ctx.moveTo(projected[0].projected.x, projected[0].projected.y);
2347
+ for (let index = 1; index < projected.length; index += 1) {
2348
+ ctx.lineTo(projected[index].projected.x, projected[index].projected.y);
2349
+ }
2350
+ ctx.stroke();
2351
+
2352
+ ctx.strokeStyle = `rgba(234, 247, 255, ${wake.opacity})`;
2353
+ ctx.lineWidth = baseWidth;
2354
+ ctx.lineCap = "round";
2355
+ ctx.lineJoin = "round";
2356
+ ctx.beginPath();
2357
+ ctx.moveTo(projected[0].projected.x, projected[0].projected.y);
2358
+ for (let index = 1; index < projected.length; index += 1) {
2359
+ ctx.lineTo(projected[index].projected.x, projected[index].projected.y);
2360
+ }
2361
+ ctx.stroke();
2362
+
2363
+ for (const entry of projected.slice(1, 5)) {
2364
+ ctx.fillStyle = `rgba(239, 248, 255, ${wake.opacity * 0.76})`;
1958
2365
  ctx.beginPath();
1959
2366
  ctx.ellipse(
1960
- reflected.x,
1961
- reflected.y,
1962
- reflectionRadius * 0.34,
1963
- reflectionRadius,
2367
+ entry.projected.x,
2368
+ entry.projected.y,
2369
+ baseWidth * 0.72,
2370
+ baseWidth * 0.44,
1964
2371
  0,
1965
2372
  0,
1966
2373
  Math.PI * 2
1967
2374
  );
1968
2375
  ctx.fill();
1969
- ctx.restore();
1970
2376
  }
1971
2377
  }
1972
- }
1973
2378
 
1974
- function renderShipLanterns(ctx, ship, state, camera, viewport, visuals) {
1975
- const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
1976
- const strength = readVisualNumber(ship.lanternStrength, 1);
1977
- for (const lantern of lanterns) {
1978
- const position = transformPoint(
1979
- vec3(lantern.x, lantern.y, lantern.z),
1980
- { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
1981
- );
1982
- renderGlowLight(
1983
- ctx,
1984
- position,
1985
- camera,
1986
- viewport,
1987
- visuals.lanternCore,
1988
- visuals.lanternGlow,
1989
- lantern.glow * strength,
1990
- visuals.lanternReflectionStrength,
1991
- state
1992
- );
1993
- }
1994
- }
2379
+ for (const ring of effects.rippleRings) {
2380
+ const center = projectPoint(ring.center, camera, viewport);
2381
+ const xAxis = projectPoint(addVec3(ring.center, vec3(ring.radius, 0, 0)), camera, viewport);
2382
+ const zAxis = projectPoint(addVec3(ring.center, vec3(0, 0, ring.radius)), camera, viewport);
2383
+ if (!center || !xAxis || !zAxis) {
2384
+ continue;
2385
+ }
1995
2386
 
1996
- function renderHarborTorches(ctx, state, camera, viewport, visuals) {
1997
- for (const torch of HARBOR_TORCHES) {
1998
- renderGlowLight(
1999
- ctx,
2000
- vec3(torch.x, torch.y, torch.z),
2001
- camera,
2002
- viewport,
2003
- visuals.torchCore,
2004
- visuals.torchGlow,
2005
- torch.glow,
2006
- visuals.lanternReflectionStrength * 0.55,
2007
- state
2008
- );
2387
+ const radiusX = Math.hypot(xAxis.x - center.x, xAxis.y - center.y);
2388
+ const radiusY = Math.hypot(zAxis.x - center.x, zAxis.y - center.y);
2389
+ ctx.strokeStyle = `rgba(216, 235, 255, ${ring.opacity})`;
2390
+ ctx.lineWidth = clamp((radiusX + radiusY) * 0.02, 1, 3.1);
2391
+ ctx.beginPath();
2392
+ ctx.ellipse(center.x, center.y, radiusX, radiusY, 0, 0, Math.PI * 2);
2393
+ ctx.stroke();
2009
2394
  }
2395
+
2396
+ ctx.restore();
2010
2397
  }
2011
2398
 
2012
2399
  function renderScene(ctx, canvas, state, shipModel, dom) {
@@ -2038,8 +2425,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
2038
2425
  visuals
2039
2426
  );
2040
2427
 
2041
- const triangles = [];
2042
- pushHarborGeometry(camera, viewport, triangles, visuals);
2428
+ const waterTriangles = [];
2429
+ const sceneTriangles = [];
2043
2430
  const water = buildWaterBands(
2044
2431
  state,
2045
2432
  state.fluidDetail.getSnapshot().currentLevel.config,
@@ -2056,7 +2443,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
2056
2443
  if (projected.some((value) => value === null)) {
2057
2444
  continue;
2058
2445
  }
2059
- triangles.push({
2446
+ waterTriangles.push({
2060
2447
  points: projected,
2061
2448
  depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
2062
2449
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
@@ -2067,6 +2454,10 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
2067
2454
  }
2068
2455
  }
2069
2456
 
2457
+ const waterMotionEffects = buildWaterMotionEffects(state);
2458
+ const lightSources = collectSceneLightSources(state, visuals);
2459
+
2460
+ pushHarborGeometry(camera, viewport, sceneTriangles, visuals);
2070
2461
  const cloth = buildClothSurface(
2071
2462
  state,
2072
2463
  state,
@@ -2082,7 +2473,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
2082
2473
  if (projected.some((value) => value === null)) {
2083
2474
  continue;
2084
2475
  }
2085
- triangles.push({
2476
+ sceneTriangles.push({
2086
2477
  points: projected,
2087
2478
  depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
2088
2479
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
@@ -2099,24 +2490,29 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
2099
2490
  ship.tint,
2100
2491
  camera,
2101
2492
  viewport,
2102
- triangles,
2493
+ sceneTriangles,
2103
2494
  nearLighting.rtParticipation.directShadows === "premium" ? 0.08 : 0.02
2104
2495
  );
2105
2496
  }
2106
2497
 
2498
+ drawTriangles(ctx, waterTriangles, lightDir, reflectionStrength, camera, shadowStrength);
2107
2499
  for (const ship of state.ships) {
2108
2500
  renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength);
2109
2501
  }
2110
2502
  renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
2111
-
2112
- drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength);
2503
+ for (const source of lightSources.reflectionLights) {
2504
+ renderWaterLightReflection(ctx, source, state, camera, viewport);
2505
+ }
2506
+ renderWaterMotionEffects(ctx, waterMotionEffects, camera, viewport);
2113
2507
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
2114
- renderHarborTorches(ctx, state, camera, viewport, visuals);
2508
+ drawTriangles(ctx, sceneTriangles, lightDir, reflectionStrength, camera, shadowStrength);
2115
2509
  renderFlagPole(ctx, camera, viewport);
2116
2510
  renderClothAccent(ctx, cloth, camera, viewport);
2511
+ for (const source of lightSources.directLights) {
2512
+ renderDirectLightGlow(ctx, source, camera, viewport);
2513
+ }
2117
2514
  for (const ship of state.ships) {
2118
2515
  renderShipRigging(ctx, ship, camera, viewport);
2119
- renderShipLanterns(ctx, ship, state, camera, viewport, visuals);
2120
2516
  }
2121
2517
  renderSprays(ctx, state.sprays, camera, viewport);
2122
2518
 
@@ -2194,6 +2590,21 @@ function updateSceneState(state, dt, shipModel) {
2194
2590
  updateShips(state, dt, shipModel);
2195
2591
  updateWaveImpulses(state, dt);
2196
2592
  updateSpray(state, dt);
2593
+ const clothPresentation = resolveClothPresentation(
2594
+ state,
2595
+ state.clothDetail.getSnapshot().currentLevel.config
2596
+ );
2597
+ const clothState = ensureShowcaseClothState(
2598
+ state,
2599
+ state.clothDetail.getSnapshot().currentLevel.config,
2600
+ clothPresentation
2601
+ );
2602
+ advanceShowcaseClothSimulationState(clothState, {
2603
+ dt,
2604
+ time: state.time,
2605
+ flagMotion: readVisualNumber(state.demoVisuals?.flagMotion, 0.92),
2606
+ waveInfluence: sampleWave(state, FLAG_LAYOUT.origin.x + FLAG_LAYOUT.width * 0.55, FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * 0.48, state.time),
2607
+ });
2197
2608
  updatePhysicsSnapshot(state, shipModel);
2198
2609
  }
2199
2610
 
@@ -2370,3 +2781,10 @@ function updatePhysicsSnapshot(state, shipModel) {
2370
2781
  },
2371
2782
  });
2372
2783
  }
2784
+
2785
+ export {
2786
+ advanceShowcaseClothSimulationState as __testOnlyAdvanceShowcaseClothSimulationState,
2787
+ buildWaterMotionEffects as __testOnlyBuildWaterMotionEffects,
2788
+ collectSceneLightSources as __testOnlyCollectSceneLightSources,
2789
+ createShowcaseClothSimulationState as __testOnlyCreateShowcaseClothSimulationState,
2790
+ };