@jgengine/shell 0.21.0 → 0.23.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 (102) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/dist/GamePlayerShell.js +18 -3
  3. package/dist/GameUiPreview.js +2 -1
  4. package/dist/Shell3dPresentation.js +13 -8
  5. package/dist/ShellChrome.d.ts +1 -1
  6. package/dist/ShellChrome.js +4 -2
  7. package/dist/ShellHudPresentation.js +2 -1
  8. package/dist/ShellNotices.d.ts +26 -0
  9. package/dist/ShellNotices.js +51 -0
  10. package/dist/audio/AudioComponents.d.ts +3 -1
  11. package/dist/audio/AudioComponents.js +5 -2
  12. package/dist/audio/listenerAnchor.d.ts +15 -0
  13. package/dist/audio/listenerAnchor.js +17 -0
  14. package/dist/audio/playableAudio.d.ts +9 -0
  15. package/dist/audio/playableAudio.js +11 -0
  16. package/dist/commandSink.d.ts +1 -1
  17. package/dist/commandSink.js +8 -3
  18. package/dist/defineGame.js +4 -2
  19. package/dist/devtools/DevtoolsOverlay.js +1 -0
  20. package/dist/devtools/agentBridge.d.ts +21 -0
  21. package/dist/devtools/agentBridge.js +95 -3
  22. package/dist/devtools/devtoolsReports.d.ts +32 -0
  23. package/dist/devtools/devtoolsReports.js +26 -0
  24. package/dist/devtools/simStepper.d.ts +21 -0
  25. package/dist/devtools/simStepper.js +37 -0
  26. package/dist/drivers/FrameDriver.js +26 -14
  27. package/dist/drivers/HudOnlyDriver.js +16 -5
  28. package/dist/environment/EnvironmentScene.js +24 -9
  29. package/dist/environment/RoadJunctions.d.ts +3 -1
  30. package/dist/environment/RoadJunctions.js +9 -2
  31. package/dist/environment/RoadRibbons.d.ts +3 -1
  32. package/dist/environment/RoadRibbons.js +16 -3
  33. package/dist/environment/terrainGroundColor.d.ts +5 -4
  34. package/dist/environment/terrainGroundColor.js +20 -9
  35. package/dist/materialOverride.js +4 -2
  36. package/dist/playSurfaceFocus.d.ts +16 -0
  37. package/dist/playSurfaceFocus.js +42 -0
  38. package/dist/postfx/PostProcessing.d.ts +3 -1
  39. package/dist/postfx/PostProcessing.js +14 -6
  40. package/dist/registry.d.ts +5 -1
  41. package/dist/render/CascadedShadows.d.ts +2 -0
  42. package/dist/render/CascadedShadows.js +4 -0
  43. package/dist/render/EntityPreview.js +1 -1
  44. package/dist/render/EnvironmentLighting.d.ts +26 -7
  45. package/dist/render/EnvironmentLighting.js +40 -27
  46. package/dist/render/SceneLighting.js +2 -2
  47. package/dist/render/modelLoad.d.ts +7 -0
  48. package/dist/render/modelLoad.js +13 -0
  49. package/dist/render/modelRender.d.ts +1 -0
  50. package/dist/render/modelRender.js +3 -0
  51. package/dist/render/modelSurface.d.ts +39 -0
  52. package/dist/render/modelSurface.js +206 -0
  53. package/dist/render/useModelInstance.d.ts +1 -1
  54. package/dist/render/useModelInstance.js +2 -2
  55. package/dist/scatter/scatterModels.js +3 -1
  56. package/dist/scene/AuthoredScene.d.ts +11 -3
  57. package/dist/scene/AuthoredScene.js +17 -9
  58. package/dist/scene/CityRenderer.js +86 -24
  59. package/dist/scene/GrassFieldRenderer.js +20 -2
  60. package/dist/scene/cityJunctionSurface.d.ts +7 -0
  61. package/dist/scene/cityJunctionSurface.js +34 -0
  62. package/dist/settings/settingsController.js +1 -1
  63. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  64. package/dist/terrain/CarvedTerrain.js +2 -2
  65. package/dist/terrain/GrassField.d.ts +2 -1
  66. package/dist/terrain/GrassField.js +3 -2
  67. package/dist/terrain/grassGeometry.d.ts +3 -0
  68. package/dist/terrain/grassGeometry.js +2 -1
  69. package/dist/terrain/index.d.ts +5 -0
  70. package/dist/terrain/index.js +5 -0
  71. package/dist/terrain/surfaceRibbonMaterial.d.ts +62 -0
  72. package/dist/terrain/surfaceRibbonMaterial.js +227 -0
  73. package/dist/terrain/terrainDetailMaterial.d.ts +33 -10
  74. package/dist/terrain/terrainDetailMaterial.js +149 -63
  75. package/dist/terrain/terrainMath.d.ts +2 -0
  76. package/dist/terrain/terrainMath.js +2 -0
  77. package/dist/terrain/terrainPaintColor.d.ts +20 -0
  78. package/dist/terrain/terrainPaintColor.js +92 -0
  79. package/dist/terrain/terrainQuality.d.ts +5 -0
  80. package/dist/terrain/terrainQuality.js +7 -0
  81. package/dist/terrain/terrainSurfaceShader.d.ts +16 -0
  82. package/dist/terrain/terrainSurfaceShader.js +266 -0
  83. package/dist/terrain/terrainSurfaces.d.ts +59 -0
  84. package/dist/terrain/terrainSurfaces.js +199 -0
  85. package/dist/world/StaticObjectModels.d.ts +1 -1
  86. package/dist/world/StaticObjectModels.js +1 -1
  87. package/dist/world/WorldScene.js +1 -1
  88. package/dist/world/staticObjectBatches.d.ts +5 -1
  89. package/dist/world/staticObjectBatches.js +9 -5
  90. package/dist/world/worldBarSamples.d.ts +13 -1
  91. package/dist/world/worldBarSamples.js +40 -8
  92. package/package.json +5 -5
  93. package/skills/game-design/SKILL.md +2 -0
  94. package/skills/jgengine/SKILL.md +1 -0
  95. package/skills/jgengine/api.md +18 -18
  96. package/skills/jgengine-ui/SKILL.md +4 -0
  97. package/skills/jgengine-ui/api.md +242 -34
  98. package/skills/jgengine-ui/capabilities.md +56 -0
  99. package/skills/jgengine-ui/reference-primitives.md +4 -4
  100. package/skills/jgengine-world/api.md +28 -15
  101. package/skills/jgengine-world/capabilities.md +4 -0
  102. package/skills/jgengine-world/reference.md +30 -0
