@jgengine/shell 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/CombatPresentation.d.ts +5 -0
  3. package/dist/CombatPresentation.js +8 -0
  4. package/dist/GameHost.d.ts +27 -1
  5. package/dist/GameHost.js +83 -4
  6. package/dist/GamePlayer.d.ts +5 -0
  7. package/dist/GamePlayer.js +5 -0
  8. package/dist/GamePlayerShell.d.ts +5 -0
  9. package/dist/GamePlayerShell.js +63 -359
  10. package/dist/GameUiPreview.js +3 -0
  11. package/dist/Shell3dPresentation.d.ts +63 -0
  12. package/dist/Shell3dPresentation.js +302 -0
  13. package/dist/ShellChrome.d.ts +51 -0
  14. package/dist/ShellChrome.js +51 -0
  15. package/dist/ShellHudPresentation.d.ts +41 -0
  16. package/dist/ShellHudPresentation.js +22 -0
  17. package/dist/audio/audioEngine.d.ts +4 -0
  18. package/dist/audio/audioEngine.js +111 -26
  19. package/dist/audio/audioWire.d.ts +22 -4
  20. package/dist/audio/audioWire.js +46 -3
  21. package/dist/audio/loopParams.d.ts +20 -0
  22. package/dist/audio/loopParams.js +28 -0
  23. package/dist/camera/GameCameraRig.js +6 -1
  24. package/dist/camera/GameOrbitCamera.js +7 -1
  25. package/dist/camera/cameraCollision.d.ts +28 -0
  26. package/dist/camera/cameraCollision.js +34 -0
  27. package/dist/camera/cameraRigs.d.ts +3 -1
  28. package/dist/camera/cameraRigs.js +24 -10
  29. package/dist/camera/index.d.ts +1 -0
  30. package/dist/camera/index.js +1 -0
  31. package/dist/camera/rigMath.d.ts +27 -0
  32. package/dist/camera/rigMath.js +45 -0
  33. package/dist/defineGame.d.ts +15 -1
  34. package/dist/defineGame.js +26 -4
  35. package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
  36. package/dist/devtools/DevtoolsOverlay.js +28 -12
  37. package/dist/devtools/agentBridge.js +4 -0
  38. package/dist/devtools/perfDiagnose.js +1 -1
  39. package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
  40. package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
  41. package/dist/environment/EnvironmentScene.js +17 -3
  42. package/dist/environment/RoadJunctions.d.ts +12 -0
  43. package/dist/environment/RoadJunctions.js +69 -0
  44. package/dist/environment/RoadRibbons.d.ts +8 -2
  45. package/dist/environment/RoadRibbons.js +26 -8
  46. package/dist/environment/terrainGroundColor.d.ts +9 -0
  47. package/dist/environment/terrainGroundColor.js +22 -0
  48. package/dist/gameKit.d.ts +27 -0
  49. package/dist/gameKit.js +29 -0
  50. package/dist/map/MapMarkerBeacons.d.ts +3 -2
  51. package/dist/postfx/PostProcessing.d.ts +9 -1
  52. package/dist/postfx/PostProcessing.js +11 -4
  53. package/dist/presentationResolve.d.ts +39 -0
  54. package/dist/presentationResolve.js +53 -0
  55. package/dist/registry.d.ts +1 -0
  56. package/dist/render/PartMotion.d.ts +18 -0
  57. package/dist/render/PartMotion.js +91 -0
  58. package/dist/render/SceneModels.d.ts +9 -2
  59. package/dist/render/SceneModels.js +31 -180
  60. package/dist/render/assetBase.d.ts +17 -0
  61. package/dist/render/assetBase.js +35 -0
  62. package/dist/render/measureBounds.d.ts +34 -0
  63. package/dist/render/measureBounds.js +127 -0
  64. package/dist/render/modelLoad.js +4 -1
  65. package/dist/render/resolveModel.d.ts +15 -0
  66. package/dist/render/resolveModel.js +43 -2
  67. package/dist/render/useModelAnimation.d.ts +10 -0
  68. package/dist/render/useModelAnimation.js +198 -0
  69. package/dist/scatter/InstancedScatter.js +5 -0
  70. package/dist/scatter/scatterProxies.js +24 -0
  71. package/dist/scene/AuthoredScene.d.ts +3 -1
  72. package/dist/scene/AuthoredScene.js +7 -2
  73. package/dist/scene/CityRenderer.d.ts +2 -0
  74. package/dist/scene/CityRenderer.js +1031 -0
  75. package/dist/scene/GrassFieldRenderer.js +53 -5
  76. package/dist/scene/PoleLineRenderer.d.ts +2 -0
  77. package/dist/scene/PoleLineRenderer.js +287 -0
  78. package/dist/scene/SoilPatchRenderer.js +70 -7
  79. package/dist/scene/WaterRenderer.js +34 -10
  80. package/dist/scene/builtinSceneKindRenderers.js +4 -0
  81. package/dist/scene/cityWindowMaterial.d.ts +16 -0
  82. package/dist/scene/cityWindowMaterial.js +81 -0
  83. package/dist/scene/sceneKindRenderers.d.ts +2 -0
  84. package/dist/settings/appliedSettings.d.ts +2 -1
  85. package/dist/settings/appliedSettings.js +4 -2
  86. package/dist/structures/GeneratedBuilding.d.ts +4 -0
  87. package/dist/structures/GeneratedBuilding.js +17 -7
  88. package/dist/structures/TransformGizmo.d.ts +11 -1
  89. package/dist/structures/TransformGizmo.js +9 -5
  90. package/dist/terrain/GrassField.d.ts +11 -3
  91. package/dist/terrain/GrassField.js +16 -6
  92. package/dist/terrain/grassGeometry.d.ts +33 -1
  93. package/dist/terrain/grassGeometry.js +83 -21
  94. package/dist/terrain/grassMaterial.d.ts +18 -0
  95. package/dist/terrain/grassMaterial.js +66 -13
  96. package/dist/terrain/heightfieldGeometry.d.ts +34 -0
  97. package/dist/terrain/heightfieldGeometry.js +109 -0
  98. package/dist/terrain/index.d.ts +1 -0
  99. package/dist/terrain/index.js +1 -0
  100. package/dist/terrain/soilPatchMaterial.d.ts +15 -2
  101. package/dist/terrain/soilPatchMaterial.js +46 -8
  102. package/dist/terrain/terrainDetailMaterial.js +15 -2
  103. package/dist/water/Ocean.d.ts +7 -1
  104. package/dist/water/Ocean.js +16 -3
  105. package/dist/water/OceanConfig.d.ts +9 -0
  106. package/dist/water/OceanConfig.js +6 -0
  107. package/dist/water/OceanMaterial.d.ts +12 -0
  108. package/dist/water/OceanMaterial.js +9 -0
  109. package/dist/water/OceanShader.d.ts +2 -2
  110. package/dist/water/OceanShader.js +78 -10
  111. package/dist/world/PlaceScene.d.ts +11 -0
  112. package/dist/world/PlaceScene.js +27 -0
  113. package/dist/world/WorldHud.d.ts +3 -1
  114. package/dist/world/WorldHud.js +2 -2
  115. package/dist/world/WorldScene.js +32 -4
  116. package/dist/worldSky.d.ts +7 -1
  117. package/dist/worldSky.js +11 -1
  118. package/package.json +4 -4
  119. package/skills/game-design/SKILL.md +28 -0
  120. package/skills/game-design/agents/openai.yaml +4 -0
  121. package/skills/game-design/references/application-playbook.md +170 -0
  122. package/skills/game-design/references/field-guide.md +288 -0
  123. package/skills/jgengine/SKILL.md +8 -4
  124. package/skills/jgengine/api.md +182 -81
  125. package/skills/jgengine/capabilities.md +104 -2
  126. package/skills/jgengine/recipes/minimal-game.md +88 -0
  127. package/skills/jgengine-ui/SKILL.md +17 -10
  128. package/skills/jgengine-ui/api.md +316 -51
  129. package/skills/jgengine-ui/capabilities.md +135 -2
  130. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  131. package/skills/jgengine-ui/reference.md +59 -9
  132. package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
  133. package/skills/jgengine-world/SKILL.md +13 -0
  134. package/skills/jgengine-world/api.md +453 -21
  135. package/skills/jgengine-world/capabilities.md +112 -2
  136. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  137. package/skills/jgengine-world/reference.md +43 -4
  138. package/skills/level-design/SKILL.md +54 -0
  139. package/skills/level-design/agents/openai.yaml +4 -0
  140. package/skills/level-design/references/application-playbook.md +192 -0
  141. package/skills/level-design/references/field-guide.md +337 -0
  142. package/skills/level-design/references/live-editor-loop.md +179 -0
  143. package/dist/cartridge/runtime.d.ts +0 -15
  144. package/dist/cartridge/runtime.js +0 -556
  145. package/dist/cartridge/spec.d.ts +0 -188
  146. package/dist/cartridge/spec.js +0 -19
  147. package/dist/cartridge/validate.d.ts +0 -2
  148. package/dist/cartridge/validate.js +0 -78
  149. package/dist/cartridge.d.ts +0 -145
  150. package/dist/cartridge.js +0 -246
  151. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -6,6 +6,11 @@ export const DEFAULT_GRASS_WIND = {
6
6
  gustScale: 0.16,
7
7
  flutter: 0.08,
8
8
  };
