@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,6 +1,8 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useMemo } from "react";
3
3
  import { resolveGrassObject } from "@jgengine/core/world/grassKind";
4
+ import { clearanceMasksFrom, DEFAULT_CLEARANCE_KINDS } from "@jgengine/core/world/scatterRegion";
5
+ import { masksInfluence } from "@jgengine/core/world/geometry";
4
6
  import { GrassField } from "../terrain/GrassField.js";
5
7
  import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
6
8
  /** Volume kinds turf yields to — blades never grow through cracked soil or out of open water. */
@@ -47,9 +49,25 @@ function sampleHeight(field, x, z) {
47
49
  */
48
50
  function OneGrass({ object, context }) {
49
51
  const resolved = useMemo(() => resolveGrassObject(object), [object]);
50
- const heightAt = useCallback((x, z) => sampleHeight(context.field, x, z), [context.field]);
52
+ const heightAt = useCallback((x, z) => resolved === null ? 0 :
53
+ sampleHeight(context.field, x + resolved.center[0], z + resolved.center[2]) - resolved.center[1], [context.field, resolved]);
51
54
  const area = useMemo(() => (resolved === null ? null : [resolved.size[0], resolved.size[1]]), [resolved]);
52
55
  const bladeHeight = useMemo(() => (resolved === null ? null : [resolved.rules.bladeHeight * 0.7, resolved.rules.bladeHeight]), [resolved]);
56
+ const bladeWidth = useMemo(() => resolved === null ? undefined : [resolved.rules.bladeWidthMin, resolved.rules.bladeWidthMax], [resolved]);
57
+ const bladeBend = useMemo(() => resolved === null ? undefined : [resolved.rules.bladeBendMin, resolved.rules.bladeBendMax], [resolved]);
58
+ const { markers, paths } = context.document;
59
+ const masks = useMemo(() => {
60
+ const authored = clearanceMasksFrom(context.document, {
61
+ kinds: [...DEFAULT_CLEARANCE_KINDS, "road", "walkway", "route", "path"],
62
+ });
63
+ // Keep the entire authored footing clear; feather regrowth outside its edge.
64
+ return {
65
+ discs: authored.discs.map(zone => ({ ...zone, radius: zone.radius + (zone.feather ?? 0) })),
66
+ corridors: authored.corridors.map(zone => ({ ...zone, halfWidth: zone.halfWidth + (zone.feather ?? 0) })),
67
+ };
68
+ }, [markers, paths, context.document.volumes]);
69
+ const coverageAt = useCallback((x, z) => resolved === null ? 0 :
70
+ 1 - masksInfluence(x + resolved.center[0], z + resolved.center[2], masks), [resolved, masks]);
53
71
  const wind = useMemo(() => resolved === null
54
72
  ? null
55
73
  : { strength: resolved.rules.windStrength, speed: resolved.rules.windSpeed, gustScale: resolved.rules.windGust }, [resolved]);
@@ -68,7 +86,7 @@ function OneGrass({ object, context }) {
68
86
  // footprint, and the fade only thins how many *draw* at range in the vertex shader — so a deeper
69
87
  // meadow reads expansive without raising the instance ceiling.
70
88
  const instanceCount = Math.max(1500, Math.min(250000, Math.ceil(rules.density * area[0] * area[1])));
71
- return (_jsx(GrassField, { position: [center[0], center[1], center[2]], area: area, count: instanceCount, density: rules.density, bladeHeight: bladeHeight, colorBase: rules.colorBase, colorTip: rules.colorTip, colorGround: context.groundColorAt?.(center[0], center[2]), colorVariation: rules.colorVariation, roughness: rules.roughness, seed: hashSeed(rules.seed.length > 0 ? rules.seed : object.id), heightAt: heightAt, wind: wind, edgeFeather: 2.2, exclude: exclude }));
89
+ return (_jsx(GrassField, { position: [center[0], center[1], center[2]], area: area, count: instanceCount, density: rules.density, bladeHeight: bladeHeight, bladeWidth: bladeWidth, bladeBend: bladeBend, tuftRadius: rules.tuftRadius, normalLift: rules.normalLift, colorBase: rules.colorBase, colorTip: rules.colorTip, colorGround: context.groundColorAt?.(center[0], center[2]), colorVariation: rules.colorVariation, roughness: rules.roughness, seed: hashSeed(rules.seed.length > 0 ? rules.seed : object.id), heightAt: heightAt, coverageAt: coverageAt, wind: wind, edgeFeather: 2.2, exclude: exclude }));
72
90
  }
73
91
  /** Registers the `grass_field` volume renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
74
92
  export function registerGrassFieldRenderer() {
@@ -0,0 +1,7 @@
1
+ import type { CityIntersection, CityStreet } from "@jgengine/core/world/cityKind";
2
+ /**
3
+ * Surface for a crossing's welded patch: gravel only when every street running through the crossing
4
+ * is gravel, so an unpaved hamlet's junctions stay gravel while a lane meeting a paved main stays asphalt.
5
+ * @internal
6
+ */
7
+ export declare function junctionSurface(cross: Pick<CityIntersection, "x" | "z" | "radius">, streets: readonly Pick<CityStreet, "points" | "width" | "surface">[]): CityStreet["surface"];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Surface for a crossing's welded patch: gravel only when every street running through the crossing
3
+ * is gravel, so an unpaved hamlet's junctions stay gravel while a lane meeting a paved main stays asphalt.
4
+ * @internal
5
+ */
6
+ export function junctionSurface(cross, streets) {
7
+ let touched = false;
8
+ for (const street of streets) {
9
+ const reach = cross.radius + street.width * 0.5 + 0.5;
10
+ if (distanceToPath(cross.x, cross.z, street.points) > reach)
11
+ continue;
12
+ if (street.surface !== "gravel")
13
+ return "asphalt";
14
+ touched = true;
15
+ }
16
+ return touched ? "gravel" : "asphalt";
17
+ }
18
+ function distanceToPath(x, z, points) {
19
+ if (points.length === 0)
20
+ return Infinity;
21
+ if (points.length === 1)
22
+ return Math.hypot(x - points[0][0], z - points[0][1]);
23
+ let best = Infinity;
24
+ for (let i = 1; i < points.length; i += 1) {
25
+ const [ax, az] = points[i - 1];
26
+ const [bx, bz] = points[i];
27
+ const dx = bx - ax;
28
+ const dz = bz - az;
29
+ const len2 = dx * dx + dz * dz;
30
+ const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((x - ax) * dx + (z - az) * dz) / len2));
31
+ best = Math.min(best, Math.hypot(x - (ax + dx * t), z - (az + dz * t)));
32
+ }
33
+ return best;
34
+ }
@@ -51,7 +51,7 @@ export function useSettingsCategories(config) {
51
51
  const id = busVolumeSettingId(bus.id);
52
52
  return {
53
53
  id,
54
- label: `${bus.id.charAt(0).toUpperCase()}${bus.id.slice(1)} volume`,
54
+ label: bus.id === "ui" ? "UI volume" : `${bus.id.charAt(0).toUpperCase()}${bus.id.slice(1)} volume`,
55
55
  kind: "slider",
56
56
  value: store.get(id, bus.gain ?? 1),
57
57
  min: 0,
@@ -11,6 +11,7 @@ export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" |
11
11
  colors?: FieldGroundOptions["colors"];
12
12
  heightRange?: FieldGroundOptions["heightRange"];
13
13
  paletteAt?: FieldGroundOptions["paletteAt"];
14
+ paintColorAt?: FieldGroundOptions["paintColorAt"];
14
15
  roughness?: number;
15
16
  metalness?: number;
16
17
  /** Override the default vertex-colour material (e.g. a procedural detail material). The caller owns its lifecycle; it is not disposed here. */
@@ -23,4 +24,4 @@ export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" |
23
24
  * Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
24
25
  * once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
25
26
  */
26
- export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness, metalness, surfaceMaterial, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
27
+ export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, paintColorAt, roughness, metalness, surfaceMaterial, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
@@ -8,8 +8,8 @@ import { createFieldGroundGeometry } from "./terrainMath.js";
8
8
  * Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
9
9
  * once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
10
10
  */
11
- export function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }) {
12
- const geometry = useDisposable(() => createFieldGroundGeometry(field, { size, segments, center, colors, heightRange, paletteAt }), [field, size, segments, center, colors, heightRange, paletteAt, epoch]);
11
+ export function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, paintColorAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }) {
12
+ const geometry = useDisposable(() => createFieldGroundGeometry(field, { size, segments, center, colors, heightRange, paletteAt, paintColorAt }), [field, size, segments, center, colors, heightRange, paletteAt, paintColorAt, epoch]);
13
13
  const defaultMaterial = useDisposable(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
14
14
  const material = surfaceMaterial ?? defaultMaterial;
15
15
  return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
@@ -19,6 +19,7 @@ export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "c
19
19
  edgeFeather?: number;
20
20
  exclude?: readonly GrassExclusion[];
21
21
  heightAt?: TerrainHeightSampler;
22
+ coverageAt?: TerrainHeightSampler;
22
23
  colorBase?: GrassMaterialOptions["colorBase"];
23
24
  colorTip?: GrassMaterialOptions["colorTip"];
24
25
  colorGround?: GrassMaterialOptions["colorGround"];
@@ -29,4 +30,4 @@ export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "c
29
30
  roughness?: number;
30
31
  }
31
32
  /** Seeded grass tufts with bounded per-camera chunk submission; density and budgets stay in blades. */
32
- export declare function GrassField({ count, density, budget, area, seed, segments, bladeHeight, bladeWidth, bladeBend, tuftBlades, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow, receiveShadow, frustumCulled, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
33
+ export declare function GrassField({ count, density, budget, area, seed, segments, bladeHeight, bladeWidth, bladeBend, tuftBlades, tuftRadius, edgeFeather, exclude, heightAt, coverageAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow, receiveShadow, frustumCulled, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
@@ -9,7 +9,7 @@ import { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget,
9
9
  export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
10
10
  export { GRASS_TUFT_BLADES } from "./grassGeometry.js";
11
11
  /** Seeded grass tufts with bounded per-camera chunk submission; density and budgets stay in blades. */
12
- export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }) {
12
+ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, coverageAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }) {
13
13
  const chunks = useMemo(() => createGrassGeometryChunks({
14
14
  count,
15
15
  area,
@@ -23,7 +23,8 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
23
23
  ...(edgeFeather === undefined ? {} : { edgeFeather }),
24
24
  ...(exclude === undefined ? {} : { exclude }),
25
25
  heightAt,
26
- }), [area, bladeBend, bladeHeight, bladeWidth, count, edgeFeather, exclude, heightAt, seed, segments, tuftBlades, tuftRadius]);
26
+ coverageAt,
27
+ }), [area, bladeBend, bladeHeight, bladeWidth, count, coverageAt, edgeFeather, exclude, heightAt, seed, segments, tuftBlades, tuftRadius]);
27
28
  useDisposable(() => chunks.map((chunk) => chunk.geometry), [chunks]);
28
29
  const handle = useMemo(() => createGrassMaterial({
29
30
  colorBase,
@@ -31,6 +31,8 @@ export interface GrassBladeGeometryOptions {
31
31
  /** Patch-local rectangles kept clear of blades (soil patches, water) with a soft regrow ramp. */
32
32
  exclude?: readonly GrassExclusion[];
33
33
  heightAt?: TerrainHeightSampler;
34
+ /** Patch-local coverage, 0..1, composed with explicit rectangular exclusions. */
35
+ coverageAt?: TerrainHeightSampler;
34
36
  }
35
37
  export interface ResolvedGrassBladeGeometryOptions {
36
38
  count: number;
@@ -45,6 +47,7 @@ export interface ResolvedGrassBladeGeometryOptions {
45
47
  edgeFeather: number;
46
48
  exclude: readonly GrassExclusion[];
47
49
  heightAt: TerrainHeightSampler;
50
+ coverageAt: TerrainHeightSampler;
48
51
  }
49
52
  /** @internal */
50
53
  export declare function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number];
@@ -24,6 +24,7 @@ export function resolveGrassBladeGeometryOptions(options = {}) {
24
24
  edgeFeather: Math.max(0, options.edgeFeather ?? 1.5),
25
25
  exclude: options.exclude ?? [],
26
26
  heightAt: options.heightAt ?? (() => 0),
27
+ coverageAt: options.coverageAt ?? (() => 1),
27
28
  };
28
29
  }
29
30
  /** Tuft instances needed to carry `bladeCount` blades at `tuftBlades` blades per tuft. @internal */
@@ -123,7 +124,7 @@ export function createGrassBladeGeometry(options = {}) {
123
124
  const d = Math.max(Math.abs(x - zone.center[0]) - zone.half[0], Math.abs(z - zone.center[1]) - zone.half[1]);
124
125
  heightScale *= Math.min(1, Math.max(0, d / Math.max(0.001, zone.feather ?? 1.5)));
125
126
  }
126
- heights[index] = heightScale;
127
+ heights[index] = heightScale * Math.max(0, Math.min(1, resolved.coverageAt(x, z)));
127
128
  widths[index] = randomRange(random, resolved.width);
128
129
  bend[index] = randomRange(random, resolved.bend);
129
130
  phase[index] = random() * Math.PI * 2;
@@ -9,3 +9,8 @@ export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMa
9
9
  export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "./random.js";
10
10
  export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainSurfaceColorOptions, type TerrainVertexColorOptions, } from "./terrainMath.js";
11
11
  export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, type FractalNoiseConfig, type NoiseFieldConfig, type TerrainField, type TerrainNormal, } from "@jgengine/core/world/terrain";
12
+ export { createTerrainPaintColorSampler, type TerrainPaintColorSampler, type TerrainPaintSource } from "./terrainPaintColor.js";
13
+ export { ALL_TERRAIN_SURFACE_CLASSES, createTerrainDetailMaterial, type TerrainDetailMaterialHandle, type TerrainDetailMaterialOptions, type TerrainDetailMaterialTextures, } from "./terrainDetailMaterial.js";
14
+ export { createTerrainPaintTextures, MAX_TERRAIN_PAINT_LAYERS, packTerrainPaintWeights, terrainSurfaceClass, terrainSurfaceClassIndex, terrainSurfaceRoughness, type TerrainPaintSlot, type TerrainPaintTextures, type TerrainPaintWeights, type TerrainSurfaceClass, } from "./terrainSurfaces.js";
15
+ export { TerrainQualityContext, useTerrainQuality } from "./terrainQuality.js";
16
+ export { createSurfaceRibbonMaterial, resolvePathSurface, RIBBON_FRAME_ATTRIBUTE, ribbonEdgeMargin, ribbonSurfaceFrame, useSurfaceRibbonMaterial, type RibbonSurfaceStyle, type SurfaceRibbonMaterialOptions, } from "./surfaceRibbonMaterial.js";
@@ -9,3 +9,8 @@ export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./gr
9
9
  export { createSeededRandom, hashNoise2, seedToUint32 } from "./random.js";
10
10
  export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
11
11
  export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, } from "@jgengine/core/world/terrain";