@@ -1,49 +1,58 @@
1
1
  import * as THREE from "three";
2
- const NOISE_GLSL = /* glsl */ `
3
- uint jgHashUint(uvec2 v){
4
- uint x = v.x * 1664525u + v.y * 1013904223u;
5
- x = (x ^ (x >> 16u)) * 2246822519u;
6
- x = (x ^ (x >> 13u)) * 3266489917u;
7
- x = x ^ (x >> 16u);
8
- return x;
9
- }
10
- float jgHash(vec2 p){
11
- ivec2 i = ivec2(floor(p));
12
- return float(jgHashUint(uvec2(i))) * (1.0 / 4294967296.0);
13
- }
14
- float jgVNoise(vec2 p){
15
- vec2 i = floor(p); vec2 f = fract(p);
16
- vec2 u = f * f * (3.0 - 2.0 * f);
17
- float a = jgHash(i);
18
- float b = jgHash(i + vec2(1.0, 0.0));
19
- float c = jgHash(i + vec2(0.0, 1.0));
20
- float d = jgHash(i + vec2(1.0, 1.0));
21
- return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
22
- }
23
- float jgFbm(vec2 p){
24
- float v = 0.0; float amp = 0.5;
25
- for (int i = 0; i < 4; i++){ v += amp * jgVNoise(p); p *= 2.0; amp *= 0.5; }
26
- return v;
2
+ import { NOISE_GLSL, PAINT_PARS_GLSL, SURFACE_BUMP_NORMAL_GLSL, SURFACE_FUNCTIONS_GLSL } from "./terrainSurfaceShader.js";
3
+ const ROCK_CLASS_BIT = 1 << 4;
4
+ /** Compile every surface response, for an editor viewport whose paint can change to any surface. */
5
+ export const ALL_TERRAIN_SURFACE_CLASSES = 0x1ff;
6
+ // Slot 0 is the unpainted base, 1–8 the paint layers, 9 slope rock.
7
+ const SLOT_GLSL = /* glsl */ `
8
+ JgSurf jgSlot(int slot, vec3 baseTone, vec3 wp){
9
+ vec3 jgFlatC = baseTone;
10
+ JgSurf s;
11
+ if (slot == 9) {
12
+ jgFlatC = uRockColor;
13
+ s = jgSurface(4, uRockColor, roughness, 1.0, wp);
14
+ }
15
+ #ifdef JG_TERRAIN_PAINT
16
+ else if (slot > 0) {
17
+ int i = slot - 1;
18
+ jgFlatC = uLayerColor[i];
19
+ s = jgSurface(int(uLayerClass[i] + 0.5), uLayerColor[i], uLayerRough[i], uLayerScale[i], wp);
20
+ }
21
+ #endif
22
+ else {
23
+ s = jgSurface(0, baseTone, roughness, 1.0, wp);
24
+ }
25
+ s.albedo = mix(jgFlatC, s.albedo, uSurface);
26
+ s.height *= uSurface;
27
+ s.bump *= uSurface;
28
+ return s;
27
29
  }
28
30
  `;
29
31
  /**
30
- * A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex
31
- * colour as the base ground and blends procedural, noise-broken rock (by slope),
32
- * sand (by waterline), and snow (by height) over it — textured-reading terrain
33
- * with no image assets. Full PBR: lit, shadowed, and fogged like any standard
34
- * material, so it composes with the post-processing chain.
32
+ * A `MeshStandardMaterial` that renders terrain as ground rather than flat colour. Each painted
33
+ * layer draws its surface type's procedural response (grass clumps and blades, dirt clods and
34
+ * pebbles, gravel stones, cracked rock, rippled sand, wet mud, snow, road grain) in the layer's
35
+ * authored hue, with albedo, roughness and a micro-normal. Layers meet along noise-warped,
36
+ * height-aware edges instead of paint-cell blobs; steep slopes turn to rock; macro sweeps break up
37
+ * tiling. Unpainted ground keeps the biome-tinted vertex colour with procedural detail, plus the
38
+ * waterline sand and altitude snow bands. Full PBR: lit, shadowed and fogged like any standard material.
35
39
  *
36
40
  * When `detail.material` is set, pass the matching loaded `textures` (color/normal/roughness/ao):
37
41
  * they tile by world position at `detail.material.repeat` world units per tile and blend over the
38
- * procedural result at `detail.material.strength` — the real-texture seam layers onto the existing
39
- * look instead of replacing it.
40
- * @internal
41
- */
42
- export function createTerrainDetailMaterial(detail, textures) {
42
+ * procedural result at `detail.material.strength`.
43
+ * @internal
44
+ */
45
+ export function createTerrainDetailMaterial(detail, textures, options = {}) {
43
46
  const hasTexture = textures !== undefined && detail.material !== undefined;
44
47
  const repeat = detail.material?.repeat ?? 4;
45
48
  const strength = detail.material?.strength ?? 1;
46
49
  const tint = detail.material?.tint ?? 1;
50
+ const paint = options.paint;
51
+ const low = options.quality === "low";
52
+ const contour = { value: options.contour ?? 0 };
53
+ // Rock is always compiled for slope rock; each extra class costs shader time even where unused on
54
+ // GPUs that run both sides of a branch.
55
+ const classMask = (options.classMask ?? paint?.classMask ?? 0) | ROCK_CLASS_BIT;
47
56
  const uniforms = {
48
57
  uRockColor: { value: new THREE.Color(detail.rockColor) },
49
58
  uSandColor: { value: new THREE.Color(detail.sandColor) },
@@ -56,6 +65,9 @@ export function createTerrainDetailMaterial(detail, textures) {
56
65
  uDetailScale: { value: Math.max(0.5, detail.detailScale) },
57
66
  uMacroScale: { value: Math.max(0.5, detail.macroScale) },
58
67
  uStrength: { value: detail.strength },
68
+ uSurface: { value: detail.surfaceStrength },
69
+ uContour: contour,
70
+ ...(paint === undefined ? {} : paint.uniforms),
59
71
  ...(hasTexture
60
72
  ? {
61
73
  uMatColor: { value: textures.color },
@@ -74,6 +86,11 @@ export function createTerrainDetailMaterial(detail, textures) {
74
86
  metalness: 0,
75
87
  vertexColors: true,
76
88
  });
89
+ material.defines = {
90
+ ...(paint === undefined ? {} : { JG_TERRAIN_PAINT: "" }),
91
+ ...(low ? { JG_SURFACE_LOW: "" } : {}),
92
+ JG_SURFACE_CLASSES: String(classMask),
93
+ };
77
94
  material.onBeforeCompile = (shader) => {
78
95
  Object.assign(shader.uniforms, uniforms);
79
96
  shader.vertexShader = shader.vertexShader
@@ -109,12 +126,15 @@ jgCol = mix(jgCol, jgMatColor, uMatStrength * uMatTint);`
109
126
  const materialRoughnessGlsl = hasTexture && textures.roughness !== undefined
