@jgengine/shell 0.22.0 → 0.24.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 (92) hide show
  1. package/CHANGELOG.md +110 -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 +3 -1
  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 +19 -7
  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.js +3 -3
  34. package/dist/materialOverride.js +4 -2
  35. package/dist/playSurfaceFocus.d.ts +16 -0
  36. package/dist/playSurfaceFocus.js +42 -0
  37. package/dist/postfx/PostProcessing.d.ts +3 -1
  38. package/dist/postfx/PostProcessing.js +14 -6
  39. package/dist/registry.d.ts +5 -1
  40. package/dist/render/CascadedShadows.d.ts +2 -0
  41. package/dist/render/CascadedShadows.js +4 -0
  42. package/dist/render/EntityPreview.js +1 -1
  43. package/dist/render/EnvironmentLighting.d.ts +26 -7
  44. package/dist/render/EnvironmentLighting.js +40 -27
  45. package/dist/render/SceneLighting.js +2 -2
  46. package/dist/render/modelLoad.d.ts +7 -0
  47. package/dist/render/modelLoad.js +13 -0
  48. package/dist/render/modelRender.d.ts +1 -0
  49. package/dist/render/modelRender.js +3 -0
  50. package/dist/render/modelSurface.d.ts +39 -0
  51. package/dist/render/modelSurface.js +206 -0
  52. package/dist/render/useModelInstance.d.ts +1 -1
  53. package/dist/render/useModelInstance.js +2 -2
  54. package/dist/scatter/scatterModels.js +3 -1
  55. package/dist/scene/AuthoredScene.d.ts +11 -3
  56. package/dist/scene/AuthoredScene.js +17 -9
  57. package/dist/scene/CityRenderer.js +142 -25
  58. package/dist/scene/cityJunctionSurface.d.ts +7 -0
  59. package/dist/scene/cityJunctionSurface.js +34 -0
  60. package/dist/settings/settingsController.js +1 -1
  61. package/dist/terrain/groundEdgeFade.d.ts +67 -0
  62. package/dist/terrain/groundEdgeFade.js +234 -0
  63. package/dist/terrain/index.d.ts +5 -0
  64. package/dist/terrain/index.js +5 -0
  65. package/dist/terrain/surfaceRibbonMaterial.d.ts +62 -0
  66. package/dist/terrain/surfaceRibbonMaterial.js +227 -0
  67. package/dist/terrain/terrainDetailMaterial.d.ts +33 -10
  68. package/dist/terrain/terrainDetailMaterial.js +149 -63
  69. package/dist/terrain/terrainPaintColor.d.ts +12 -3
  70. package/dist/terrain/terrainPaintColor.js +46 -1
  71. package/dist/terrain/terrainQuality.d.ts +5 -0
  72. package/dist/terrain/terrainQuality.js +7 -0
  73. package/dist/terrain/terrainSurfaceShader.d.ts +16 -0
  74. package/dist/terrain/terrainSurfaceShader.js +266 -0
  75. package/dist/terrain/terrainSurfaces.d.ts +59 -0
  76. package/dist/terrain/terrainSurfaces.js +199 -0
  77. package/dist/world/StaticObjectModels.d.ts +1 -1
  78. package/dist/world/StaticObjectModels.js +1 -1
  79. package/dist/world/WorldScene.js +1 -1
  80. package/dist/world/staticObjectBatches.d.ts +5 -1
  81. package/dist/world/staticObjectBatches.js +9 -5
  82. package/dist/world/worldBarSamples.d.ts +13 -1
  83. package/dist/world/worldBarSamples.js +40 -8
  84. package/package.json +5 -5
  85. package/skills/jgengine/api.md +18 -18
  86. package/skills/jgengine-ui/SKILL.md +4 -0
  87. package/skills/jgengine-ui/api.md +254 -37
  88. package/skills/jgengine-ui/capabilities.md +58 -0
  89. package/skills/jgengine-ui/reference-primitives.md +4 -4
  90. package/skills/jgengine-world/api.md +27 -14
  91. package/skills/jgengine-world/capabilities.md +4 -0
  92. package/skills/jgengine-world/reference.md +27 -8
@@ -1,8 +1,14 @@
1
1
  import * as THREE from "three";
2
+ import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
2
3
  import type { ResolvedTerrainDetail } from "@jgengine/core/world/terrain";
4
+ import type { TerrainPaintTextures } from "./terrainSurfaces.js";
3
5
  /** The built procedural detail terrain material, ready to mount on the ground mesh. */
4
6
  export interface TerrainDetailMaterialHandle {
5
7
  material: THREE.MeshStandardMaterial;
8
+ /** Editor relief-contour strength 0..1 (0 in play); write `.value` to retune without recompiling. */
9
+ contour: {
10
+ value: number;
11
+ };
6
12
  }