9
+ /** Default camera-distance fade band: tufts start thinning at 35 m and are gone by 95 m. */
10
+ export const DEFAULT_GRASS_DISTANCE_FADE = {
11
+ start: 35,
12
+ end: 95,
13
+ };
9
14
  function normalizeWindDirection(direction) {
10
15
  const vector = new THREE.Vector2(direction[0], direction[1]);
11
16
  return vector.lengthSq() === 0 ? new THREE.Vector2(1, 0) : vector.normalize();
@@ -23,8 +28,23 @@ export function resolveGrassWind(wind) {
23
28
  };
24
29
  }
25
30
  /** @internal */
31
+ export function resolveGrassDistanceFade(fade) {
32
+ // start >= end disables the fade in the shader, so "off" is just an empty band.
33
+ if (fade === false)
34
+ return { start: 0, end: 0 };
35
+ return {
36
+ start: fade?.start ?? DEFAULT_GRASS_DISTANCE_FADE.start,
37
+ end: fade?.end ?? DEFAULT_GRASS_DISTANCE_FADE.end,
38
+ };
39
+ }
40
+ /** @internal */
26
41
  export function createGrassMaterial(options = {}) {
27
42
  const wind = resolveGrassWind(options.wind);
43
+ const fade = resolveGrassDistanceFade(options.distanceFade);
44
+ const colorBase = new THREE.Color(options.colorBase ?? "#2d431f");
45
+ const colorGround = options.colorGround === undefined
46
+ ? colorBase.clone().multiplyScalar(0.82)
47
+ : new THREE.Color(options.colorGround);
28
48
  const uniforms = {
29
49
  uTime: { value: 0 },
30
50
  uWindDirection: { value: normalizeWindDirection(wind.direction) },
@@ -32,9 +52,12 @@ export function createGrassMaterial(options = {}) {
32
52
  uWindSpeed: { value: wind.speed },
33
53
  uWindGustScale: { value: wind.gustScale },
34
54
  uWindFlutter: { value: wind.flutter },
35
- uColorBase: { value: new THREE.Color(options.colorBase ?? "#2d431f") },
55
+ uColorBase: { value: colorBase },
36
56
  uColorTip: { value: new THREE.Color(options.colorTip ?? "#a6cc58") },
57
+ uColorGround: { value: colorGround },
37
58
  uColorVariation: { value: options.colorVariation ?? 0.28 },
59
+ uDistanceFade: { value: new THREE.Vector2(fade.start, fade.end) },
60
+ uNormalLift: { value: THREE.MathUtils.clamp(options.normalLift ?? 0.6, 0, 1) },
38
61
  };
39
62
  const material = new THREE.MeshStandardMaterial({
40
63
  color: "#ffffff",
@@ -54,36 +77,61 @@ attribute float instanceWidth;
54
77
  attribute float instanceBend;
55
78
  attribute float instancePhase;
56
79
  attribute float instanceColorMix;
80
+ attribute vec2 bladeOffset;
81
+ attribute vec4 bladeVary;
82
+ attribute float bladeBendScale;
57
83
  uniform float uTime;
58
84
  uniform vec2 uWindDirection;
59
85
  uniform float uWindStrength;
60
86
  uniform float uWindSpeed;
61
87
  uniform float uWindGustScale;
62
88
  uniform float uWindFlutter;
89
+ uniform vec2 uDistanceFade;
90
+ uniform float uNormalLift;
63
91
  varying float vGrassBladeT;
64
92
  varying float vGrassColorMix;
93
+ varying float vGrassFade;
65
94
  vec3 jgGrassPosition;
66
95
  vec3 jgGrassNormal;
67
96
  void jgComputeGrassBlade() {
68
97
  float t = position.y;
69
98
  float side = position.x;
70
- float taper = (1.0 - t) * (0.72 + 0.28 * (1.0 - t));
71
- float width = instanceWidth * taper;
72
- float arc = instanceBend * t * t;
73
- vec3 localPosition = vec3(side * width, instanceHeight * t - arc * instanceHeight * 0.18, arc * instanceHeight);
74
- vec3 localNormal = normalize(vec3(0.0, -instanceBend * t, 1.0));
75
- float cy = cos(instanceYaw);
76
- float sy = sin(instanceYaw);
99
+ // Camera-distance thinning: each tuft draws a random threshold, so the field dissolves
100
+ // progressively across the fade band instead of ending at a hard curtain.
101
+ float camDist = distance((modelMatrix * vec4(instanceOffset, 1.0)).xyz, cameraPosition);
102
+ float fadeT = uDistanceFade.y > uDistanceFade.x
103
+ ? smoothstep(uDistanceFade.x, uDistanceFade.y, camDist)
104
+ : 0.0;
105
+ float threshold = fract(instanceColorMix * 61.7) * 0.85;
106
+ float keep = 1.0 - smoothstep(threshold, threshold + 0.15, fadeT);
107
+ float heightScale = instanceHeight * bladeVary.y * keep;
108
+ // Paddle taper: hold width through the mid-blade and only pinch near the tip — thin
109
+ // triangle blades leave the ground visible between tufts and read as stubble.
110
+ float taper = (1.0 - 0.3 * t) * (1.0 - smoothstep(0.5, 1.0, t) * 0.95);
111
+ // Surviving distant tufts widen to hold silhouette coverage as neighbors thin out.
112
+ float width = instanceWidth * bladeVary.z * taper * (1.0 + fadeT * 1.4);
113
+ float arc = instanceBend * bladeBendScale * t * t;
114
+ vec3 localPosition = vec3(side * width, heightScale * t - arc * heightScale * 0.18, arc * heightScale);
115
+ vec3 localNormal = normalize(vec3(0.0, -instanceBend * bladeBendScale * t, 1.0));
116
+ float bladeYaw = instanceYaw + bladeVary.x;
117
+ float cy = cos(bladeYaw);
118
+ float sy = sin(bladeYaw);
77
119
  vec3 yawedPosition = vec3(localPosition.x * cy + localPosition.z * sy, localPosition.y, -localPosition.x * sy + localPosition.z * cy);
78
120
  vec3 yawedNormal = normalize(vec3(localNormal.x * cy + localNormal.z * sy, localNormal.y, -localNormal.x * sy + localNormal.z * cy));
121
+ float ct = cos(instanceYaw);
122
+ float st = sin(instanceYaw);
123
+ vec3 tuftOffset = vec3(bladeOffset.x * ct + bladeOffset.y * st, 0.0, -bladeOffset.x * st + bladeOffset.y * ct);
79
124
  float gustPhase = dot(instanceOffset.xz, uWindDirection) * uWindGustScale + uTime * uWindSpeed + instancePhase;
80
125
  float gust = sin(gustPhase) * 0.7 + sin(gustPhase * 0.43 + 2.4) * 0.3;
81
- float flutter = sin(uTime * 7.5 + instancePhase * 2.7) * uWindFlutter;
82
- vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t;
83
- jgGrassPosition = instanceOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
84
- jgGrassNormal = yawedNormal;
126
+ float flutter = sin(uTime * 7.5 + instancePhase * 2.7 + bladeVary.w * 6.2832) * uWindFlutter;
127
+ vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t * (0.4 + 0.6 * bladeVary.y);
128
+ jgGrassPosition = instanceOffset + tuftOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
129
+ // Lifting normals toward straight-up lights the field like a continuous meadow surface —
130
+ // side-lit individual blade planes read as dark stubble, not turf.
131
+ jgGrassNormal = normalize(mix(yawedNormal, vec3(0.0, 1.0, 0.0), uNormalLift));
85
132
  vGrassBladeT = t;
86
133
  vGrassColorMix = instanceColorMix;
134
+ vGrassFade = fadeT;
87
135
  }
88
136
  `)
89
137
  .replace("#include <beginnormal_vertex>", "jgComputeGrassBlade();\nvec3 objectNormal = jgGrassNormal;")
@@ -93,15 +141,20 @@ void jgComputeGrassBlade() {
93
141
  #include <common>
94
142
  uniform vec3 uColorBase;
95
143
  uniform vec3 uColorTip;
144
+ uniform vec3 uColorGround;
96
145
  uniform float uColorVariation;
97
146
  varying float vGrassBladeT;
98
147
  varying float vGrassColorMix;
148
+ varying float vGrassFade;
99
149
  `)
100
150
  .replace("#include <color_fragment>", `
101
151
  #include <color_fragment>
102
152
  vec3 grassColor = mix(uColorBase, uColorTip, smoothstep(0.0, 1.0, vGrassBladeT));
103
153
  grassColor *= mix(1.0 - uColorVariation, 1.0 + uColorVariation, vGrassColorMix);
104
- grassColor *= mix(0.58, 1.0, smoothstep(0.05, 0.45, vGrassBladeT));
154
+ // Roots take the ground color so blades grow out of the terrain instead of floating on it,
155
+ // and distant blades converge back toward it so the fade band blends into the mottled ground.
156
+ grassColor = mix(uColorGround, grassColor, smoothstep(0.0, 0.55, vGrassBladeT));
157
+ grassColor = mix(grassColor, uColorGround * 1.04, vGrassFade * 0.6);
105
158
  diffuseColor.rgb = grassColor;
106
159
  `);
107
160
  };
@@ -0,0 +1,34 @@
1
+ import * as THREE from "three";
2
+ /** XZ rectangle in world units — matches the shape of a terraform snapshot's `bounds`. */
3
+ export interface HeightfieldRect {
4
+ minX: number;
5
+ minZ: number;
6
+ maxX: number;
7
+ maxZ: number;
8
+ }
9
+ /** Per-vertex color hook for {@link displaceHeightfieldGeometry}; write the tone into `out`. */
10
+ export type HeightfieldColorFn = (x: number, z: number, height: number, out: THREE.Color) => void;
11
+ /** Options for {@link displaceHeightfieldGeometry}. */
12
+ export interface HeightfieldDisplaceOptions {
13
+ /** World-space rect the plane spans (the plane is centered on it, XZ, +Y up). */
14
+ bounds: HeightfieldRect;
15
+ /**
16
+ * Dirty world-space region to re-sample; only the vertex window covering it (plus a one-vertex
17
+ * normal ring) updates. Omit or pass null for a full rebuild.
18
+ */
19
+ region?: HeightfieldRect | null;
20
+ /** When given, writes a `color` attribute (creating it on demand) via this per-vertex hook. */
21
+ color?: HeightfieldColorFn;
22
+ }
23
+ /**
24
+ * Re-samples a `PlaneGeometry(width, depth, segments, segments).rotateX(-π/2)` heightfield mesh
25
+ * from a `sampleHeight` field in place: vertex Y, optional vertex color, grid central-difference
26
+ * normals, and an analytically maintained bounding sphere. With a dirty `region`, the work is
27
+ * O(region vertices) — heights/colors update inside the covering vertex window, normals inside the
28
+ * window plus a one-vertex ring — never a whole-mesh `computeVertexNormals`/`computeBoundingSphere`
29
+ * pass, which is what keeps per-frame brush stamps inside the editor's frame budget. The bounding
30
+ * sphere derives from the fixed plane extents plus a running height range kept on
31
+ * `geometry.userData`; partial passes only expand it, a full pass resets it exactly.
32
+ * @capability heightfield-mesh-update in-place partial update of a displaced ground plane mesh
33
+ */
34
+ export declare function displaceHeightfieldGeometry(geometry: THREE.BufferGeometry, sampleHeight: (x: number, z: number) => number, options: HeightfieldDisplaceOptions): void;
@@ -0,0 +1,109 @@
1
+ import * as THREE from "three";
2
+ const EXTENT_KEY = "jgHeightfieldExtent";
3
+ /**
4
+ * Re-samples a `PlaneGeometry(width, depth, segments, segments).rotateX(-π/2)` heightfield mesh
5
+ * from a `sampleHeight` field in place: vertex Y, optional vertex color, grid central-difference
6
+ * normals, and an analytically maintained bounding sphere. With a dirty `region`, the work is
7
+ * O(region vertices) — heights/colors update inside the covering vertex window, normals inside the
8
+ * window plus a one-vertex ring — never a whole-mesh `computeVertexNormals`/`computeBoundingSphere`
9
+ * pass, which is what keeps per-frame brush stamps inside the editor's frame budget. The bounding
10
+ * sphere derives from the fixed plane extents plus a running height range kept on
11
+ * `geometry.userData`; partial passes only expand it, a full pass resets it exactly.
12
+ * @capability heightfield-mesh-update in-place partial update of a displaced ground plane mesh
13
+ */
14
+ export function displaceHeightfieldGeometry(geometry, sampleHeight, options) {
15
+ const position = geometry.attributes.position;
16
+ const normals = geometry.attributes.normal;
17
+ const { bounds } = options;
18
+ const cx = (bounds.minX + bounds.maxX) / 2;
19
+ const cz = (bounds.minZ + bounds.maxZ) / 2;
20
+ const width = Math.max(1, bounds.maxX - bounds.minX);
21
+ const depth = Math.max(1, bounds.maxZ - bounds.minZ);
22
+ // PlaneGeometry(w, d, s, s) rotated -π/2 about X is a (s+1)² row-major grid: x fastest ascending,
23
+ // z ascending per row (verified by heightfieldGeometry.test.ts against the three.js constructor).
24
+ const side = Math.round(Math.sqrt(position.count));
25
+ const segments = Math.max(1, side - 1);
26
+ const stepX = width / segments;
27
+ const stepZ = depth / segments;
28
+ let colors = geometry.attributes.color;
29
+ const wantColors = options.color !== undefined;
30
+ const colorsMissing = wantColors && (colors === undefined || colors.count !== position.count);
31
+ const region = options.region ?? null;
32
+ const full = region === null || colorsMissing;
33
+ if (colorsMissing) {
34
+ colors = new THREE.BufferAttribute(new Float32Array(position.count * 3), 3);
35
+ geometry.setAttribute("color", colors);
36
+ }
37
+ const clampCell = (value) => (value < 0 ? 0 : value > segments ? segments : value);
38
+ const col0 = full ? 0 : clampCell(Math.floor(((region.minX - bounds.minX) / width) * segments));
39
+ const col1 = full ? segments : clampCell(Math.ceil(((region.maxX - bounds.minX) / width) * segments));
40
+ const row0 = full ? 0 : clampCell(Math.floor(((region.minZ - bounds.minZ) / depth) * segments));
41
+ const row1 = full ? segments : clampCell(Math.ceil(((region.maxZ - bounds.minZ) / depth) * segments));
42
+ const tone = new THREE.Color();
43
+ let windowMin = Infinity;
44
+ let windowMax = -Infinity;
45
+ for (let row = row0; row <= row1; row += 1) {
46
+ const base = row * side;
47
+ for (let col = col0; col <= col1; col += 1) {
48
+ const index = base + col;
49
+ const x = position.getX(index) + cx;
50
+ const z = position.getZ(index) + cz;
51
+ const height = sampleHeight(x, z);
52
+ position.setY(index, height);
53
+ if (height < windowMin)
54
+ windowMin = height;
55
+ if (height > windowMax)
56
+ windowMax = height;
57
+ if (options.color !== undefined) {
58
+ options.color(x, z, height, tone);
59
+ colors.setXYZ(index, tone.r, tone.g, tone.b);
60
+ }
61
+ }
62
+ }
63
+ // Grid central-difference normals over the window plus a one-vertex ring — a height change moves
64
+ // its neighbors' normals too. Matches smooth shading without a whole-mesh triangle pass.
65
+ const nc0 = Math.max(0, col0 - 1);
66
+ const nc1 = Math.min(segments, col1 + 1);
67
+ const nr0 = Math.max(0, row0 - 1);
68
+ const nr1 = Math.min(segments, row1 + 1);
69
+ for (let row = nr0; row <= nr1; row += 1) {
70
+ const rowD = Math.max(0, row - 1);
71
+ const rowU = Math.min(segments, row + 1);
72
+ for (let col = nc0; col <= nc1; col += 1) {
73
+ const colL = Math.max(0, col - 1);
74
+ const colR = Math.min(segments, col + 1);
75
+ const hL = position.getY(row * side + colL);
76
+ const hR = position.getY(row * side + colR);
77
+ const hD = position.getY(rowD * side + col);
78
+ const hU = position.getY(rowU * side + col);
79
+ const nx = -(hR - hL) / Math.max(1e-6, (colR - colL) * stepX);
80
+ const nz = -(hU - hD) / Math.max(1e-6, (rowU - rowD) * stepZ);
81
+ const inv = 1 / Math.sqrt(nx * nx + 1 + nz * nz);
82
+ normals.setXYZ(row * side + col, nx * inv, inv, nz * inv);
83
+ }
84
+ }
85
+ const extent = geometry.userData[EXTENT_KEY] ?? {
86
+ minY: Infinity,
87
+ maxY: -Infinity,
88
+ };
89
+ if (full) {
90
+ extent.minY = windowMin;
91
+ extent.maxY = windowMax;
92
+ }
93
+ else {
94
+ extent.minY = Math.min(extent.minY, windowMin);
95
+ extent.maxY = Math.max(extent.maxY, windowMax);
96
+ }
97
+ geometry.userData[EXTENT_KEY] = extent;
98
+ const hasExtent = extent.minY <= extent.maxY;
99
+ const midY = hasExtent ? (extent.minY + extent.maxY) / 2 : 0;
100
+ const halfY = hasExtent ? (extent.maxY - extent.minY) / 2 : 0;
101
+ if (geometry.boundingSphere === null)
102
+ geometry.boundingSphere = new THREE.Sphere();
103
+ geometry.boundingSphere.center.set(0, midY, 0);
104
+ geometry.boundingSphere.radius = Math.sqrt((width / 2) ** 2 + (depth / 2) ** 2 + halfY ** 2);
105
+ position.needsUpdate = true;
106
+ if (colors !== undefined && wantColors)
107
+ colors.needsUpdate = true;
108
+ normals.needsUpdate = true;
109
+ }
@@ -2,6 +2,7 @@ export { CarvedTerrain, type CarvedTerrainProps } from "./CarvedTerrain.js";
2
2
  export { GrassField, type GrassFieldProps } from "./GrassField.js";
3
3
  export { ProceduralGround, type ProceduralGroundProps } from "./ProceduralGround.js";
4
4
  export { EditableGround, type EditableGroundProps } from "./EditableGround.js";
5
+ export { displaceHeightfieldGeometry, type HeightfieldColorFn, type HeightfieldDisplaceOptions, type HeightfieldRect, } from "./heightfieldGeometry.js";
5
6
  export { TerraformBrushCursor, type TerraformBrushCursorProps } from "./TerraformBrushCursor.js";
6
7
  export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
7
8
  export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
@@ -2,6 +2,7 @@ export { CarvedTerrain } from "./CarvedTerrain.js";
2
2
  export { GrassField } from "./GrassField.js";
3
3
  export { ProceduralGround } from "./ProceduralGround.js";
4
4
  export { EditableGround } from "./EditableGround.js";
5
+ export { displaceHeightfieldGeometry, } from "./heightfieldGeometry.js";
5
6
  export { TerraformBrushCursor } from "./TerraformBrushCursor.js";
6
7
  export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, } from "./grassGeometry.js";
7
8
  export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./grassMaterial.js";
@@ -1,10 +1,23 @@
1
1
  import * as THREE from "three";
2
2
  import type { SoilRules } from "@jgengine/core/world/soilKind";
3
+ /** Optional noise-eroded edge-fade config for {@link createSoilPatchMaterial} — the patch dissolves into the ground at its border. */
4
+ export interface SoilPatchEdge {
5
+ /** Patch center on XZ, world units — with `halfSize`, drives the noise-eroded edge fade. */
6
+ center: readonly [number, number];
7
+ /** Patch half extents on XZ, world units. */
8
+ halfSize: readonly [number, number];
9
+ /** Fade band width in meters from the patch border inward. Default 1.6. */
10
+ fade?: number;
11
+ /** Terrain color under the patch: the border blends its *color* toward the ground it meets, not just its alpha. Defaults to the soil base color (color blend off). */
12
+ groundColor?: string;
13
+ }
3
14
  /**
4
15
  * A `MeshStandardMaterial` for one authored `soil` volume: a base dirt tint with a Worley crack-line
5
16
  * network and an FBM moss-patch overlay injected via `onBeforeCompile`, seeded from `rules.seed`. Full
6
17
  * PBR — lit, shadowed, fogged — like the terrain detail material it borrows its noise machinery from.
7
- * A game never calls this directly — author a soil patch via the editor's `soil` studio kind.
18
+ * Pass `edge` to add a noise-eroded alpha fade at the patch border (the draped scene-kind renderer
19
+ * does) so the patch blends into the surrounding ground. A game never calls this directly — author a
20
+ * soil patch via the editor's `soil` studio kind.
8
21
  * @internal
9
22
  */
10
- export declare function createSoilPatchMaterial(rules: SoilRules): THREE.MeshStandardMaterial;
23
+ export declare function createSoilPatchMaterial(rules: SoilRules, edge?: SoilPatchEdge): THREE.MeshStandardMaterial;
@@ -53,6 +53,10 @@ vec2 jgSoilWorleyF1F2(vec2 p){
53
53
  }
54
54
  return vec2(f1, f2);
55
55
  }