110
127
  ? `
111
128
  float jgMatRough = texture2D(uMatRoughness, jgMatUv).g;
112
- roughnessFactor = mix(roughnessFactor, jgMatRough, uMatStrength);`
129
+ // The map varies the authored roughness rather than replacing it: scanned ground roughness sits
130
+ // near 0.45, which turned matte terrain glossy enough to throw a wet sheen across dry sand.
131
+ roughnessFactor = clamp(roughnessFactor + (jgMatRough - 0.5) * uMatStrength * 0.6, 0.0, 1.0);`
113
132
  : "";
133
+ // The map's tilt is world-space (U along +X, V along +Z on flat ground); `normal` is view-space.
114
134
  const materialNormalGlsl = hasTexture
115
135
  ? `
116
136
  vec2 jgMatNormalXy = texture2D(uMatNormal, jgMatUv).rg * 2.0 - 1.0;
117
- normal = normalize(normal + vec3(jgMatNormalXy.x, 0.0, jgMatNormalXy.y) * uMatStrength * 0.6);`
137
+ normal = normalize(normal + (viewMatrix * vec4(jgMatNormalXy.x, 0.0, jgMatNormalXy.y, 0.0)).xyz * uMatStrength * 0.6);`
118
138
  : "";
119
139
  const materialAoGlsl = hasTexture && textures.ao !== undefined
120
140
  ? `
@@ -136,48 +156,114 @@ uniform float uWaterLevel;
136
156
  uniform float uDetailScale;
137
157
  uniform float uMacroScale;
138
158
  uniform float uStrength;
159
+ uniform float uSurface;
160
+ uniform float uContour;
139
161
  ${materialUniformsGlsl}
140
- ${NOISE_GLSL}`)
162
+ ${NOISE_GLSL}
163
+ ${SURFACE_FUNCTIONS_GLSL}
164
+ #ifdef JG_TERRAIN_PAINT
165
+ ${PAINT_PARS_GLSL}
166
+ #endif
167
+ ${SLOT_GLSL}`)
141
168
  .replace("#include <color_fragment>", `#include <color_fragment>
142
169
  vec2 jgWp = vJgWorldPos.xz;
143
- float jgFine = jgFbm(jgWp / uDetailScale);
170
+ jgFootprint = max(length(dFdx(vJgWorldPos)), length(dFdy(vJgWorldPos)));
144
171
  float jgMacro = jgFbm(jgWp / uMacroScale);
145
- float jgPatch = jgFbm(jgWp / (uMacroScale * 0.35) + vec2(37.2, 11.7));
146
172
  float jgSlope = 1.0 - clamp(vJgWorldNormal.y, 0.0, 1.0);
147
173
  float jgH = vJgWorldPos.y;
148
- // Blade-scale flecks fade with view distance so close ground reads as turf without
149
- // shimmering into noise on the horizon.
150
- float jgViewDist = length(vJgWorldPos - cameraPosition);
151
- float jgNear = 1.0 - smoothstep(14.0, 70.0, jgViewDist);
152
- float jgFleck = jgVNoise(jgWp / max(0.05, uDetailScale * 0.024));
153
- float jgFleckMid = jgVNoise(jgWp / max(0.12, uDetailScale * 0.07) + vec2(51.3, 7.9));
174
+ float jgRockW = smoothstep(uRockSlopeStart, uRockSlopeStart + 0.18, jgSlope) * mix(uStrength, 1.0, uSurface);
175
+
176
+ // Only the two strongest of base, eight paint slots and slope rock are shaded; a third
177
+ // contribution is negligible once the coverage is smoothed, and shading every slot cost 10x.
178
+ vec4 jgPa = vec4(0.0);
179
+ vec4 jgPb = vec4(0.0);
180
+ float jgPaintTotal = 0.0;
181
+ #ifdef JG_TERRAIN_PAINT
182
+ vec2 jgPaintTexUv = jgPaintUv(jgWp);
183
+ jgPa = texture2D(uPaintA, jgPaintTexUv);
184
+ jgPb = texture2D(uPaintB, jgPaintTexUv);
185
+ jgPaintTotal = dot(jgPa, vec4(1.0)) + dot(jgPb, vec4(1.0));
186
+ #endif
187
+ float jgPaintNorm = (1.0 - jgRockW) / max(1.0, jgPaintTotal);
188
+ jgPaintTotal = min(jgPaintTotal, 1.0);
189
+ int jgI1 = 0;
190
+ int jgI2 = 0;
191
+ float jgW1 = (1.0 - jgPaintTotal) * (1.0 - jgRockW);
192
+ float jgW2 = 0.0;
193
+ #define JG_PICK(idx, w) { float jgPk = (w); if (jgPk > jgW1) { jgI2 = jgI1; jgW2 = jgW1; jgI1 = idx; jgW1 = jgPk; } else if (jgPk > jgW2) { jgI2 = idx; jgW2 = jgPk; } }
194
+ #ifdef JG_TERRAIN_PAINT
195
+ JG_PICK(1, jgPa.r * jgPaintNorm)
196
+ JG_PICK(2, jgPa.g * jgPaintNorm)
197
+ JG_PICK(3, jgPa.b * jgPaintNorm)
198
+ JG_PICK(4, jgPa.a * jgPaintNorm)
199
+ JG_PICK(5, jgPb.r * jgPaintNorm)
200
+ JG_PICK(6, jgPb.g * jgPaintNorm)
201
+ JG_PICK(7, jgPb.b * jgPaintNorm)
202
+ JG_PICK(8, jgPb.a * jgPaintNorm)
203
+ #endif
204
+ JG_PICK(9, jgRockW)
205
+ bool jgTwo = jgW2 > 0.004;
206
+
154
207
  vec3 jgBase = diffuseColor.rgb;