7
13
  /**
8
14
  * Loaded PBR textures matching a resolved `ResolvedTerrainDetailMaterial.maps`' roles. The caller
@@ -15,17 +21,34 @@ export interface TerrainDetailMaterialTextures {
15
21
  roughness?: THREE.Texture;
16
22
  ao?: THREE.Texture;
17
23
  }
24
+ /** Surface-pass inputs beyond the resolved detail config. */
25
+ export interface TerrainDetailMaterialOptions {
26
+ /** Authored paint coverage from {@link createTerrainPaintTextures}; omit for unpainted ground. */
27
+ paint?: TerrainPaintTextures;
28
+ /** Graphics tier. `low` draws one cheap mottle per surface and skips micro-normals. Default `high`. */
29
+ quality?: GraphicsQuality;
30
+ /**
31
+ * Surface classes to compile (bit `1 << classIndex`); defaults to the paint's own classes. An
32
+ * editor that can paint any surface passes {@link ALL_TERRAIN_SURFACE_CLASSES}.
33
+ */
34
+ classMask?: number;
35
+ /** Initial editor relief-contour strength. Default 0. */
36
+ contour?: number;
37
+ }
38
+ /** Compile every surface response, for an editor viewport whose paint can change to any surface. */
39
+ export declare const ALL_TERRAIN_SURFACE_CLASSES = 511;
18
40
  /**
19
- * A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex
20
- * colour as the base ground and blends procedural, noise-broken rock (by slope),
21
- * sand (by waterline), and snow (by height) over it — textured-reading terrain
22
- * with no image assets. Full PBR: lit, shadowed, and fogged like any standard
23
- * material, so it composes with the post-processing chain.
41
+ * A `MeshStandardMaterial` that renders terrain as ground rather than flat colour. Each painted
42
+ * layer draws its surface type's procedural response (grass clumps and blades, dirt clods and
43
+ * pebbles, gravel stones, cracked rock, rippled sand, wet mud, snow, road grain) in the layer's
44
+ * authored hue, with albedo, roughness and a micro-normal. Layers meet along noise-warped,
45
+ * height-aware edges instead of paint-cell blobs; steep slopes turn to rock; macro sweeps break up
46
+ * tiling. Unpainted ground keeps the biome-tinted vertex colour with procedural detail, plus the
47
+ * waterline sand and altitude snow bands. Full PBR: lit, shadowed and fogged like any standard material.
24
48
  *
25
49
  * When `detail.material` is set, pass the matching loaded `textures` (color/normal/roughness/ao):
26
50
  * they tile by world position at `detail.material.repeat` world units per tile and blend over the
27
- * procedural result at `detail.material.strength` — the real-texture seam layers onto the existing
28
- * look instead of replacing it.
29
- * @internal
30
- */
31
- export declare function createTerrainDetailMaterial(detail: ResolvedTerrainDetail, textures?: TerrainDetailMaterialTextures): TerrainDetailMaterialHandle;
51
+ * procedural result at `detail.material.strength`.
52
+ * @internal
53
+ */
54
+ export declare function createTerrainDetailMaterial(detail: ResolvedTerrainDetail, textures?: TerrainDetailMaterialTextures, options?: TerrainDetailMaterialOptions): TerrainDetailMaterialHandle;
@@ -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
  }
@@ -1,11 +1,20 @@
1
1
  import * as THREE from "three";
2
2
  import type { EditableTerrain } from "@jgengine/core/world/terraform";
3
- /** Live authored paint channels needed to sample a terrain's material stack without its heightfield. */
4
- export type TerrainPaintSource = Pick<EditableTerrain, "layers" | "weightsAt" | "surfaceAt">;
5
- /** Writes authored paint into an existing linear color; returns false for unpainted ground. */
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
+ */
6
12
  export type TerrainPaintColorSampler = (x: number, z: number, out: THREE.Color) => boolean;
7
13
  /**
8
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.
9
18
  * @capability terrain-paint-color sample authored terrain surfaces and blended layers for shared ground rendering
10
19
  */
11
20
  export declare function createTerrainPaintColorSampler(terrain: TerrainPaintSource): TerrainPaintColorSampler;
@@ -1,14 +1,18 @@
1
1
  import * as THREE from "three";
2
2
  import { TERRAIN_MATERIAL_COLORS } from "@jgengine/core/editor/terrainMaterials";
3
+ const smooth = (t) => t * t * (3 - 2 * t);
3
4
  /**
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.
5
9
  * @capability terrain-paint-color sample authored terrain surfaces and blended layers for shared ground rendering
6
10
  */
7
11
  export function createTerrainPaintColorSampler(terrain) {
8
12
  const layers = terrain.layers;
9
13
  const colors = layers.map(layer => new THREE.Color(layer.tint ?? TERRAIN_MATERIAL_COLORS[layer.surface] ?? "#7c7f86"));
10
14
  const defaults = new Map(Object.entries(TERRAIN_MATERIAL_COLORS).map(([id, color]) => [id, new THREE.Color(color)]));
11
- return (x, z, out) => {
15
+ const cellColor = (x, z, out) => {
12
16
  const weights = terrain.weightsAt(x, z);
13
17
  if (layers.length > 0 && weights.length === layers.length) {
14
18
  let total = 0, r = 0, g = 0, b = 0;
@@ -44,4 +48,45 @@ export function createTerrainPaintColorSampler(terrain) {
44
48
  out.copy(color);
45
49
  return true;
46
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
+ };
47
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";