56
+ float jgSoilCrackMask(vec2 p, float scale){
57
+ vec2 f = jgSoilWorleyF1F2(p / scale);
58
+ return 1.0 - smoothstep(0.0, 0.065, f.y - f.x);
59
+ }
56
60
  `;
57
61
  function hashSeed(text) {
58
62
  let h = 2166136261 >>> 0;
@@ -66,15 +70,18 @@ function hashSeed(text) {
66
70
  * A `MeshStandardMaterial` for one authored `soil` volume: a base dirt tint with a Worley crack-line
67
71
  * network and an FBM moss-patch overlay injected via `onBeforeCompile`, seeded from `rules.seed`. Full
68
72
  * PBR — lit, shadowed, fogged — like the terrain detail material it borrows its noise machinery from.
69
- * A game never calls this directly — author a soil patch via the editor's `soil` studio kind.
73
+ * Pass `edge` to add a noise-eroded alpha fade at the patch border (the draped scene-kind renderer
74
+ * does) so the patch blends into the surrounding ground. A game never calls this directly — author a
75
+ * soil patch via the editor's `soil` studio kind.
70
76
  * @internal
71
77
  */
72
- export function createSoilPatchMaterial(rules) {
78
+ export function createSoilPatchMaterial(rules, edge) {
73
79
  const seedOffset = hashSeed(rules.seed);
74
80
  const material = new THREE.MeshStandardMaterial({
75
81
  color: rules.baseColor,
76
82
  roughness: 0.92,
77
83
  metalness: 0,
84
+ ...(edge === undefined ? {} : { transparent: true, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 }),
78
85
  });
79
86
  material.onBeforeCompile = (shader) => {
80
87
  shader.uniforms.uSoilCrackColor = { value: new THREE.Color(rules.crackColor) };
@@ -83,6 +90,12 @@ export function createSoilPatchMaterial(rules) {
83
90
  shader.uniforms.uSoilMossColor = { value: new THREE.Color(rules.mossColor) };
84
91
  shader.uniforms.uSoilMossCoverage = { value: rules.mossCoverage };
85
92
  shader.uniforms.uSoilSeed = { value: seedOffset };
93
+ if (edge !== undefined) {
94
+ shader.uniforms.uSoilCenter = { value: new THREE.Vector2(edge.center[0], edge.center[1]) };
95
+ shader.uniforms.uSoilHalfSize = { value: new THREE.Vector2(edge.halfSize[0], edge.halfSize[1]) };
96
+ shader.uniforms.uSoilEdgeFade = { value: edge.fade ?? 1.6 };
97
+ shader.uniforms.uSoilGroundColor = { value: new THREE.Color(edge.groundColor ?? rules.baseColor) };
98
+ }
86
99
  shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
87
100
  varying vec3 vJgSoilWorldPos;`).replace("#include <begin_vertex>", `#include <begin_vertex>
88
101
  vJgSoilWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;`);