155
- jgBase *= mix(1.0 - 0.2 * uStrength, 1.0 + 0.16 * uStrength, jgFine);
156
- // Biome patchwork: hue-shifted dry sweeps and cooler wet pockets, tinted per-world via detail.sweeps.
157
- jgBase = mix(jgBase, jgBase * uSweepDry, smoothstep(0.5, 0.8, jgMacro) * 0.55 * uStrength);
158
- jgBase = mix(jgBase, jgBase * uSweepWet, smoothstep(0.52, 0.85, jgPatch) * 0.5 * uStrength);
159
- // Rolling large-scale light/dark sweeps keep far hills from flattening into one tone.
160
- jgBase *= 1.0 + (jgFbm(jgWp / (uMacroScale * 2.3) + vec2(9.1, 73.4)) - 0.5) * 0.22 * uStrength;
161
- jgBase *= 1.0 + (jgFleck - 0.5) * 0.48 * uStrength * jgNear;
162
- jgBase *= 1.0 + (jgFleckMid - 0.5) * 0.34 * uStrength;
163
- vec3 jgRock = uRockColor * mix(0.72, 1.15, jgFbm(jgWp / (uDetailScale * 1.7)));
164
- float jgRockW = smoothstep(uRockSlopeStart, uRockSlopeStart + 0.18, jgSlope) * uStrength;
165
- vec3 jgCol = mix(jgBase, jgRock, jgRockW);
166
- vec3 jgSand = uSandColor * mix(0.9, 1.08, jgFine);
208
+ if (jgI1 == 0 || (jgTwo && jgI2 == 0)) {
209
+ float jgFine = jgFbm(jgWp / uDetailScale);
210
+ float jgPatch = jgFbm(jgWp / (uMacroScale * 0.35) + vec2(37.2, 11.7));
211
+ float jgFleck = jgVNoise(jgWp / max(0.05, uDetailScale * 0.024));
212
+ float jgFleckMid = jgVNoise(jgWp / max(0.12, uDetailScale * 0.07) + vec2(51.3, 7.9));
213
+ jgBase *= mix(1.0 - 0.2 * uStrength, 1.0 + 0.16 * uStrength, jgFine);
214
+ // Biome patchwork: hue-shifted dry sweeps and cooler wet pockets, tinted per-world via detail.sweeps.
215
+ jgBase = mix(jgBase, jgBase * uSweepDry, smoothstep(0.5, 0.8, jgMacro) * 0.55 * uStrength);
216
+ jgBase = mix(jgBase, jgBase * uSweepWet, smoothstep(0.52, 0.85, jgPatch) * 0.5 * uStrength);
217
+ jgBase *= 1.0 + (jgFbm(jgWp / (uMacroScale * 2.3) + vec2(9.1, 73.4)) - 0.5) * 0.22 * uStrength;
218
+ jgBase *= 1.0 + (jgFleck - 0.5) * 0.48 * uStrength * jgBand(1.0 / max(0.05, uDetailScale * 0.024));
219
+ jgBase *= 1.0 + (jgFleckMid - 0.5) * 0.34 * uStrength * jgBand(1.0 / max(0.12, uDetailScale * 0.07));
220
+ }
221
+
222
+ JgSurf jgS1 = jgSlot(jgI1, jgBase, vJgWorldPos);
223
+ JgSurf jgS2 = jgS1;
224
+ float jgA1 = 1.0;
225
+ float jgA2 = 0.0;
226
+ if (jgTwo) {
227
+ jgS2 = jgSlot(jgI2, jgBase, vJgWorldPos);
228
+ float jgB1 = jgW1 / (jgW1 + jgW2);
229
+ float jgB2 = 1.0 - jgB1;
230
+ // Height-aware blend: along an edge the layer standing taller (gravel stones, grass clumps) wins
231
+ // first, so transitions read as one material breaking through another, not a cross-fade.
232
+ float jgH1 = jgB1 + jgS1.height * 0.5;
233
+ float jgH2 = jgB2 + jgS2.height * 0.5;
234
+ float jgM = max(jgH1, jgH2) - 0.28;
235
+ jgA1 = mix(jgB1, max(0.0, jgH1 - jgM), uSurface);
236
+ jgA2 = mix(jgB2, max(0.0, jgH2 - jgM), uSurface);
237
+ float jgT = max(jgA1 + jgA2, 1e-4);
238
+ jgA1 /= jgT;
239
+ jgA2 /= jgT;
240
+ }
241
+ vec3 jgCol = jgS1.albedo * jgA1 + jgS2.albedo * jgA2;
242
+ float jgRough = jgS1.rough * jgA1 + jgS2.rough * jgA2;
243
+ float jgBumpH = jgS1.bump * jgA1 + jgS2.bump * jgA2;
244
+
245
+ // Large-scale light and sun-dried sweeps over painted ground, so a field painted one hue never tiles flat.
246
+ float jgPaintCover = jgPaintTotal * (1.0 - jgRockW) * uSurface;
247
+ jgCol *= 1.0 + (jgFbm2(jgWp / (uMacroScale * 1.3) + vec2(3.7, 61.2)) - 0.5) * 0.3 * jgPaintCover;
248
+ jgCol = mix(jgCol, jgCol * uSweepDry, smoothstep(0.52, 0.78, jgMacro) * 0.2 * jgPaintCover);
249
+
167
250
  float jgSandW = smoothstep(uWaterLevel + 2.4, uWaterLevel - 0.4, jgH) * (1.0 - jgRockW) * uStrength;
168
- jgCol = mix(jgCol, jgSand, jgSandW);
169
- vec3 jgSnow = uSnowColor * mix(0.94, 1.04, jgFine);
251
+ jgCol = mix(jgCol, uSandColor * (0.9 + 0.2 * jgMacro), jgSandW);
170
252
  float jgSnowW = smoothstep(uSnowHeight, uSnowHeight + 8.0, jgH) * (1.0 - jgRockW * 0.6) * uStrength;
171
- jgCol = mix(jgCol, jgSnow, jgSnowW);
253
+ jgCol = mix(jgCol, uSnowColor * (0.94 + 0.1 * jgMacro), jgSnowW);
172
254
  ${materialColorGlsl}
255
+ float jgContour = 0.5 + 0.5 * cos(jgH / 1.5 * 6.2831853);
256
+ jgCol *= (1.0 - uContour * 0.4 * (1.0 - jgContour)) * (1.0 + clamp(jgH * 0.03, -0.12, 0.22) * uContour);
173
257
  diffuseColor.rgb = jgCol;`)
174
258
  .replace("#include <roughnessmap_fragment>", `#include <roughnessmap_fragment>