12
+ export { createTerrainPaintColorSampler } from "./terrainPaintColor.js";
13
+ export { ALL_TERRAIN_SURFACE_CLASSES, createTerrainDetailMaterial, } from "./terrainDetailMaterial.js";
14
+ export { createTerrainPaintTextures, MAX_TERRAIN_PAINT_LAYERS, packTerrainPaintWeights, terrainSurfaceClass, terrainSurfaceClassIndex, terrainSurfaceRoughness, } from "./terrainSurfaces.js";
15
+ export { TerrainQualityContext, useTerrainQuality } from "./terrainQuality.js";
16
+ export { createSurfaceRibbonMaterial, resolvePathSurface, RIBBON_FRAME_ATTRIBUTE, ribbonEdgeMargin, ribbonSurfaceFrame, useSurfaceRibbonMaterial, } from "./surfaceRibbonMaterial.js";
@@ -0,0 +1,62 @@
1
+ import * as THREE from "three";
2
+ import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
3
+ /** Vertex attribute {@link ribbonSurfaceFrame} fills: metres along the ribbon, signed metres across it, metres to its far end. */
4
+ export declare const RIBBON_FRAME_ATTRIBUTE = "jgRibbon";
5
+ /** How a ground ribbon (path, road, sidewalk) shades: procedural surface, edge blend and wear. */
6
+ export interface RibbonSurfaceStyle {
7
+ /** Surface id classified like terrain paint (`"dirt"`, `"gravel"`, `"asphalt"`, …). */
8
+ surface: string;
9
+ /** Width of the edge fade in metres, centred on the authored edge. 0 keeps a hard opaque edge. */
10
+ feather: number;
11
+ /** How far noise pushes the edge in or out, in metres. */
12
+ warp: number;
13
+ /** Wheel ruts on soft ground, polished tyre lanes on pavement, 0..1. */
14
+ wear: number;
15
+ }
16
+ /** Options for {@link createSurfaceRibbonMaterial}. */
17
+ export interface SurfaceRibbonMaterialOptions extends Partial<RibbonSurfaceStyle> {
18
+ /** Authored hue; the procedural surface varies around it. */
19
+ color: string;
20
+ /** Authored half width in metres; the edge fade is centred on it. */
21
+ halfWidth: number;
22
+ /** Override the surface class's default roughness. */
23
+ roughness?: number;
24
+ /** Pattern frequency multiplier. Default 1. */
25
+ scale?: number;
26
+ /** Graphics tier, from `useTerrainQuality()`. `low` drops micro-normals and fine octaves. Default `high`. */
27
+ quality?: GraphicsQuality;
28
+ /** Pull the ribbon toward the camera in depth so it wins over a coplanar layer beneath. */
29
+ polygonOffset?: boolean;
30
+ /** Multiply by the geometry's `color` attribute, as city ground meshes carry per-vertex tints. */
31
+ vertexColors?: boolean;
32
+ }
33
+ /**
34
+ * Default ribbon look for an authored path: the surface comes from `meta.surface`, else the path
35
+ * kind (`"road"` paves, `"gravel-track"` gravels), else dirt. Soft ground gets a noise-warped edge
36
+ * that fades into the terrain and wheel ruts; pavement keeps a near-hard edge and faint tyre lanes.
37
+ * `meta.surfaceDetail: false` (or `meta.surface: false`) returns `null` for the old flat ribbon;
38
+ * `meta.edgeSoftness` scales the fade (0 = hard) and `meta.wear` sets wear 0..1.
39
+ */
40
+ export declare function resolvePathSurface(kind: string, width: number, meta: Readonly<Record<string, unknown>> | undefined): RibbonSurfaceStyle | null;
41
+ /** Extra ribbon width (metres, both sides together) a style needs so its fade straddles the authored edge. */
42
+ export declare function ribbonEdgeMargin(style: Pick<RibbonSurfaceStyle, "feather" | "warp">): number;
43
+ /**
44
+ * Per-vertex `(along, across, toEnd)` for a ribbon in `buildRoadRibbon`'s layout (left/right vertex
45
+ * pairs down the centreline). Pass several ribbons' position arrays to get one concatenated frame
46
+ * for a merged mesh; each ribbon's distances restart at its own start.
47
+ */
48
+ export declare function ribbonSurfaceFrame(ribbons: readonly Float32Array[]): Float32Array;
49
+ /**
50
+ * A `MeshStandardMaterial` that shades a ground ribbon (path, road, sidewalk) with the terrain's own
51
+ * procedural surface functions, so a dirt track or asphalt strip matches painted ground: the
52
+ * authored colour varied by the surface class's grain, roughness and micro-normal, wheel ruts or tyre
53
+ * lanes, and (with `feather > 0`) a noise-warped edge that fades into the terrain. The geometry needs
54
+ * a {@link RIBBON_FRAME_ATTRIBUTE} attribute from {@link ribbonSurfaceFrame}; build it wider by
55
+ * {@link ribbonEdgeMargin} so the fade straddles the authored edge. The caller disposes the material.
56
+ */
57
+ export declare function createSurfaceRibbonMaterial(options: SurfaceRibbonMaterialOptions): THREE.MeshStandardMaterial;
58
+ /**
59
+ * {@link createSurfaceRibbonMaterial} for a React mesh: rebuilt only when an option changes and
60
+ * disposed on unmount. Pass `null` to get `null` (the caller falls back to a flat material).
61
+ */
62
+ export declare function useSurfaceRibbonMaterial(options: SurfaceRibbonMaterialOptions | null): THREE.MeshStandardMaterial | null;
@@ -0,0 +1,227 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import * as THREE from "three";
3
+ import { NOISE_GLSL, SURFACE_BUMP_NORMAL_GLSL, SURFACE_FUNCTIONS_GLSL } from "./terrainSurfaceShader.js";
4
+ import { terrainSurfaceClass, terrainSurfaceClassIndex, terrainSurfaceRoughness } from "./terrainSurfaces.js";
5
+ /** Vertex attribute {@link ribbonSurfaceFrame} fills: metres along the ribbon, signed metres across it, metres to its far end. */
6
+ export const RIBBON_FRAME_ATTRIBUTE = "jgRibbon";
7
+ const SOFT_GROUND = new Set(["dirt", "mud", "sand", "gravel", "grass", "ground", "snow"]);
8
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
9
+ /**
10
+ * Default ribbon look for an authored path: the surface comes from `meta.surface`, else the path
11
+ * kind (`"road"` paves, `"gravel-track"` gravels), else dirt. Soft ground gets a noise-warped edge
12
+ * that fades into the terrain and wheel ruts; pavement keeps a near-hard edge and faint tyre lanes.
13
+ * `meta.surfaceDetail: false` (or `meta.surface: false`) returns `null` for the old flat ribbon;
14
+ * `meta.edgeSoftness` scales the fade (0 = hard) and `meta.wear` sets wear 0..1.
15
+ */
16
+ export function resolvePathSurface(kind, width, meta) {
17
+ if (meta?.["surfaceDetail"] === false || meta?.["surface"] === false)
18
+ return null;
19
+ const id = typeof meta?.["surface"] === "string" ? meta["surface"] : kind;
20
+ const classified = terrainSurfaceClass(id);
21
+ const cls = classified === "ground" ? "dirt" : classified;
22
+ const surface = classified === "ground" ? "dirt" : id;
23
+ const soft = SOFT_GROUND.has(cls);
24
+ const softness = typeof meta?.["edgeSoftness"] === "number" ? Math.max(0, meta["edgeSoftness"]) : 1;
25
+ const feather = (soft ? clamp(width * 0.18, 0.35, 1.6) : clamp(width * 0.04, 0.08, 0.3)) * softness;
26
+ const warp = (soft ? clamp(width * 0.08, 0.15, 0.6) : 0.03) * softness;
27
+ const wear = typeof meta?.["wear"] === "number" ? clamp(meta["wear"], 0, 1) : soft ? 1 : 0.6;
28
+ return { surface, feather, warp, wear };
29
+ }
30
+ /** Extra ribbon width (metres, both sides together) a style needs so its fade straddles the authored edge. */
31
+ export function ribbonEdgeMargin(style) {
32
+ return style.feather + style.warp * 2;
33
+ }
34
+ /**
35
+ * Per-vertex `(along, across, toEnd)` for a ribbon in `buildRoadRibbon`'s layout (left/right vertex
36
+ * pairs down the centreline). Pass several ribbons' position arrays to get one concatenated frame
37
+ * for a merged mesh; each ribbon's distances restart at its own start.
38
+ */
39
+ export function ribbonSurfaceFrame(ribbons) {
40
+ const total = ribbons.reduce((sum, positions) => sum + positions.length, 0);
41
+ const out = new Float32Array(total);
42
+ let offset = 0;
43
+ for (const p of ribbons) {
44
+ const pairs = Math.floor(p.length / 6);
45
+ let along = 0;
46
+ let px = 0;
47
+ let pz = 0;
48
+ for (let i = 0; i < pairs; i++) {
49
+ const b = i * 6;
50
+ const cx = (p[b] + p[b + 3]) / 2;
51
+ const cz = (p[b + 2] + p[b + 5]) / 2;
52
+ if (i > 0)
53
+ along += Math.hypot(cx - px, cz - pz);
54
+ px = cx;
55
+ pz = cz;
56
+ const half = Math.hypot(p[b] - p[b + 3], p[b + 2] - p[b + 5]) / 2;
57
+ out[offset + b] = along;
58
+ out[offset + b + 1] = half;
59
+ out[offset + b + 3] = along;
60
+ out[offset + b + 4] = -half;
61
+ }
62
+ for (let i = 0; i < pairs; i++) {
63
+ const b = offset + i * 6;
64
+ out[b + 2] = along - out[b];
65
+ out[b + 5] = along - out[b + 3];
66
+ }
67
+ offset += p.length;
68
+ }
69
+ return out;
70
+ }
71
+ const RIBBON_FRAGMENT_GLSL = /* glsl */ `
72
+ vec2 jgWp = vJgWorldPos.xz;
73
+ jgFootprint = max(length(dFdx(vJgWorldPos)), length(dFdy(vJgWorldPos)));
74
+ JgSurf jgS = jgSurface(JG_RIBBON_CLASS, diffuseColor.rgb, uJgRough, uJgScale, vJgWorldPos);
75
+ vec3 jgCol = jgS.albedo;
76
+ float jgRough = jgS.rough;
77
+ float jgBumpH = jgS.bump;
78
+ float jgAcross = vJgRibbon.y;
79
+ jgCol *= 1.0 + (jgFbm2(jgWp / 14.0 + vec2(3.7, 61.2)) - 0.5) * 0.24;
80
+ #if defined(JG_RIBBON_PAVED) && !defined(JG_SURFACE_LOW)
81
+ {
82
+ // Weathered pavement: resurfaced patches, exposed aggregate and a partial crack network.
83
+ jgCol *= 1.0 + (jgFbm3o(jgWp / 5.0 + 11.0) - 0.5) * 0.34;
84
+ jgCol *= 1.0 + (jgVNoise(jgWp * 16.0) - 0.5) * 0.26 * jgBand(16.0);
85
+ vec3 jgCell = jgVoronoi(jgWp * 0.32 + 5.1);
86
+ float jgCrack = (1.0 - smoothstep(0.0, 0.03, jgCell.y)) * smoothstep(0.45, 0.6, jgVNoise(jgWp * 0.45 + 3.3)) * jgBand(2.0);
87
+ jgCol *= 1.0 - 0.4 * jgCrack;
88
+ jgRough += 0.05 * jgCrack;
89
+ jgBumpH -= jgCrack * 0.004;
90
+ }
91
+ #endif
92
+ #ifdef JG_RIBBON_WEAR
93
+ {
94
+ // Two wheel tracks a cart-axle apart, wandering together along the ribbon.
95
+ float jgWob = (jgVNoise(vec2(vJgRibbon.x * 0.09, 4.7)) - 0.5) * uJgHalfWidth * 0.35;
96
+ float jgRutD = abs(abs(jgAcross - jgWob) - uJgHalfWidth * 0.45);
97
+ float jgRutW = max(0.15, uJgHalfWidth * 0.14);
98
+ float jgRut = exp(-(jgRutD * jgRutD) / (jgRutW * jgRutW)) * (0.55 + 0.45 * jgVNoise(vec2(vJgRibbon.x * 0.4, 9.1))) * uJgWear;
99
+ #ifdef JG_RIBBON_PAVED
100
+ jgCol *= 1.0 - 0.22 * jgRut;
101
+ jgRough -= 0.16 * jgRut;
102
+ #else
103
+ // Trodden band: packed flat, fewer loose grains and a little dustier, so the track reads against
104
+ // painted dirt of the same hue; then darker ruts and loose soil heaped toward the shoulders.
105
+ float jgTrod = (1.0 - smoothstep(uJgHalfWidth * 0.5, uJgHalfWidth * 0.95, abs(jgAcross))) * uJgWear;
106
+ jgCol = mix(jgCol, diffuseColor.rgb * (0.96 + 0.12 * jgFbm2(jgWp * 0.7 + 2.9)), 0.5 * jgTrod);
107
+ jgCol *= 1.0 + 0.08 * jgTrod;
108
+ jgBumpH *= 1.0 - 0.6 * jgTrod;
109
+ jgCol *= 1.0 - 0.32 * jgRut;
110
+ jgRough -= 0.12 * jgRut;
111
+ jgBumpH -= jgRut * 0.03;
112
+ float jgShoulder = smoothstep(uJgHalfWidth * 0.6, uJgHalfWidth, abs(jgAcross)) * uJgWear;
113
+ jgCol *= 1.0 - 0.16 * jgShoulder;
114
+ jgBumpH += jgShoulder * 0.014;
115
+ #endif
116
+ }
117
+ #endif
118
+ #ifdef JG_RIBBON_EDGE
119
+ {
120
+ float jgEdgeN = (jgFbm2(jgWp * 0.55 + 7.3) - 0.5) * 2.0 + (jgVNoise(jgWp * 2.3 + 1.7) - 0.5) * 0.6;
121
+ float jgEdge = uJgHalfWidth + jgEdgeN * uJgWarp;
122
+ float jgAlpha = 1.0 - smoothstep(jgEdge - uJgFeather * 0.5, jgEdge + uJgFeather * 0.5, abs(jgAcross));
123
+ jgAlpha *= smoothstep(0.0, uJgFeather, min(vJgRibbon.x, vJgRibbon.z) + jgEdgeN * uJgWarp);
124
+ // Taller grains (pebbles, clods) hold on longer, so the edge breaks up instead of fading evenly.
125
+ jgAlpha = clamp(jgAlpha + (jgS.height - 0.4) * 2.4 * jgAlpha * (1.0 - jgAlpha), 0.0, 1.0);
126
+ diffuseColor.a *= jgAlpha;
127
+ jgBumpH *= jgAlpha;
128
+ }
129
+ #endif
130
+ #ifdef USE_COLOR_ALPHA
131
+ {
132
+ // A per-vertex fade interpolated across big triangles draws straight iso-lines; warp it with noise
133
+ // so it breaks up like painted terrain. Fully opaque or fully clear vertices are left alone.
134
+ float jgA = diffuseColor.a;
135
+ float jgMid = min(jgA, 1.0 - jgA);
136
+ float jgFadeN = jgFbm2(jgWp * 0.35 + 21.7) - 0.5 + (jgVNoise(jgWp * 1.7 + 5.3) - 0.5) * 0.4;
137
+ diffuseColor.a = clamp(jgA + jgFadeN * min(1.0, jgMid * 4.0) * 1.1, 0.0, 1.0);
138
+ diffuseColor.a = smoothstep(0.08, 0.92, diffuseColor.a);
139
+ }
140
+ #endif
141
+ diffuseColor.rgb = jgCol;
142
+ `;
143
+ /**
144
+ * A `MeshStandardMaterial` that shades a ground ribbon (path, road, sidewalk) with the terrain's own
145
+ * procedural surface functions, so a dirt track or asphalt strip matches painted ground: the
146
+ * authored colour varied by the surface class's grain, roughness and micro-normal, wheel ruts or tyre
147
+ * lanes, and (with `feather > 0`) a noise-warped edge that fades into the terrain. The geometry needs
148
+ * a {@link RIBBON_FRAME_ATTRIBUTE} attribute from {@link ribbonSurfaceFrame}; build it wider by
149
+ * {@link ribbonEdgeMargin} so the fade straddles the authored edge. The caller disposes the material.
150
+ */
151
+ export function createSurfaceRibbonMaterial(options) {
152
+ const cls = terrainSurfaceClass(options.surface ?? "road");
153
+ const classIndex = terrainSurfaceClassIndex(cls);
154
+ const low = options.quality === "low";
155
+ const feather = Math.max(0, options.feather ?? 0);
156
+ const edge = feather > 0;
157
+ const wear = clamp(options.wear ?? 0, 0, 1);
158
+ const paved = cls === "road" || cls === "rock";
159
+ const roughness = options.roughness ?? terrainSurfaceRoughness(cls);
160
+ const material = new THREE.MeshStandardMaterial({
161
+ color: options.color,
162
+ roughness,
163
+ metalness: 0,
164
+ side: THREE.DoubleSide,
165
+ transparent: edge,
166
+ vertexColors: options.vertexColors === true,
167
+ ...(options.polygonOffset === true ? { polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 } : {}),
168
+ });
169
+ const uniforms = {
170
+ uJgRough: { value: roughness },
171
+ uJgScale: { value: options.scale ?? 1 },
172
+ uJgHalfWidth: { value: Math.max(0.01, options.halfWidth) },
173
+ uJgFeather: { value: Math.max(1e-3, feather) },
174
+ uJgWarp: { value: Math.max(0, options.warp ?? 0) },
175
+ uJgWear: { value: wear },
176
+ };
177
+ material.defines = {
178
+ JG_RIBBON_CLASS: String(classIndex),
179
+ JG_SURFACE_CLASSES: String(1 << classIndex),
180
+ ...(low ? { JG_SURFACE_LOW: "" } : {}),
181
+ ...(edge ? { JG_RIBBON_EDGE: "" } : {}),
182
+ ...(wear > 0 ? { JG_RIBBON_WEAR: "" } : {}),
183
+ ...(paved ? { JG_RIBBON_PAVED: "" } : {}),
184
+ };
185
+ material.onBeforeCompile = (shader) => {
186
+ Object.assign(shader.uniforms, uniforms);
187
+ shader.vertexShader = shader.vertexShader
188
+ .replace("#include <common>", `#include <common>
189
+ attribute vec3 ${RIBBON_FRAME_ATTRIBUTE};
190
+ varying vec3 vJgWorldPos;
191
+ varying vec3 vJgRibbon;`)
192
+ .replace("#include <begin_vertex>", `#include <begin_vertex>
193
+ vJgWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;
194
+ vJgRibbon = ${RIBBON_FRAME_ATTRIBUTE};`);
195
+ shader.fragmentShader = shader.fragmentShader
196
+ .replace("#include <common>", `#include <common>
197
+ varying vec3 vJgWorldPos;
198
+ varying vec3 vJgRibbon;
199
+ uniform float uJgRough;
200
+ uniform float uJgScale;
201
+ uniform float uJgHalfWidth;
202
+ uniform float uJgFeather;
203
+ uniform float uJgWarp;
204
+ uniform float uJgWear;
205
+ ${NOISE_GLSL}
206
+ ${SURFACE_FUNCTIONS_GLSL}`)
207
+ .replace("#include <color_fragment>", `#include <color_fragment>\n${RIBBON_FRAGMENT_GLSL}`)
208
+ .replace("#include <roughnessmap_fragment>", `#include <roughnessmap_fragment>
209
+ roughnessFactor = clamp(jgRough, 0.04, 1.0);`)
210
+ .replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
211
+ ${SURFACE_BUMP_NORMAL_GLSL}`);
212
+ };
213
+ material.customProgramCacheKey = () => `jgengine-surface-ribbon-${classIndex}-${low ? "low" : "full"}-${edge ? "edge" : "hard"}-${wear > 0 ? "wear" : "clean"}`;
214
+ return material;
215
+ }
216
+ /**
217
+ * {@link createSurfaceRibbonMaterial} for a React mesh: rebuilt only when an option changes and
218
+ * disposed on unmount. Pass `null` to get `null` (the caller falls back to a flat material).
219
+ */
220
+ export function useSurfaceRibbonMaterial(options) {
221
+ const o = options;
222
+ const material = useMemo(() => (o === null ? null : createSurfaceRibbonMaterial(o)),
223
+ // eslint-disable-next-line react-hooks/exhaustive-deps
224
+ [o === null, o?.color, o?.halfWidth, o?.surface, o?.feather, o?.warp, o?.wear, o?.roughness, o?.scale, o?.quality, o?.polygonOffset, o?.vertexColors]);
225
+ useEffect(() => () => material?.dispose(), [material]);
226
+ return material;
227
+ }
@@ -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;