@@ -94,16 +107,41 @@ uniform float uSoilCrackIntensity;
94
107
  uniform vec3 uSoilMossColor;
95
108
  uniform float uSoilMossCoverage;
96
109
  uniform float uSoilSeed;
97
- ${SOIL_NOISE_GLSL}`).replace("#include <color_fragment>", `#include <color_fragment>
110
+ ${edge === undefined ? "" : "uniform vec2 uSoilCenter;\nuniform vec2 uSoilHalfSize;\nuniform float uSoilEdgeFade;\nuniform vec3 uSoilGroundColor;\n"}${SOIL_NOISE_GLSL}`).replace("#include <color_fragment>", `#include <color_fragment>
98
111
  vec2 jgSoilP = vJgSoilWorldPos.xz + vec2(uSoilSeed * 0.37, uSoilSeed * 0.71);
99
- vec2 jgSoilF1F2 = jgSoilWorleyF1F2(jgSoilP / uSoilCrackScale);
100
- float jgSoilCrack = 1.0 - smoothstep(0.0, 0.1, jgSoilF1F2.y - jgSoilF1F2.x);
112
+ float jgSoilCrack = jgSoilCrackMask(jgSoilP, uSoilCrackScale);
101
113
  vec3 jgSoilCol = mix(diffuseColor.rgb, uSoilCrackColor, jgSoilCrack * uSoilCrackIntensity);