259
+ roughnessFactor = clamp(jgRough, 0.04, 1.0);
175
260
  ${materialRoughnessGlsl}`)
176
261
  .replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
262
+ ${SURFACE_BUMP_NORMAL_GLSL}
177
263
  ${materialNormalGlsl}`)
178
264
  .replace("#include <aomap_fragment>", `#include <aomap_fragment>
179
265
  ${materialAoGlsl}`);
180
266
  };
181
- material.customProgramCacheKey = () => `jgengine-terrain-detail-${detail.rockSlopeStart}-${detail.snowHeight}-${hasTexture ? "tex" : "flat"}-${hasTexture && textures.roughness !== undefined ? "rough" : "scalar"}-${hasTexture && textures.ao !== undefined ? "ao" : "unoccluded"}`;
182
- return { material };
267
+ material.customProgramCacheKey = () => `jgengine-terrain-detail-${hasTexture ? "tex" : "flat"}-${hasTexture && textures.roughness !== undefined ? "rough" : "scalar"}-${hasTexture && textures.ao !== undefined ? "ao" : "unoccluded"}-${paint === undefined ? "bare" : "paint"}-${low ? "low" : "full"}-${classMask}`;
268
+ return { material, contour };
183
269
  }
@@ -82,6 +82,8 @@ export interface FieldGroundOptions {
82
82
  paletteAt?: TerrainPaletteSampler;
83
83
  /** Slope/noise surface shading over the height lerp. Omit for the default terrain look; pass `{ slopeStrength: 0, mottleStrength: 0 }` for a flat gradient. */
84
84
  surface?: TerrainSurfaceColorOptions;
85
+ /** Authored paint overrides descriptor shading at painted vertices. */
86
+ paintColorAt?: (x: number, z: number, out: THREE.Color) => boolean;
85
87
  }
86
88
  /**
87
89
  * Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
@@ -106,6 +106,7 @@ export function createFieldGroundGeometry(field, options = {}) {
106
106
  heightRange: options.heightRange,
107
107
  paletteAt: options.paletteAt,
108
108
  surface: options.surface,
109
+ paintColorAt: options.paintColorAt,
109
110
  });
110
111
  }
111
112
  function buildGroundGeometry(size, segments, sampler, opts) {
@@ -162,6 +163,7 @@ function buildGroundGeometry(size, segments, sampler, opts) {
162
163
  slope01 = grad / (grad + 1);
163
164
  }
164
165
  applyTerrainSurfaceShading(color, x, z, slope01, surface);
166
+ opts.paintColorAt?.(x, z, color);
165
167
  colorValues[colorIndex] = color.r;
166
168
  colorValues[colorIndex + 1] = color.g;
167
169
  colorValues[colorIndex + 2] = color.b;
@@ -0,0 +1,20 @@
1
+ import * as THREE from "three";
2
+ import type { EditableTerrain } from "@jgengine/core/world/terraform";
3
+ /**
4
+ * Live authored paint channels needed to sample a terrain's material stack without its heightfield.
5
+ * With the grid fields present, paint is filtered across neighbouring cells instead of snapping to each cell.
6
+ */
7
+ export type TerrainPaintSource = Pick<EditableTerrain, "layers" | "weightsAt" | "surfaceAt"> & Partial<Pick<EditableTerrain, "gridBounds" | "cols" | "rows">>;
8
+ /**
9
+ * Blends authored paint over the base ground color already in `out`; returns false (leaving `out`
10
+ * untouched) where no painted cell contributes.
11
+ */
12
+ export type TerrainPaintColorSampler = (x: number, z: number, out: THREE.Color) => boolean;
13
+ /**
14
+ * Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground.
15
+ * Paint lives on a coarse cell grid; when the source exposes that grid, each sample smoothly blends
16
+ * the four surrounding cell centres (unpainted cells contribute the caller's base color in `out`),
17
+ * so painted paths and clearings get soft edges instead of grid-aligned stair steps.
18
+ * @capability terrain-paint-color sample authored terrain surfaces and blended layers for shared ground rendering
19
+ */
20
+ export declare function createTerrainPaintColorSampler(terrain: TerrainPaintSource): TerrainPaintColorSampler;
@@ -0,0 +1,92 @@
1
+ import * as THREE from "three";
2
+ import { TERRAIN_MATERIAL_COLORS } from "@jgengine/core/editor/terrainMaterials";
3
+ const smooth = (t) => t * t * (3 - 2 * t);
4
+ /**
5
+ * Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground.
6
+ * Paint lives on a coarse cell grid; when the source exposes that grid, each sample smoothly blends
7
+ * the four surrounding cell centres (unpainted cells contribute the caller's base color in `out`),
8
+ * so painted paths and clearings get soft edges instead of grid-aligned stair steps.
9
+ * @capability terrain-paint-color sample authored terrain surfaces and blended layers for shared ground rendering
10
+ */
11
+ export function createTerrainPaintColorSampler(terrain) {
12
+ const layers = terrain.layers;
13
+ const colors = layers.map(layer => new THREE.Color(layer.tint ?? TERRAIN_MATERIAL_COLORS[layer.surface] ?? "#7c7f86"));
14
+ const defaults = new Map(Object.entries(TERRAIN_MATERIAL_COLORS).map(([id, color]) => [id, new THREE.Color(color)]));
15
+ const cellColor = (x, z, out) => {
16
+ const weights = terrain.weightsAt(x, z);
17
+ if (layers.length > 0 && weights.length === layers.length) {
18
+ let total = 0, r = 0, g = 0, b = 0;
19
+ for (let i = 0; i < layers.length; i++) {
20
+ const weight = weights[i] * (layers[i].opacity ?? 1);
21
+ if (weight <= 0)
22
+ continue;
23
+ const color = colors[i];
24
+ r += color.r * weight;
25
+ g += color.g * weight;
26
+ b += color.b * weight;
27
+ total += weight;
28
+ }
29
+ if (total > 0) {
30
+ out.setRGB(r / total, g / total, b / total);
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ const surface = terrain.surfaceAt(x, z);
36
+ if (surface === null)
37
+ return false;
38
+ const index = layers.findIndex(layer => layer.surface === surface);
39
+ if (index >= 0) {
40
+ if ((layers[index].opacity ?? 1) <= 0)
41
+ return false;
42
+ out.copy(colors[index]);
43
+ return true;
44
+ }
45
+ const color = defaults.get(surface);
46
+ if (color === undefined)
47
+ return false;
48
+ out.copy(color);
49
+ return true;
50
+ };
51
+ const cols = terrain.cols;
52
+ const rows = terrain.rows;
53
+ if (terrain.gridBounds === undefined || cols === undefined || rows === undefined || cols < 2 || rows < 2)
54
+ return cellColor;
55
+ const grid = terrain.gridBounds;
56
+ const cellW = (grid.maxX - grid.minX) / cols;
57
+ const cellD = (grid.maxZ - grid.minZ) / rows;
58
+ const base = new THREE.Color();
59
+ const corner = new THREE.Color();
60
+ return (x, z, out) => {
61
+ if (x < grid.minX || x > grid.maxX || z < grid.minZ || z > grid.maxZ)
62
+ return false;
63
+ const fx = (x - grid.minX) / cellW - 0.5;
64
+ const fz = (z - grid.minZ) / cellD - 0.5;
65
+ const gx = Math.floor(fx);
66
+ const gz = Math.floor(fz);
67
+ const tx = smooth(fx - gx);
68
+ const tz = smooth(fz - gz);
69
+ base.copy(out);
70
+ let painted = false, r = 0, g = 0, b = 0;
71
+ for (let dz = 0; dz <= 1; dz++) {
72
+ const cz = Math.min(rows - 1, Math.max(0, gz + dz));
73
+ const wz = dz === 0 ? 1 - tz : tz;
74
+ for (let dx = 0; dx <= 1; dx++) {
75
+ const cx = Math.min(cols - 1, Math.max(0, gx + dx));
76
+ const w = (dx === 0 ? 1 - tx : tx) * wz;
77
+ if (w <= 0)
78
+ continue;
79
+ corner.copy(base);
80
+ if (cellColor(grid.minX + (cx + 0.5) * cellW, grid.minZ + (cz + 0.5) * cellD, corner))
81
+ painted = true;
82
+ r += corner.r * w;
83
+ g += corner.g * w;
84
+ b += corner.b * w;
85
+ }
86
+ }
87
+ if (!painted)
88
+ return false;
89
+ out.setRGB(r, g, b);
90
+ return true;
91
+ };
92
+ }
@@ -0,0 +1,5 @@
1
+ import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
2
+ /** The player's graphics tier as terrain materials see it; the shell provides it, `high` outside one. */
3
+ export declare const TerrainQualityContext: import("react").Context<GraphicsQuality>;
4
+ /** Graphics tier for terrain surface shading (`low` takes the cheap path). */
5
+ export declare function useTerrainQuality(): GraphicsQuality;
@@ -0,0 +1,7 @@
1
+ import { createContext, useContext } from "react";
2
+ /** The player's graphics tier as terrain materials see it; the shell provides it, `high` outside one. */
3
+ export const TerrainQualityContext = createContext("high");
4
+ /** Graphics tier for terrain surface shading (`low` takes the cheap path). */
5
+ export function useTerrainQuality() {
6
+ return useContext(TerrainQualityContext);
7
+ }
@@ -0,0 +1,16 @@
1
+ /** Integer-hash value noise (`jgHashUint`, `jgVNoise`, `jgFbm`) every surface chunk builds on. */
2
+ export declare const NOISE_GLSL = "\nuint jgHashUint(uvec2 v){\n uint x = v.x * 1664525u + v.y * 1013904223u;\n x = (x ^ (x >> 16u)) * 2246822519u;\n x = (x ^ (x >> 13u)) * 3266489917u;\n x = x ^ (x >> 16u);\n return x;\n}\nfloat jgHash(vec2 p){\n ivec2 i = ivec2(floor(p));\n return float(jgHashUint(uvec2(i))) * (1.0 / 4294967296.0);\n}\nfloat jgVNoise(vec2 p){\n vec2 i = floor(p); vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n float a = jgHash(i);\n float b = jgHash(i + vec2(1.0, 0.0));\n float c = jgHash(i + vec2(0.0, 1.0));\n float d = jgHash(i + vec2(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\nfloat jgFbm(vec2 p){\n float v = 0.0; float amp = 0.5;\n for (int i = 0; i < 4; i++){ v += amp * jgVNoise(p); p *= 2.0; amp *= 0.5; }\n return v;\n}\n";
3
+ /**
4
+ * GLSL for the terrain surface pass: per-surface procedural ground (grass, dirt, gravel, rock, sand,
5
+ * mud, snow, road) and the height-aware blend between painted layers. Needs `NOISE_GLSL` first.
6
+ * Every octave is gated by `jgBand(frequency)` so detail finer than a pixel fades to its mean
7
+ * instead of shimmering at distance.
8
+ */
9
+ export declare const SURFACE_FUNCTIONS_GLSL = "\nfloat jgFootprint;\nfloat jgFbm2(vec2 p){ return jgVNoise(p) * 0.62 + jgVNoise(p * 2.03 + 17.3) * 0.38; }\nfloat jgFbm3o(vec2 p){ return (jgVNoise(p) * 0.5 + jgVNoise(p * 2.03 + 17.3) * 0.3 + jgVNoise(p * 4.1 + 5.9) * 0.2); }\nfloat jgBand(float freq){ return 1.0 - smoothstep(0.16, 0.42, freq * jgFootprint); }\nvec2 jgHash22(vec2 cell){\n uint h = jgHashUint(uvec2(ivec2(floor(cell + 0.5))));\n uint h2 = h * 747796405u + 2891336453u;\n h2 = (h2 ^ (h2 >> 15u)) * 2246822519u;\n return vec2(float(h), float(h2 ^ (h2 >> 13u))) * (1.0 / 4294967296.0);\n}\nfloat jgHash3(vec3 i){\n ivec3 c = ivec3(i);\n return float(jgHashUint(uvec2(uint(c.x) ^ (uint(c.z) * 2654435761u), uint(c.y) + uint(c.z) * 1013904223u))) * (1.0 / 4294967296.0);\n}\nfloat jgVNoise3(vec3 p){\n vec3 i = floor(p); vec3 f = fract(p);\n vec3 u = f * f * (3.0 - 2.0 * f);\n float a = jgHash3(i), b = jgHash3(i + vec3(1.0, 0.0, 0.0));\n float c = jgHash3(i + vec3(0.0, 1.0, 0.0)), d = jgHash3(i + vec3(1.0, 1.0, 0.0));\n float e = jgHash3(i + vec3(0.0, 0.0, 1.0)), g = jgHash3(i + vec3(1.0, 0.0, 1.0));\n float h = jgHash3(i + vec3(0.0, 1.0, 1.0)), k = jgHash3(i + vec3(1.0, 1.0, 1.0));\n return mix(mix(mix(a, b, u.x), mix(c, d, u.x), u.y), mix(mix(e, g, u.x), mix(h, k, u.x), u.y), u.z);\n}\nfloat jgFbm3(vec3 p){\n float v = 0.0; float amp = 0.5;\n for (int i = 0; i < 3; i++){ v += amp * jgVNoise3(p); p = p * 2.03 + vec3(1.7, 9.2, 4.1); amp *= 0.5; }\n return v / 0.875;\n}\n// x: distance to the nearest feature point, y: F2 - F1 (0 on a cell border), z: that cell's random id.\nvec3 jgVoronoi(vec2 p){\n vec2 n = floor(p); vec2 f = fract(p);\n float d1 = 8.0; float d2 = 8.0; float id = 0.0;\n for (int j = -1; j <= 1; j++){\n for (int i = -1; i <= 1; i++){\n vec2 g = vec2(float(i), float(j));\n vec2 o = jgHash22(n + g);\n vec2 r = g + 0.15 + o * 0.7 - f;\n float d = dot(r, r);\n if (d < d1){ d2 = d1; d1 = d; id = o.x; } else if (d < d2){ d2 = d; }\n }\n }\n d1 = sqrt(d1);\n return vec3(d1, sqrt(d2) - d1, id);\n}\n\nstruct JgSurf { vec3 albedo; float height; float rough; float bump; };\n\n// height: 0..1 relief used to decide which layer wins along a blend edge; bump: displacement in metres.\nJgSurf jgSurface(int cls, vec3 color, float rough, float scale, vec3 wp){\n vec2 p = wp.xz * scale;\n JgSurf s;\n s.albedo = color; s.rough = rough; s.height = 0.4; s.bump = 0.0;\n float m = jgFbm3o(p / 1.6 + float(cls) * 13.7);\n#ifdef JG_SURFACE_LOW\n s.albedo = color * (0.84 + 0.34 * m);\n s.height = m;\n return s;\n#else\n bool shaded = false;\n#if (JG_SURFACE_CLASSES & 2) != 0\n if (cls == 1) {\n float clump = jgFbm3o(p / 0.9 + 5.3);\n float dry = smoothstep(0.5, 0.75, jgFbm2(p / 7.0 + 17.0));\n float blade = jgVNoise(p * 11.0 + clump * 3.0);\n float fine = jgVNoise(p * 27.0 + 91.0);\n float bBlade = jgBand(11.0);\n s.albedo = mix(color, color * vec3(1.24, 1.12, 0.7), dry * 0.55);\n s.albedo *= 0.74 + 0.48 * clump;\n s.albedo = mix(s.albedo, color * vec3(0.78, 0.64, 0.46), smoothstep(0.42, 0.24, clump) * 0.25);\n s.albedo *= 1.0 + (blade - 0.5) * 0.5 * bBlade + (fine - 0.5) * 0.32 * jgBand(27.0);\n s.height = 0.2 + clump * 0.55 + blade * 0.25 * bBlade;\n s.bump = clump * 0.014 + blade * 0.008 * bBlade;\n shaded = true;\n }\n#endif\n#if (JG_SURFACE_CLASSES & 4) != 0\n if (cls == 2) {\n float clod = jgVNoise(p * 3.0 + m * 2.0);\n float crumb = jgVNoise(p * 14.0 + 7.0);\n vec3 v = jgVoronoi(p * 2.2 + 7.7);\n float peb = (1.0 - smoothstep(0.1, 0.2, v.x)) * step(0.7, v.z) * jgBand(7.0);\n s.albedo = color * (0.78 + 0.44 * m);\n s.albedo *= 1.0 + (clod - 0.5) * 0.32 * jgBand(3.0) + (crumb - 0.5) * 0.24 * jgBand(14.0);\n s.albedo = mix(s.albedo, color * vec3(1.34, 1.3, 1.24) + 0.02, peb * 0.75);\n s.height = 0.05 + m * 0.4 + clod * 0.2 + peb * 0.5;\n s.bump = clod * 0.012 * jgBand(3.0) + crumb * 0.003 * jgBand(14.0) + peb * 0.012;\n shaded = true;\n }\n#endif\n#if (JG_SURFACE_CLASSES & 8) != 0\n if (cls == 3) {\n // Round pebbles from F1 distance (not Voronoi cells, which read as flagstone paving), two sizes,\n // packed over darker grit.\n vec3 va = jgVoronoi(p * 5.5);\n float ra = 0.28 + 0.18 * fract(va.z * 13.1);\n float sa = 1.0 - smoothstep(ra * 0.72, ra, va.x);\n float da = sqrt(max(0.0, 1.0 - (va.x / ra) * (va.x / ra))) * sa;\n vec3 vb = jgVoronoi(p * 11.0 + 3.1);\n float rb = 0.32 + 0.14 * fract(vb.z * 7.7);\n float sb = (1.0 - smoothstep(rb * 0.66, rb, vb.x)) * (1.0 - sa);\n float db = sqrt(max(0.0, 1.0 - (vb.x / rb) * (vb.x / rb))) * sb;\n float grit = jgVNoise(p * 24.0);\n float bA = jgBand(5.5);\n float bB = jgBand(11.0);\n vec3 hueA = mix(vec3(1.08, 1.0, 0.9), vec3(0.92, 0.97, 1.08), fract(va.z * 7.13));\n vec3 hueB = mix(vec3(1.05, 1.0, 0.93), vec3(0.95, 0.98, 1.05), fract(vb.z * 5.71));\n vec3 near = color * (0.58 + 0.2 * grit);\n near = mix(near, color * hueB * (0.66 + 0.5 * vb.z) * (0.72 + 0.28 * db), sb * bB);\n near = mix(near, color * hueA * (0.64 + 0.62 * va.z) * (0.7 + 0.3 * da), sa);\n s.albedo = mix(color * (0.8 + 0.26 * m), near, bA);\n s.height = 0.3 + max(da, db * 0.6 * bB) * 0.6 * bA;\n s.bump = (da * 0.03 + db * 0.012 * bB) * bA;\n s.rough = rough - 0.08 * sa * bA;\n shaded = true;\n }\n#endif\n#if (JG_SURFACE_CLASSES & 16) != 0\n if (cls == 4) {\n vec3 q = wp * scale;\n float n = jgFbm3(q / 1.8);\n float n2 = jgFbm3(q * 1.7 + 5.0);\n float grain = jgVNoise3(q * 9.0);\n float strata = sin(q.y * 2.6 + n * 5.0) * 0.5 + 0.5;\n float pits = smoothstep(0.72, 0.9, jgVNoise3(q * 4.3 + 11.0)) * jgBand(4.3);\n float lichen = smoothstep(0.6, 0.78, jgFbm2(p / 3.0 + 41.0));\n s.albedo = color * (0.4 + 1.25 * n) * (0.8 + 0.4 * n2 * jgBand(3.4)) * (1.0 - pits * 0.35) * (0.94 + strata * 0.12);\n s.albedo *= 1.0 + (grain - 0.5) * 0.3 * jgBand(9.0);\n s.albedo = mix(s.albedo, s.albedo * vec3(0.92, 1.06, 0.82), lichen * 0.45);\n s.height = 0.35 + n * 0.65;\n s.bump = n * 0.1 + n2 * 0.02 * jgBand(3.4) - pits * 0.006 + grain * 0.004 * jgBand(9.0);\n s.rough = rough - 0.1 * n;\n shaded = true;\n }\n#endif\n#if (JG_SURFACE_CLASSES & 32) != 0\n if (cls == 5) {\n float warp = jgFbm2(p / 3.0 + 3.3);\n float ripple = sin((p.x * 0.83 + p.y * 0.56) * 6.0 + warp * 7.0) * 0.5 + 0.5;\n float grain = jgVNoise(p * 40.0);\n float bRipple = jgBand(1.0);\n s.albedo = color * (0.92 + 0.16 * m) * (1.0 + (ripple - 0.5) * 0.1 * bRipple);\n float speck = jgVNoise(p * 9.0 + 2.7);\n s.albedo *= 1.0 + (grain - 0.5) * 0.14 * jgBand(40.0) + (speck - 0.5) * 0.12 * jgBand(9.0);\n s.height = 0.05 + ripple * 0.25 + m * 0.2;\n s.bump = ripple * 0.012 * bRipple;\n shaded = true;\n }\n#endif\n#if (JG_SURFACE_CLASSES & 64) != 0\n if (cls == 6) {\n float puddle = smoothstep(0.56, 0.66, jgFbm3o(p / 3.2 + 31.0));\n float lumps = jgVNoise(p * 4.0 + m);\n s.albedo = color * (0.8 + 0.38 * m) * (1.0 + (lumps - 0.5) * 0.26 * jgBand(4.0));\n s.albedo = mix(s.albedo, s.albedo * 0.58, puddle);\n s.rough = mix(rough, 0.22, puddle);\n s.height = (0.1 + m * 0.4 + lumps * 0.2) * (1.0 - puddle * 0.8);\n s.bump = lumps * 0.012 * jgBand(4.0) * (1.0 - puddle);\n shaded = true;\n }\n#endif\n#if (JG_SURFACE_CLASSES & 128) != 0\n if (cls == 7) {\n float drift = jgFbm2(p / 4.0 + 9.0);\n s.albedo = color * (0.93 + 0.1 * m + 0.04 * drift);\n s.height = 0.3 + drift * 0.5;\n s.bump = drift * 0.03 + m * 0.01;\n shaded = true;\n }\n#endif\n#if (JG_SURFACE_CLASSES & 256) != 0\n if (cls == 8) {\n float grain = jgVNoise(p * 30.0);\n float stain = jgFbm2(p / 5.0 + 2.0);\n s.albedo = color * (0.88 + 0.22 * stain) * (1.0 + (grain - 0.5) * 0.2 * jgBand(30.0));\n s.height = 0.2 + stain * 0.2;\n s.bump = grain * 0.0015 * jgBand(30.0);\n shaded = true;\n }\n#endif\n if (!shaded) {\n float fine = jgVNoise(p * 6.0 + 3.0);\n s.albedo = color * (1.0 + (fine - 0.5) * 0.18 * jgBand(6.0));\n s.height = 0.25 + m * 0.4;\n s.bump = m * 0.02 + fine * 0.004 * jgBand(6.0);\n }\n return s;\n#endif\n}\n";
10
+ /** Paint-coverage uniforms and the warped, smoothed cell lookup. */
11
+ export declare const PAINT_PARS_GLSL = "\nuniform sampler2D uPaintA;\nuniform sampler2D uPaintB;\nuniform vec2 uPaintMin;\nuniform vec2 uPaintCell;\nuniform vec2 uPaintGrid;\nuniform float uPaintWarp;\nuniform vec3 uLayerColor[8];\nuniform float uLayerClass[8];\nuniform float uLayerRough[8];\nuniform float uLayerScale[8];\nfloat jgWarpNoise(vec2 p){ return (jgVNoise(p) * 0.65 + jgVNoise(p * 2.7 + 13.1) * 0.35) - 0.5; }\nvec2 jgPaintUv(vec2 wp){\n vec2 cell = (wp - uPaintMin) / uPaintCell;\n cell += vec2(jgWarpNoise(wp / 2.3 + 4.1), jgWarpNoise(wp / 2.3 + vec2(19.7, 3.3))) * uPaintWarp;\n vec2 c = cell - 0.5;\n vec2 i = floor(c); vec2 f = fract(c);\n f = f * f * (3.0 - 2.0 * f);\n return (i + f + 0.5) / uPaintGrid;\n}\n";
12
+ /**
13
+ * Micro-normal from a surface height `jgBumpH` (metres) via screen-space derivatives; goes after
14
+ * `normal_fragment_maps`. Skipped on the low tier.
15
+ */
16
+ export declare const SURFACE_BUMP_NORMAL_GLSL = "\n#ifndef JG_SURFACE_LOW\n{\n // Bump from the blended surface height (metres) via screen-space derivatives, unnormalised so the\n // tilt is the true world slope of that height field at any distance.\n vec3 jgSigX = dFdx(-vViewPosition);\n vec3 jgSigY = dFdy(-vViewPosition);\n vec2 jgDh = vec2(dFdx(jgBumpH), dFdy(jgBumpH));\n vec3 jgR1 = cross(jgSigY, normal);\n vec3 jgR2 = cross(normal, jgSigX);\n float jgDet = dot(jgSigX, jgR1) * faceDirection;\n vec3 jgGrad = sign(jgDet) * (jgDh.x * jgR1 + jgDh.y * jgR2);\n normal = normalize(abs(jgDet) * normal - jgGrad);\n}\n#endif\n";