114
+ // Crack floors go darker still — paired with the relief normals they read as recessed, not painted.
115
+ jgSoilCol *= 1.0 - jgSoilCrack * uSoilCrackIntensity * 0.4;
116
+ // Sun-baked plate mottling so the dirt between cracks isn't one flat swatch.
117
+ jgSoilCol *= mix(0.92, 1.08, jgSoilFbm(jgSoilP / 1.1));
102
118
  float jgSoilMossN = jgSoilFbm(jgSoilP / 2.4);
103
119
  float jgSoilMossMask = smoothstep(1.0 - uSoilMossCoverage, 1.0 - uSoilMossCoverage + 0.18, jgSoilMossN);
104
- jgSoilCol = mix(jgSoilCol, uSoilMossColor, jgSoilMossMask);
105
- diffuseColor.rgb = jgSoilCol;`);
120
+ // Fine clump break-up: moss creeps in speckled colonies (and pools into the cracks), not airbrushed blobs.
121
+ jgSoilMossMask *= smoothstep(0.3, 0.75, jgSoilFbm(jgSoilP / 0.55));
122
+ jgSoilMossMask = min(1.0, jgSoilMossMask + jgSoilMossMask * jgSoilCrack * 0.8);
123
+ jgSoilCol = mix(jgSoilCol, uSoilMossColor * mix(0.85, 1.1, jgSoilFbm(jgSoilP / 0.9)), jgSoilMossMask);${edge === undefined
124
+ ? "\ndiffuseColor.rgb = jgSoilCol;"
125
+ : `
126
+ // Border: color walks toward the terrain tint across the fade band (alpha alone left a decal
127
+ // seam — the dirt must *become* the ground it meets), then a noise-eaten alpha finishes it.
128
+ vec2 jgSoilEdgeD = uSoilHalfSize - abs(vJgSoilWorldPos.xz - uSoilCenter);
129
+ float jgSoilEdgeDist = min(jgSoilEdgeD.x, jgSoilEdgeD.y);
130
+ float jgSoilEdgeNoise = jgSoilFbm(jgSoilP * 0.9) * uSoilEdgeFade;
131
+ float jgSoilInside = clamp((jgSoilEdgeDist - jgSoilEdgeNoise) / max(uSoilEdgeFade, 0.001) + 0.55, 0.0, 1.0);
132
+ jgSoilCol = mix(uSoilGroundColor, jgSoilCol, smoothstep(0.0, 0.85, jgSoilInside));
133
+ diffuseColor.rgb = jgSoilCol;
134
+ diffuseColor.a *= clamp(jgSoilInside * 1.5, 0.0, 1.0);`}`).replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
135
+ // Crack relief: tilt normals along the Worley gradient so crack walls catch and lose the light
136
+ // (finite differences over the same field that colors the cracks — they self-shadow in sync).
137
+ vec2 jgSoilNp = vJgSoilWorldPos.xz + vec2(uSoilSeed * 0.37, uSoilSeed * 0.71);
138
+ float jgSoilNe = 0.05;
139
+ vec2 jgSoilGrad = vec2(
140
+ jgSoilCrackMask(jgSoilNp + vec2(jgSoilNe, 0.0), uSoilCrackScale) - jgSoilCrackMask(jgSoilNp - vec2(jgSoilNe, 0.0), uSoilCrackScale),
141
+ jgSoilCrackMask(jgSoilNp + vec2(0.0, jgSoilNe), uSoilCrackScale) - jgSoilCrackMask(jgSoilNp - vec2(0.0, jgSoilNe), uSoilCrackScale)
142
+ ) / (2.0 * jgSoilNe);
143
+ normal = normalize(normal + vec3(jgSoilGrad.x, 0.0, jgSoilGrad.y) * uSoilCrackIntensity * 0.2);`);
106
144
  };
107
- material.customProgramCacheKey = () => `jgengine-soil-${rules.crackScale}-${rules.crackIntensity}-${rules.mossCoverage}-${seedOffset}`;
145
+ material.customProgramCacheKey = () => `jgengine-soil-${rules.crackScale}-${rules.crackIntensity}-${rules.mossCoverage}-${seedOffset}-${edge === undefined ? "solid" : "faded"}`;
108
146
  return material;
109
147
  }
@@ -129,11 +129,24 @@ ${NOISE_GLSL}`)
129
129
  vec2 jgWp = vJgWorldPos.xz;
130
130
  float jgFine = jgFbm(jgWp / uDetailScale);
131
131
  float jgMacro = jgFbm(jgWp / uMacroScale);
132
+ float jgPatch = jgFbm(jgWp / (uMacroScale * 0.35) + vec2(37.2, 11.7));
132
133
  float jgSlope = 1.0 - clamp(vJgWorldNormal.y, 0.0, 1.0);
133
134
  float jgH = vJgWorldPos.y;
135
+ // Blade-scale flecks fade with view distance so close ground reads as turf without
136
+ // shimmering into noise on the horizon.
137
+ float jgViewDist = length(vJgWorldPos - cameraPosition);
138
+ float jgNear = 1.0 - smoothstep(14.0, 70.0, jgViewDist);
139
+ float jgFleck = jgVNoise(jgWp / max(0.05, uDetailScale * 0.024));
140
+ float jgFleckMid = jgVNoise(jgWp / max(0.12, uDetailScale * 0.07) + vec2(51.3, 7.9));
134
141
  vec3 jgBase = diffuseColor.rgb;
135
- jgBase *= mix(1.0 - 0.22 * uStrength, 1.0 + 0.18 * uStrength, jgFine);
136
- jgBase = mix(jgBase, jgBase * vec3(0.86, 0.9, 0.82), jgMacro * 0.25 * uStrength);
142
+ jgBase *= mix(1.0 - 0.2 * uStrength, 1.0 + 0.16 * uStrength, jgFine);
143
+ // Meadow patchwork: hue-shifted sun-dried sweeps and cooler lush pockets, not just darker spots.
144
+ jgBase = mix(jgBase, jgBase * vec3(1.14, 1.05, 0.76), smoothstep(0.5, 0.8, jgMacro) * 0.55 * uStrength);
145
+ jgBase = mix(jgBase, jgBase * vec3(0.78, 1.0, 0.86), smoothstep(0.52, 0.85, jgPatch) * 0.5 * uStrength);
146
+ // Rolling large-scale light/dark sweeps keep far hills from flattening into one tone.
147
+ jgBase *= 1.0 + (jgFbm(jgWp / (uMacroScale * 2.3) + vec2(9.1, 73.4)) - 0.5) * 0.22 * uStrength;
148
+ jgBase *= 1.0 + (jgFleck - 0.5) * 0.48 * uStrength * jgNear;
149
+ jgBase *= 1.0 + (jgFleckMid - 0.5) * 0.34 * uStrength;
137
150
  vec3 jgRock = uRockColor * mix(0.72, 1.15, jgFbm(jgWp / (uDetailScale * 1.7)));
138
151
  float jgRockW = smoothstep(uRockSlopeStart, uRockSlopeStart + 0.18, jgSlope) * uStrength;
139
152
  vec3 jgCol = mix(jgBase, jgRock, jgRockW);
@@ -2,5 +2,11 @@ import { type ThreeElements } from "@react-three/fiber";
2
2
  import { type OceanConfig } from "./OceanConfig.js";
3
3
  export interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
4
4
  config?: OceanConfig;
5
+ /**
6
+ * Water-column depth (m) under a point of the sheet, in the sheet's local XZ. When provided,
7
+ * the shader shades shallow→deep by real depth, foams and turns transparent at the shore, and
8
+ * damps waves as the bed shoals. Omit for an open ocean (everywhere-deep behavior).
9
+ */
10
+ depthAt?: (x: number, z: number) => number;
5
11
  }
6
- export declare function Ocean({ config, ...meshProps }: OceanProps): import("react").JSX.Element;
12
+ export declare function Ocean({ config, depthAt, ...meshProps }: OceanProps): import("react").JSX.Element;
@@ -4,16 +4,29 @@ import { useEffect, useMemo, useRef } from "react";
4
4
  import * as THREE from "three";
5
5
  import { createOceanConfig } from "./OceanConfig.js";
6
6
  import { createOceanMaterial, syncOceanMaterial } from "./OceanMaterial.js";
7
- function createOceanGeometry(config) {
7
+ /** aDepth when no sampler is given: everywhere-deep, matching the pre-depth shader look. */
8
+ const OCEAN_DEFAULT_DEPTH = 1000;
9
+ function createOceanGeometry(config, depthAt) {
8
10
  const geometry = new THREE.PlaneGeometry(config.size, config.depth, config.resolution, config.resolution);
9
11
  geometry.rotateX(-Math.PI / 2);
12
+ const positions = geometry.getAttribute("position");
13
+ const depths = new Float32Array(positions.count);
14
+ if (depthAt === undefined) {
15
+ depths.fill(OCEAN_DEFAULT_DEPTH);
16
+ }
17
+ else {
18
+ for (let i = 0; i < positions.count; i += 1) {
19
+ depths[i] = Math.max(0, depthAt(positions.getX(i), positions.getZ(i)));
20
+ }
21
+ }
22
+ geometry.setAttribute("aDepth", new THREE.BufferAttribute(depths, 1));
10
23
  geometry.computeBoundingSphere();
11
24
  return geometry;
12
25
  }
13
- export function Ocean({ config, ...meshProps }) {
26
+ export function Ocean({ config, depthAt, ...meshProps }) {
14
27
  const resolved = useMemo(() => createOceanConfig(config), [config]);
15
28
  const elapsedRef = useRef(0);
16
- const geometry = useMemo(() => createOceanGeometry(resolved), [resolved]);
29
+ const geometry = useMemo(() => createOceanGeometry(resolved, depthAt), [resolved, depthAt]);
17
30
  const material = useMemo(() => createOceanMaterial(resolved), [resolved]);
18
31
  useEffect(() => {
19
32
  return () => geometry.dispose();
@@ -21,12 +21,18 @@ export interface OceanColorConfig {
21
21
  opacity?: number;
22
22
  fresnelStrength?: number;
23
23
  horizonBlend?: number;
24
+ /** Water depth (m) at which the body color reaches the full `deep` tone. Default 2.6. */
25
+ depthRange?: number;
26
+ /** Sun-glint sparkle intensity, 0 disables. Default 0.35. */
27
+ sparkle?: number;
24
28
  }
25
29
  export interface OceanFoamConfig {
26
30
  crestThreshold?: number;
27
31
  softness?: number;
28
32
  intensity?: number;
29
33
  coverage?: number;
34
+ /** Shoreline foam lick width (m of water depth it covers). Default 0.5. */
35
+ shoreWidth?: number;
30
36
  }
31
37
  export interface OceanConfig {
32
38
  quality?: OceanQualityPreset;
@@ -59,12 +65,15 @@ export interface ResolvedOceanColorConfig {
59
65
  opacity: number;
60
66
  fresnelStrength: number;
61
67
  horizonBlend: number;
68
+ depthRange: number;
69
+ sparkle: number;
62
70
  }
63
71
  export interface ResolvedOceanFoamConfig {
64
72
  crestThreshold: number;
65
73
  softness: number;
66
74
  intensity: number;
67
75
  coverage: number;
76
+ shoreWidth: number;
68
77
  }
69
78
  export interface ResolvedOceanWaveConfig {
70
79
  amplitude: number;
@@ -28,12 +28,15 @@ export const DEFAULT_OCEAN_CONFIG = {
28
28
  opacity: 0.88,
29
29
  fresnelStrength: 0.7,
30
30
  horizonBlend: 0.38,
31
+ depthRange: 2.6,
32
+ sparkle: 0.35,
31
33
  },
32
34
  foam: {
33
35
  crestThreshold: 0.64,
34
36
  softness: 0.22,
35
37
  intensity: 0.8,
36
38
  coverage: 0.45,
39
+ shoreWidth: 0.5,
37
40
  },
38
41
  waves: [],
39
42
  };
@@ -111,6 +114,8 @@ export function createOceanConfig(patch = {}) {
111
114
  opacity: clamp(finiteOr(patch.color?.opacity, DEFAULT_OCEAN_CONFIG.color.opacity), 0, 1),
112
115
  fresnelStrength: Math.max(0, finiteOr(patch.color?.fresnelStrength, DEFAULT_OCEAN_CONFIG.color.fresnelStrength)),
113
116
  horizonBlend: clamp(finiteOr(patch.color?.horizonBlend, DEFAULT_OCEAN_CONFIG.color.horizonBlend), 0, 1),
117
+ depthRange: Math.max(0.05, finiteOr(patch.color?.depthRange, DEFAULT_OCEAN_CONFIG.color.depthRange)),
118
+ sparkle: Math.max(0, finiteOr(patch.color?.sparkle, DEFAULT_OCEAN_CONFIG.color.sparkle)),
114
119
  },
115
120
  foam: {
116
121
  ...DEFAULT_OCEAN_CONFIG.foam,
@@ -119,6 +124,7 @@ export function createOceanConfig(patch = {}) {
119
124
  softness: clamp(finiteOr(patch.foam?.softness, DEFAULT_OCEAN_CONFIG.foam.softness), 0.001, 1),
120
125
  intensity: Math.max(0, finiteOr(patch.foam?.intensity, DEFAULT_OCEAN_CONFIG.foam.intensity)),
121
126
  coverage: clamp(finiteOr(patch.foam?.coverage, DEFAULT_OCEAN_CONFIG.foam.coverage), 0, 1),
127
+ shoreWidth: Math.max(0, finiteOr(patch.foam?.shoreWidth, DEFAULT_OCEAN_CONFIG.foam.shoreWidth)),
122
128
  },
123
129
  };
124
130
  const defaultWaves = createDefaultWaves(withoutWaves);
@@ -46,6 +46,18 @@ export interface OceanMaterialUniforms {
46
46
  uFoamCoverage: {
47
47
  value: number;
48
48
  };
49
+ uDepthRange: {
50
+ value: number;
51
+ };
52
+ uShoreFoamWidth: {
53
+ value: number;
54
+ };
55
+ uSparkle: {
56
+ value: number;
57
+ };
58
+ uSunDirection: {
59
+ value: THREE.Vector3;
60
+ };
49
61
  }
50
62
  export type OceanShaderMaterial = THREE.ShaderMaterial & {
51
63
  uniforms: OceanMaterialUniforms;
@@ -1,6 +1,8 @@
1
1
  import * as THREE from "three";
2
2
  import { buildOceanWaveUniforms } from "./OceanConfig.js";
3
3
  import { oceanFragmentShader, oceanVertexShader } from "./OceanShader.js";
4
+ /** Matches the day-sky sun heading closely enough for stylized glints — not a lighting contract. */
5
+ const OCEAN_SUN_DIRECTION = new THREE.Vector3(0.45, 0.78, 0.35).normalize();
4
6
  export function createOceanMaterial(config) {
5
7
  const material = new THREE.ShaderMaterial({
6
8
  vertexShader: oceanVertexShader,
@@ -24,6 +26,10 @@ export function createOceanMaterial(config) {
24
26
  uFoamSoftness: { value: config.foam.softness },
25
27
  uFoamIntensity: { value: config.foam.intensity },
26
28
  uFoamCoverage: { value: config.foam.coverage },
29
+ uDepthRange: { value: config.color.depthRange },
30
+ uShoreFoamWidth: { value: config.foam.shoreWidth },
31
+ uSparkle: { value: config.color.sparkle },
32
+ uSunDirection: { value: OCEAN_SUN_DIRECTION.clone() },
27
33
  },
28
34
  });
29
35
  syncOceanMaterial(material, config, 0);
@@ -46,6 +52,9 @@ export function syncOceanMaterial(material, config, elapsedSeconds) {
46
52
  material.uniforms.uFoamSoftness.value = config.foam.softness;
47
53
  material.uniforms.uFoamIntensity.value = config.foam.intensity;
48
54
  material.uniforms.uFoamCoverage.value = config.foam.coverage;
55
+ material.uniforms.uDepthRange.value = config.color.depthRange;
56
+ material.uniforms.uShoreFoamWidth.value = config.foam.shoreWidth;
57
+ material.uniforms.uSparkle.value = config.color.sparkle;
49
58
  material.transparent = config.color.opacity < 1;
50
59
  material.depthWrite = config.color.opacity >= 0.98;
51
60
  }