@jgengine/shell 0.11.0 → 0.12.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 (130) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/CombatPresentation.d.ts +5 -0
  3. package/dist/CombatPresentation.js +8 -0
  4. package/dist/GameHost.d.ts +25 -1
  5. package/dist/GameHost.js +60 -4
  6. package/dist/GamePlayer.d.ts +5 -0
  7. package/dist/GamePlayer.js +5 -0
  8. package/dist/GamePlayerShell.d.ts +5 -0
  9. package/dist/GamePlayerShell.js +48 -358
  10. package/dist/GameUiPreview.js +3 -0
  11. package/dist/Shell3dPresentation.d.ts +63 -0
  12. package/dist/Shell3dPresentation.js +302 -0
  13. package/dist/ShellChrome.d.ts +51 -0
  14. package/dist/ShellChrome.js +51 -0
  15. package/dist/ShellHudPresentation.d.ts +41 -0
  16. package/dist/ShellHudPresentation.js +22 -0
  17. package/dist/audio/audioEngine.d.ts +4 -0
  18. package/dist/audio/audioEngine.js +111 -26
  19. package/dist/audio/audioWire.d.ts +22 -4
  20. package/dist/audio/audioWire.js +46 -3
  21. package/dist/audio/loopParams.d.ts +20 -0
  22. package/dist/audio/loopParams.js +28 -0
  23. package/dist/camera/GameCameraRig.js +6 -1
  24. package/dist/camera/cameraRigs.d.ts +3 -1
  25. package/dist/camera/cameraRigs.js +21 -3
  26. package/dist/camera/rigMath.d.ts +20 -0
  27. package/dist/camera/rigMath.js +29 -0
  28. package/dist/defineGame.d.ts +15 -1
  29. package/dist/defineGame.js +26 -4
  30. package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
  31. package/dist/devtools/DevtoolsOverlay.js +28 -12
  32. package/dist/devtools/agentBridge.js +4 -0
  33. package/dist/devtools/perfDiagnose.js +1 -1
  34. package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
  35. package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
  36. package/dist/environment/EnvironmentScene.js +12 -3
  37. package/dist/environment/terrainGroundColor.d.ts +9 -0
  38. package/dist/environment/terrainGroundColor.js +22 -0
  39. package/dist/gameKit.d.ts +27 -0
  40. package/dist/gameKit.js +29 -0
  41. package/dist/map/MapMarkerBeacons.d.ts +3 -2
  42. package/dist/postfx/PostProcessing.d.ts +9 -1
  43. package/dist/postfx/PostProcessing.js +11 -4
  44. package/dist/presentationResolve.d.ts +35 -0
  45. package/dist/presentationResolve.js +51 -0
  46. package/dist/registry.d.ts +1 -0
  47. package/dist/render/assetBase.d.ts +17 -0
  48. package/dist/render/assetBase.js +35 -0
  49. package/dist/render/modelLoad.js +4 -1
  50. package/dist/render/resolveModel.d.ts +15 -0
  51. package/dist/render/resolveModel.js +37 -2
  52. package/dist/scatter/InstancedScatter.js +5 -0
  53. package/dist/scatter/scatterProxies.js +24 -0
  54. package/dist/scene/AuthoredScene.d.ts +3 -1
  55. package/dist/scene/AuthoredScene.js +7 -2
  56. package/dist/scene/CityRenderer.d.ts +2 -0
  57. package/dist/scene/CityRenderer.js +1031 -0
  58. package/dist/scene/GrassFieldRenderer.js +53 -5
  59. package/dist/scene/PoleLineRenderer.d.ts +2 -0
  60. package/dist/scene/PoleLineRenderer.js +287 -0
  61. package/dist/scene/SoilPatchRenderer.js +70 -7
  62. package/dist/scene/WaterRenderer.js +34 -10
  63. package/dist/scene/builtinSceneKindRenderers.js +4 -0
  64. package/dist/scene/cityWindowMaterial.d.ts +16 -0
  65. package/dist/scene/cityWindowMaterial.js +81 -0
  66. package/dist/scene/sceneKindRenderers.d.ts +2 -0
  67. package/dist/settings/appliedSettings.d.ts +2 -1
  68. package/dist/settings/appliedSettings.js +4 -2
  69. package/dist/structures/TransformGizmo.d.ts +11 -1
  70. package/dist/structures/TransformGizmo.js +9 -5
  71. package/dist/terrain/GrassField.d.ts +11 -3
  72. package/dist/terrain/GrassField.js +16 -6
  73. package/dist/terrain/grassGeometry.d.ts +33 -1
  74. package/dist/terrain/grassGeometry.js +83 -21
  75. package/dist/terrain/grassMaterial.d.ts +18 -0
  76. package/dist/terrain/grassMaterial.js +66 -13
  77. package/dist/terrain/heightfieldGeometry.d.ts +34 -0
  78. package/dist/terrain/heightfieldGeometry.js +109 -0
  79. package/dist/terrain/index.d.ts +1 -0
  80. package/dist/terrain/index.js +1 -0
  81. package/dist/terrain/soilPatchMaterial.d.ts +15 -2
  82. package/dist/terrain/soilPatchMaterial.js +46 -8
  83. package/dist/terrain/terrainDetailMaterial.js +15 -2
  84. package/dist/water/Ocean.d.ts +7 -1
  85. package/dist/water/Ocean.js +16 -3
  86. package/dist/water/OceanConfig.d.ts +9 -0
  87. package/dist/water/OceanConfig.js +6 -0
  88. package/dist/water/OceanMaterial.d.ts +12 -0
  89. package/dist/water/OceanMaterial.js +9 -0
  90. package/dist/water/OceanShader.d.ts +2 -2
  91. package/dist/water/OceanShader.js +78 -10
  92. package/dist/world/PlaceScene.d.ts +11 -0
  93. package/dist/world/PlaceScene.js +27 -0
  94. package/dist/world/WorldScene.js +29 -2
  95. package/dist/worldSky.d.ts +7 -1
  96. package/dist/worldSky.js +11 -1
  97. package/package.json +4 -4
  98. package/skills/game-design/SKILL.md +28 -0
  99. package/skills/game-design/agents/openai.yaml +4 -0
  100. package/skills/game-design/references/application-playbook.md +170 -0
  101. package/skills/game-design/references/field-guide.md +288 -0
  102. package/skills/jgengine/SKILL.md +7 -4
  103. package/skills/jgengine/api.md +172 -79
  104. package/skills/jgengine/capabilities.md +104 -2
  105. package/skills/jgengine/recipes/minimal-game.md +88 -0
  106. package/skills/jgengine-ui/SKILL.md +15 -10
  107. package/skills/jgengine-ui/api.md +202 -47
  108. package/skills/jgengine-ui/capabilities.md +71 -2
  109. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  110. package/skills/jgengine-ui/reference.md +20 -7
  111. package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
  112. package/skills/jgengine-world/SKILL.md +13 -0
  113. package/skills/jgengine-world/api.md +419 -15
  114. package/skills/jgengine-world/capabilities.md +108 -2
  115. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  116. package/skills/jgengine-world/reference.md +43 -4
  117. package/skills/level-design/SKILL.md +54 -0
  118. package/skills/level-design/agents/openai.yaml +4 -0
  119. package/skills/level-design/references/application-playbook.md +192 -0
  120. package/skills/level-design/references/field-guide.md +337 -0
  121. package/skills/level-design/references/live-editor-loop.md +179 -0
  122. package/dist/cartridge/runtime.d.ts +0 -15
  123. package/dist/cartridge/runtime.js +0 -556
  124. package/dist/cartridge/spec.d.ts +0 -188
  125. package/dist/cartridge/spec.js +0 -19
  126. package/dist/cartridge/validate.d.ts +0 -2
  127. package/dist/cartridge/validate.js +0 -78
  128. package/dist/cartridge.d.ts +0 -145
  129. package/dist/cartridge.js +0 -246
  130. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -1,5 +1,5 @@
1
1
  import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
2
- import { type SettingsStore } from "@jgengine/core/settings/settingsModel";
2
+ import { type GraphicsQuality, type SettingsStore } from "@jgengine/core/settings/settingsModel";
3
3
  import { type TouchStyle } from "@jgengine/core/input/touchScheme";
4
4
  import type { AudioEngine } from "../audio/audioEngine.js";
5
5
  /** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
@@ -11,6 +11,7 @@ export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault
11
11
  shadows: boolean;
12
12
  dpr: number;
13
13
  uiScale: number;
14
+ quality: GraphicsQuality;
14
15
  };
15
16
  export declare function AudioSettingsBridge({ store, engine, buses, }: {
16
17
  store: SettingsStore;
@@ -16,12 +16,14 @@ export function useSettingsRevision(store) {
16
16
  }
17
17
  export function useGraphicsSettings(store, shadowsDefault) {
18
18
  useSettingsRevision(store);
19
- const quality = store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY);
19
+ const rawQuality = store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY);
20
+ const quality = GRAPHICS_QUALITY_DPR[rawQuality] !== undefined ? rawQuality : "high";
20
21
  const rawUiScale = store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE);
21
22
  return {
22
23
  shadows: store.get(SETTING_IDS.graphicsShadows, shadowsDefault),
23
- dpr: GRAPHICS_QUALITY_DPR[quality] ?? GRAPHICS_QUALITY_DPR.high,
24
+ dpr: GRAPHICS_QUALITY_DPR[quality],
24
25
  uiScale: Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, rawUiScale)),
26
+ quality,
25
27
  };
26
28
  }
27
29
  export function AudioSettingsBridge({ store, engine, buses, }) {
@@ -2,6 +2,10 @@
2
2
  export type TransformGizmoMode = "translate" | "rotate" | "scale";
3
3
  /** Snap policy: grid quantize, free drag, or ground-height sample on release. */
4
4
  export type TransformGizmoSnap = "grid" | "free" | "ground";
5
+ /** Gizmo handle orientation: world axes, or the target's local (yaw-rotated) axes. */
6
+ export type TransformGizmoSpace = "world" | "local";
7
+ /** Default rotation snap increment (radians) when callers don't configure one. */
8
+ export declare const DEFAULT_ROTATION_SNAP: number;
5
9
  /** Pose reported by {@link TransformGizmo} when the user releases a drag. */
6
10
  export interface TransformGizmoPose {
7
11
  position: {
@@ -27,6 +31,12 @@ export interface TransformGizmoProps {
27
31
  mode?: TransformGizmoMode;
28
32
  snapMode?: TransformGizmoSnap;
29
33
  gridSize?: number;
34
+ /** Handle orientation; `"local"` aligns handles to the target's yaw. */
35
+ space?: TransformGizmoSpace;
36
+ /** Rotation snap increment in radians; `null` rotates freely. */
37
+ rotationSnap?: number | null;
38
+ /** Scale snap increment; `null` (default) scales freely. */
39
+ scaleSnap?: number | null;
30
40
  /** Vertical offset so the gizmo sits above ground markers. */
31
41
  lift?: number;
32
42
  size?: number;
@@ -40,4 +50,4 @@ export interface TransformGizmoProps {
40
50
  * Wraps TransformControls; callers own selection and commit side-effects.
41
51
  * @capability transform-gizmo in-game select/move/rotate/scale gizmo
42
52
  */
43
- export declare const TransformGizmo: import("react").MemoExoticComponent<({ position, rotationY, mode, snapMode, gridSize, lift, size, groundSnap, onDraggingChange, onRelease, }: TransformGizmoProps) => import("react").JSX.Element>;
53
+ export declare const TransformGizmo: import("react").MemoExoticComponent<({ position, rotationY, mode, snapMode, gridSize, space, rotationSnap, scaleSnap, lift, size, groundSnap, onDraggingChange, onRelease, }: TransformGizmoProps) => import("react").JSX.Element>;
@@ -2,12 +2,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { TransformControls } from "@react-three/drei";
3
3
  import { useThree } from "@react-three/fiber";
4
4
  import { memo, useEffect, useRef, useState } from "react";
5
+ /** Default rotation snap increment (radians) when callers don't configure one. */
6
+ export const DEFAULT_ROTATION_SNAP = Math.PI / 12;
5
7
  /**
6
8
  * Runtime selection/move gizmo shared by games and the editor.
7
9
  * Wraps TransformControls; callers own selection and commit side-effects.
8
10
  * @capability transform-gizmo in-game select/move/rotate/scale gizmo
9
11
  */
10
- export const TransformGizmo = memo(function TransformGizmo({ position, rotationY = 0, mode = "translate", snapMode = "free", gridSize = 1, lift = 0, size = 0.85, groundSnap, onDraggingChange, onRelease, }) {
12
+ export const TransformGizmo = memo(function TransformGizmo({ position, rotationY = 0, mode = "translate", snapMode = "free", gridSize = 1, space = "world", rotationSnap = DEFAULT_ROTATION_SNAP, scaleSnap = null, lift = 0, size = 0.85, groundSnap, onDraggingChange, onRelease, }) {
11
13
  const groupRef = useRef(null);
12
14
  const [object, setObject] = useState(null);
13
15
  const draggingRef = useRef(false);
@@ -24,9 +26,11 @@ export const TransformGizmo = memo(function TransformGizmo({ position, rotationY
24
26
  group.rotation.set(0, rotationY, 0);
25
27
  group.scale.set(1, 1, 1);
26
28
  }, [position.x, position.y, position.z, rotationY, lift, anchorKey, mode]);
27
- const snapProps = snapMode === "grid"
28
- ? { translationSnap: gridSize, rotationSnap: Math.PI / 12 }
29
- : { rotationSnap: Math.PI / 12 };
29
+ const snapProps = {
30
+ translationSnap: snapMode === "grid" ? gridSize : null,
31
+ rotationSnap,
32
+ scaleSnap,
33
+ };
30
34
  const handleRelease = () => {
31
35
  draggingRef.current = false;
32
36
  if (controls !== null && "enabled" in controls)
@@ -46,7 +50,7 @@ export const TransformGizmo = memo(function TransformGizmo({ position, rotationY
46
50
  scale: { x: current.scale.x, y: current.scale.y, z: current.scale.z },
47
51
  });
48
52
  };
49
- return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef }), object !== null ? (_jsx(TransformControls, { object: object, mode: mode, size: size, ...snapProps, onMouseDown: () => {
53
+ return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef }), object !== null ? (_jsx(TransformControls, { object: object, mode: mode, size: size, space: space, ...snapProps, onMouseDown: () => {
50
54
  draggingRef.current = true;
51
55
  if (controls !== null && "enabled" in controls)
52
56
  controls.enabled = false;
@@ -1,8 +1,9 @@
1
1
  import { type ThreeElements } from "@react-three/fiber";
2
- import { type GrassBladeGeometryOptions, type GrassRange } from "./grassGeometry.js";
3
- import { type GrassMaterialOptions, type GrassWindOptions } from "./grassMaterial.js";
2
+ import { type GrassBladeGeometryOptions, type GrassExclusion, type GrassRange } from "./grassGeometry.js";
3
+ import { type GrassDistanceFadeOptions, type GrassMaterialOptions, type GrassWindOptions } from "./grassMaterial.js";
4
4
  import type { TerrainArea, TerrainHeightSampler } from "./terrainMath.js";
5
5
  export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
6
+ export { GRASS_TUFT_BLADES } from "./grassGeometry.js";
6
7
  export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
7
8
  count?: number;
8
9
  density?: number;
@@ -13,11 +14,18 @@ export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "c
13
14
  bladeHeight?: GrassRange;
14
15
  bladeWidth?: GrassRange;
15
16
  bladeBend?: GrassRange;
17
+ tuftBlades?: number;
18
+ tuftRadius?: number;
19
+ edgeFeather?: number;
20
+ exclude?: readonly GrassExclusion[];
16
21
  heightAt?: TerrainHeightSampler;
17
22
  colorBase?: GrassMaterialOptions["colorBase"];
18
23
  colorTip?: GrassMaterialOptions["colorTip"];
24
+ colorGround?: GrassMaterialOptions["colorGround"];
19
25
  colorVariation?: number;
20
26
  wind?: GrassWindOptions | false;
27
+ distanceFade?: GrassDistanceFadeOptions | false;
28
+ normalLift?: number;
21
29
  roughness?: number;
22
30
  }
23
- export declare function GrassField({ count, density, budget, area, seed, segments, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow, receiveShadow, frustumCulled, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
31
+ 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, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useFrame } from "@react-three/fiber";
3
3
  import { useEffect, useMemo } from "react";
4
- import { createGrassBladeGeometry } from "./grassGeometry.js";
5
- import { createGrassMaterial } from "./grassMaterial.js";
4
+ import { createGrassBladeGeometry, grassTuftCount, GRASS_TUFT_BLADES, } from "./grassGeometry.js";
5
+ import { createGrassMaterial, } from "./grassMaterial.js";
6
6
  import { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget, } from "./grassBudget.js";
7
7
  export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
8
- export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, ...meshProps }) {
8
+ export { GRASS_TUFT_BLADES } from "./grassGeometry.js";
9
+ 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, ...meshProps }) {
9
10
  const geometry = useMemo(() => createGrassBladeGeometry({
10
11
  count,
11
12
  area,
@@ -14,16 +15,25 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
14
15
  height: bladeHeight,
15
16
  width: bladeWidth,
16
17
  bend: bladeBend,
18
+ ...(tuftBlades === undefined ? {} : { tuftBlades }),
19
+ ...(tuftRadius === undefined ? {} : { tuftRadius }),
20
+ ...(edgeFeather === undefined ? {} : { edgeFeather }),
21
+ ...(exclude === undefined ? {} : { exclude }),
17
22
  heightAt,
18
- }), [area, bladeBend, bladeHeight, bladeWidth, count, heightAt, seed, segments]);
23
+ }), [area, bladeBend, bladeHeight, bladeWidth, count, edgeFeather, exclude, heightAt, seed, segments, tuftBlades, tuftRadius]);
19
24
  const handle = useMemo(() => createGrassMaterial({
20
25
  colorBase,
21
26
  colorTip,
27
+ colorGround,
22
28
  colorVariation,
23
29
  wind,
30
+ distanceFade,
31
+ normalLift,
24
32
  roughness,
25
- }), [colorBase, colorTip, colorVariation, roughness, wind]);
26
- geometry.instanceCount = resolveGrassInstanceBudget(count, density, area, budget);
33
+ }), [colorBase, colorTip, colorGround, colorVariation, distanceFade, normalLift, roughness, wind]);
34
+ // Budgets stay in blades (the public unit); the instance buffer carries tufts.
35
+ const instanceCount = useMemo(() => grassTuftCount(resolveGrassInstanceBudget(count, density, area, budget), tuftBlades), [count, density, area, budget, tuftBlades]);
36
+ geometry.instanceCount = Math.min(instanceCount, grassTuftCount(count, tuftBlades));
27
37
  useFrame((state) => {
28
38
  handle.uniforms.uTime.value = state.clock.elapsedTime;
29
39
  });
@@ -2,7 +2,19 @@ import * as THREE from "three";
2
2
  import { type TerrainSeed } from "./random.js";
3
3
  import { type TerrainArea, type TerrainHeightSampler } from "./terrainMath.js";
4
4
  export type GrassRange = number | readonly [min: number, max: number];
5
+ /** Blades baked into each tuft instance — one instance reads as a clump, not a lone blade. */
6
+ export declare const GRASS_TUFT_BLADES = 5;
7
+ /** A rectangle (patch-local XZ) the grass bake keeps clear — soil patches, water, plazas. */
8
+ export interface GrassExclusion {
9
+ /** Rect center in patch-local coordinates. */
10
+ center: readonly [x: number, z: number];
11
+ /** Rect half extents. */
12
+ half: readonly [x: number, z: number];
13
+ /** Ramp width (m) outside the rect over which blades regrow to full height. Default 1.5. */
14
+ feather?: number;
15
+ }
5
16
  export interface GrassBladeGeometryOptions {
17
+ /** Total blade budget for the patch (instances are tufts of `tuftBlades` blades each). */
6
18
  count?: number;
7
19
  area?: TerrainArea;
8
20
  seed?: TerrainSeed;
@@ -10,6 +22,14 @@ export interface GrassBladeGeometryOptions {
10
22
  height?: GrassRange;
11
23
  width?: GrassRange;
12
24
  bend?: GrassRange;
25
+ /** Blades merged into each instanced tuft. Default {@link GRASS_TUFT_BLADES}. */
26
+ tuftBlades?: number;
27
+ /** Tuft spread radius in meters — how far clump blades splay from the tuft root. Default 0.16. */
28
+ tuftRadius?: number;
29
+ /** Meters of density/height feathering at the patch border so the field ends in a soft fringe, not a mowed rectangle. Default 1.5; 0 disables. */
30
+ edgeFeather?: number;
31
+ /** Patch-local rectangles kept clear of blades (soil patches, water) with a soft regrow ramp. */
32
+ exclude?: readonly GrassExclusion[];
13
33
  heightAt?: TerrainHeightSampler;
14
34
  }
15
35
  export interface ResolvedGrassBladeGeometryOptions {
@@ -20,11 +40,23 @@ export interface ResolvedGrassBladeGeometryOptions {
20
40
  height: readonly [min: number, max: number];
21
41
  width: readonly [min: number, max: number];
22
42
  bend: readonly [min: number, max: number];
43
+ tuftBlades: number;
44
+ tuftRadius: number;
45
+ edgeFeather: number;
46
+ exclude: readonly GrassExclusion[];
23
47
  heightAt: TerrainHeightSampler;
24
48
  }
25
49
  /** @internal */
26
50
  export declare function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number];
27
51
  /** @internal */
28
52
  export declare function resolveGrassBladeGeometryOptions(options?: GrassBladeGeometryOptions): ResolvedGrassBladeGeometryOptions;
29
- /** @internal */
53
+ /** Tuft instances needed to carry `bladeCount` blades at `tuftBlades` blades per tuft. @internal */
54
+ export declare function grassTuftCount(bladeCount: number, tuftBlades?: number): number;
55
+ /**
56
+ * Instanced tuft geometry: the template mesh is a clump of `tuftBlades` blades splayed around the
57
+ * tuft root (per-blade yaw/offset/scale baked as vertex attributes), and each instance places one
58
+ * whole clump. Clumps read as turf at a fraction of the instance count single blades need, and the
59
+ * per-instance yaw/scale jitter hides the shared clump layout.
60
+ * @internal
61
+ */
30
62
  export declare function createGrassBladeGeometry(options?: GrassBladeGeometryOptions): THREE.InstancedBufferGeometry;
@@ -1,6 +1,8 @@
1
1
  import * as THREE from "three";
2
2
  import { createSeededRandom } from "./random.js";
3
3
  import { resolveTerrainSize } from "./terrainMath.js";
4
+ /** Blades baked into each tuft instance — one instance reads as a clump, not a lone blade. */
5
+ export const GRASS_TUFT_BLADES = 5;
4
6
  /** @internal */
5
7
  export function resolveGrassRange(value, fallback) {
6
8
  if (value === undefined)
@@ -15,53 +17,113 @@ export function resolveGrassBladeGeometryOptions(options = {}) {
15
17
  seed: options.seed ?? 1,
16
18
  segments: Math.max(1, Math.floor(options.segments ?? 4)),
17
19
  height: resolveGrassRange(options.height, [0.55, 1.1]),
18
- width: resolveGrassRange(options.width, [0.035, 0.075]),
19
- bend: resolveGrassRange(options.bend, [0.08, 0.32]),
20
+ width: resolveGrassRange(options.width, [0.05, 0.1]),
21
+ bend: resolveGrassRange(options.bend, [0.12, 0.42]),
22
+ tuftBlades: Math.max(1, Math.floor(options.tuftBlades ?? GRASS_TUFT_BLADES)),
23
+ tuftRadius: Math.max(0, options.tuftRadius ?? 0.16),
24
+ edgeFeather: Math.max(0, options.edgeFeather ?? 1.5),
25
+ exclude: options.exclude ?? [],
20
26
  heightAt: options.heightAt ?? (() => 0),
21
27
  };
22
28
  }
29
+ /** Tuft instances needed to carry `bladeCount` blades at `tuftBlades` blades per tuft. @internal */
30
+ export function grassTuftCount(bladeCount, tuftBlades = GRASS_TUFT_BLADES) {
31
+ return Math.ceil(Math.max(0, bladeCount) / Math.max(1, tuftBlades));
32
+ }
23
33
  function randomRange(random, range) {
24
34
  return THREE.MathUtils.lerp(range[0], range[1], random());
25
35
  }
26
- /** @internal */
36
+ /**
37
+ * Instanced tuft geometry: the template mesh is a clump of `tuftBlades` blades splayed around the
38
+ * tuft root (per-blade yaw/offset/scale baked as vertex attributes), and each instance places one
39
+ * whole clump. Clumps read as turf at a fraction of the instance count single blades need, and the
40
+ * per-instance yaw/scale jitter hides the shared clump layout.
41
+ * @internal
42
+ */
27
43
  export function createGrassBladeGeometry(options = {}) {
28
44
  const resolved = resolveGrassBladeGeometryOptions(options);
29
45
  const size = resolveTerrainSize(resolved.area);
30
46
  const positions = [];
31
47
  const normals = [];
32
48
  const uvs = [];
49
+ const bladeOffsets = [];
50
+ const bladeVary = [];
51
+ const bladeBendScale = [];
33
52
  const indices = [];
34
- for (let segment = 0; segment <= resolved.segments; segment += 1) {
35
- const t = segment / resolved.segments;
36
- positions.push(-0.5, t, 0, 0.5, t, 0);
37
- normals.push(0, 0, 1, 0, 0, 1);
38
- uvs.push(0, t, 1, t);
39
- if (segment < resolved.segments) {
40
- const base = segment * 2;
41
- indices.push(base, base + 1, base + 3, base, base + 3, base + 2);
53
+ // Blade layout inside the tuft is baked once from the seed, shared by every instance.
54
+ const templateRandom = createSeededRandom(typeof resolved.seed === "number" ? resolved.seed + 7919 : `${resolved.seed}/tuft`);
55
+ const vertsPerBlade = (resolved.segments + 1) * 2;
56
+ for (let blade = 0; blade < resolved.tuftBlades; blade += 1) {
57
+ // First blade anchors the root; the rest ring outward with jittered angle/radius.
58
+ const angle = blade === 0 ? 0 : ((blade - 1) / Math.max(1, resolved.tuftBlades - 1)) * Math.PI * 2 + templateRandom() * 1.4;
59
+ const radius = blade === 0 ? 0 : resolved.tuftRadius * (0.35 + 0.65 * templateRandom());
60
+ const offsetX = Math.cos(angle) * radius;
61
+ const offsetZ = Math.sin(angle) * radius;
62
+ // Bend direction points away from the tuft center (plus jitter) so the clump splays open.
63
+ const yaw = angle + Math.PI / 2 + (templateRandom() - 0.5) * 1.6;
64
+ const heightScale = 0.72 + templateRandom() * 0.45;
65
+ const widthScale = 0.8 + templateRandom() * 0.4;
66
+ const phase = templateRandom();
67
+ const bendScale = 0.7 + templateRandom() * 0.7;
68
+ for (let segment = 0; segment <= resolved.segments; segment += 1) {
69
+ const t = segment / resolved.segments;
70
+ positions.push(-0.5, t, 0, 0.5, t, 0);
71
+ normals.push(0, 0, 1, 0, 0, 1);
72
+ uvs.push(0, t, 1, t);
73
+ for (let corner = 0; corner < 2; corner += 1) {
74
+ bladeOffsets.push(offsetX, offsetZ);
75
+ bladeVary.push(yaw, heightScale, widthScale, phase);
76
+ bladeBendScale.push(bendScale);
77
+ }
78
+ if (segment < resolved.segments) {
79
+ const base = blade * vertsPerBlade + segment * 2;
80
+ indices.push(base, base + 1, base + 3, base, base + 3, base + 2);
81
+ }
42
82
  }
43
83
  }
44
84
  const geometry = new THREE.InstancedBufferGeometry();
45
85
  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
46
86
  geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
47
87
  geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
88
+ geometry.setAttribute("bladeOffset", new THREE.Float32BufferAttribute(bladeOffsets, 2));
89
+ geometry.setAttribute("bladeVary", new THREE.Float32BufferAttribute(bladeVary, 4));
90
+ geometry.setAttribute("bladeBendScale", new THREE.Float32BufferAttribute(bladeBendScale, 1));
48
91
  geometry.setIndex(indices);
92
+ const tuftCount = grassTuftCount(resolved.count, resolved.tuftBlades);
49
93
  const random = createSeededRandom(resolved.seed);
50
- const offsets = new Float32Array(resolved.count * 3);
51
- const yaw = new Float32Array(resolved.count);
52
- const heights = new Float32Array(resolved.count);
53
- const widths = new Float32Array(resolved.count);
54
- const bend = new Float32Array(resolved.count);
55
- const phase = new Float32Array(resolved.count);
56
- const colorMix = new Float32Array(resolved.count);
57
- for (let index = 0; index < resolved.count; index += 1) {
94
+ const offsets = new Float32Array(tuftCount * 3);
95
+ const yaw = new Float32Array(tuftCount);
96
+ const heights = new Float32Array(tuftCount);
97
+ const widths = new Float32Array(tuftCount);
98
+ const bend = new Float32Array(tuftCount);
99
+ const phase = new Float32Array(tuftCount);
100
+ const colorMix = new Float32Array(tuftCount);
101
+ const halfW = size.width / 2;
102
+ const halfD = size.depth / 2;
103
+ for (let index = 0; index < tuftCount; index += 1) {
58
104
  const x = (random() - 0.5) * size.width;
59
105
  const z = (random() - 0.5) * size.depth;
60
106
  offsets[index * 3] = x;
61
107
  offsets[index * 3 + 1] = resolved.heightAt(x, z);
62
108
  offsets[index * 3 + 2] = z;
63
109
  yaw[index] = random() * Math.PI * 2;
64
- heights[index] = randomRange(random, resolved.height);
110
+ let heightScale = randomRange(random, resolved.height);
111
+ // Two random draws happen on every path so the seeded stream stays aligned regardless of feather.
112
+ const gate = random();
113
+ const settle = random();
114
+ if (resolved.edgeFeather > 0) {
115
+ // Tufts drop out probabilistically across the feather band (mostly keeping full height),
116
+ // so the border reads as grass growing sparser — not a mowed short-pile strip.
117
+ const edge = Math.min(halfW - Math.abs(x), halfD - Math.abs(z));
118
+ const inside = Math.min(1, Math.max(0, edge / resolved.edgeFeather));
119
+ heightScale *= gate < inside * inside ? 0.7 + 0.3 * settle : 0;
120
+ }
121
+ for (const zone of resolved.exclude) {
122
+ // Signed distance outside the rect: blades vanish inside and regrow across the feather ramp.
123
+ const d = Math.max(Math.abs(x - zone.center[0]) - zone.half[0], Math.abs(z - zone.center[1]) - zone.half[1]);
124
+ heightScale *= Math.min(1, Math.max(0, d / Math.max(0.001, zone.feather ?? 1.5)));
125
+ }
126
+ heights[index] = heightScale;
65
127
  widths[index] = randomRange(random, resolved.width);
66
128
  bend[index] = randomRange(random, resolved.bend);
67
129
  phase[index] = random() * Math.PI * 2;
@@ -74,7 +136,7 @@ export function createGrassBladeGeometry(options = {}) {
74
136
  geometry.setAttribute("instanceBend", new THREE.InstancedBufferAttribute(bend, 1));
75
137
  geometry.setAttribute("instancePhase", new THREE.InstancedBufferAttribute(phase, 1));
76
138
  geometry.setAttribute("instanceColorMix", new THREE.InstancedBufferAttribute(colorMix, 1));
77
- geometry.instanceCount = resolved.count;
139
+ geometry.instanceCount = tuftCount;
78
140
  geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), Math.max(size.width, size.depth) * 0.75 + resolved.height[1]);
79
141
  return geometry;
80
142
  }
@@ -6,11 +6,22 @@ export interface GrassWindOptions {
6
6
  gustScale?: number;
7
7
  flutter?: number;
8
8
  }
9
+ /** Camera-distance fade band: tufts thin out between `start` and `end` meters, so the instance budget spends where the camera lives. */
10
+ export interface GrassDistanceFadeOptions {
11
+ start?: number;
12
+ end?: number;
13
+ }
9
14
  export interface GrassMaterialOptions {
10
15
  colorBase?: THREE.ColorRepresentation;
11
16
  colorTip?: THREE.ColorRepresentation;
17
+ /** Ground color the blade roots blend into (pass the terrain color under the patch so blades grow *out of* the ground instead of floating on it). Defaults to a darkened `colorBase`. */
18
+ colorGround?: THREE.ColorRepresentation;
12
19
  colorVariation?: number;
13
20
  wind?: GrassWindOptions | false;
21
+ /** Distance thinning; `false` disables. Default fades between 35 m and 95 m. */
22
+ distanceFade?: GrassDistanceFadeOptions | false;
23
+ /** 0..1 blend of blade normals toward straight-up — lit like a soft carpet instead of dark individual planes. Default 0.6. */
24
+ normalLift?: number;
14
25
  roughness?: number;
15
26
  }
16
27
  export interface GrassShaderUniforms {
@@ -22,14 +33,21 @@ export interface GrassShaderUniforms {
22
33
  uWindFlutter: THREE.IUniform<number>;
23
34
  uColorBase: THREE.IUniform<THREE.Color>;
24
35
  uColorTip: THREE.IUniform<THREE.Color>;
36
+ uColorGround: THREE.IUniform<THREE.Color>;
25
37
  uColorVariation: THREE.IUniform<number>;
38
+ uDistanceFade: THREE.IUniform<THREE.Vector2>;
39
+ uNormalLift: THREE.IUniform<number>;
26
40
  }
27
41
  export interface GrassMaterialHandle {
28
42
  material: THREE.MeshStandardMaterial;
29
43
  uniforms: GrassShaderUniforms;
30
44
  }
31
45
  export declare const DEFAULT_GRASS_WIND: Required<GrassWindOptions>;
46
+ /** Default camera-distance fade band: tufts start thinning at 35 m and are gone by 95 m. */
47
+ export declare const DEFAULT_GRASS_DISTANCE_FADE: Required<GrassDistanceFadeOptions>;
32
48
  /** @internal */
33
49
  export declare function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>;
34
50
  /** @internal */
51
+ export declare function resolveGrassDistanceFade(fade: GrassDistanceFadeOptions | false | undefined): Required<GrassDistanceFadeOptions>;
52
+ /** @internal */
35
53
  export declare function createGrassMaterial(options?: GrassMaterialOptions): GrassMaterialHandle;
@@ -6,6 +6,11 @@ export const DEFAULT_GRASS_WIND = {
6
6
  gustScale: 0.16,
7
7
  flutter: 0.08,
8
8
  };
9
+ /** Default camera-distance fade band: tufts start thinning at 35 m and are gone by 95 m. */
10
+ export const DEFAULT_GRASS_DISTANCE_FADE = {
11
+ start: 35,
12
+ end: 95,
13
+ };
9
14
  function normalizeWindDirection(direction) {
10
15
  const vector = new THREE.Vector2(direction[0], direction[1]);
11
16
  return vector.lengthSq() === 0 ? new THREE.Vector2(1, 0) : vector.normalize();
@@ -23,8 +28,23 @@ export function resolveGrassWind(wind) {
23
28
  };
24
29
  }
25
30
  /** @internal */
31
+ export function resolveGrassDistanceFade(fade) {
32
+ // start >= end disables the fade in the shader, so "off" is just an empty band.
33
+ if (fade === false)
34
+ return { start: 0, end: 0 };
35
+ return {
36
+ start: fade?.start ?? DEFAULT_GRASS_DISTANCE_FADE.start,
37
+ end: fade?.end ?? DEFAULT_GRASS_DISTANCE_FADE.end,
38
+ };
39
+ }
40
+ /** @internal */
26
41
  export function createGrassMaterial(options = {}) {
27
42
  const wind = resolveGrassWind(options.wind);
43
+ const fade = resolveGrassDistanceFade(options.distanceFade);
44
+ const colorBase = new THREE.Color(options.colorBase ?? "#2d431f");
45
+ const colorGround = options.colorGround === undefined
46
+ ? colorBase.clone().multiplyScalar(0.82)
47
+ : new THREE.Color(options.colorGround);
28
48
  const uniforms = {
29
49
  uTime: { value: 0 },
30
50
  uWindDirection: { value: normalizeWindDirection(wind.direction) },
@@ -32,9 +52,12 @@ export function createGrassMaterial(options = {}) {
32
52
  uWindSpeed: { value: wind.speed },
33
53
  uWindGustScale: { value: wind.gustScale },
34
54
  uWindFlutter: { value: wind.flutter },
35
- uColorBase: { value: new THREE.Color(options.colorBase ?? "#2d431f") },
55
+ uColorBase: { value: colorBase },
36
56
  uColorTip: { value: new THREE.Color(options.colorTip ?? "#a6cc58") },
57
+ uColorGround: { value: colorGround },
37
58
  uColorVariation: { value: options.colorVariation ?? 0.28 },
59
+ uDistanceFade: { value: new THREE.Vector2(fade.start, fade.end) },
60
+ uNormalLift: { value: THREE.MathUtils.clamp(options.normalLift ?? 0.6, 0, 1) },
38
61
  };
39
62
  const material = new THREE.MeshStandardMaterial({
40
63
  color: "#ffffff",
@@ -54,36 +77,61 @@ attribute float instanceWidth;
54
77
  attribute float instanceBend;
55
78
  attribute float instancePhase;
56
79
  attribute float instanceColorMix;
80
+ attribute vec2 bladeOffset;
81
+ attribute vec4 bladeVary;
82
+ attribute float bladeBendScale;
57
83
  uniform float uTime;
58
84
  uniform vec2 uWindDirection;
59
85
  uniform float uWindStrength;
60
86
  uniform float uWindSpeed;
61
87
  uniform float uWindGustScale;
62
88
  uniform float uWindFlutter;
89
+ uniform vec2 uDistanceFade;
90
+ uniform float uNormalLift;
63
91
  varying float vGrassBladeT;
64
92
  varying float vGrassColorMix;
93
+ varying float vGrassFade;
65
94
  vec3 jgGrassPosition;
66
95
  vec3 jgGrassNormal;
67
96
  void jgComputeGrassBlade() {
68
97
  float t = position.y;
69
98
  float side = position.x;
70
- float taper = (1.0 - t) * (0.72 + 0.28 * (1.0 - t));
71
- float width = instanceWidth * taper;
72
- float arc = instanceBend * t * t;
73
- vec3 localPosition = vec3(side * width, instanceHeight * t - arc * instanceHeight * 0.18, arc * instanceHeight);
74
- vec3 localNormal = normalize(vec3(0.0, -instanceBend * t, 1.0));
75
- float cy = cos(instanceYaw);
76
- float sy = sin(instanceYaw);
99
+ // Camera-distance thinning: each tuft draws a random threshold, so the field dissolves
100
+ // progressively across the fade band instead of ending at a hard curtain.
101
+ float camDist = distance((modelMatrix * vec4(instanceOffset, 1.0)).xyz, cameraPosition);
102
+ float fadeT = uDistanceFade.y > uDistanceFade.x
103
+ ? smoothstep(uDistanceFade.x, uDistanceFade.y, camDist)
104
+ : 0.0;
105
+ float threshold = fract(instanceColorMix * 61.7) * 0.85;
106
+ float keep = 1.0 - smoothstep(threshold, threshold + 0.15, fadeT);
107
+ float heightScale = instanceHeight * bladeVary.y * keep;
108
+ // Paddle taper: hold width through the mid-blade and only pinch near the tip — thin
109
+ // triangle blades leave the ground visible between tufts and read as stubble.
110
+ float taper = (1.0 - 0.3 * t) * (1.0 - smoothstep(0.5, 1.0, t) * 0.95);
111
+ // Surviving distant tufts widen to hold silhouette coverage as neighbors thin out.
112
+ float width = instanceWidth * bladeVary.z * taper * (1.0 + fadeT * 1.4);
113
+ float arc = instanceBend * bladeBendScale * t * t;
114
+ vec3 localPosition = vec3(side * width, heightScale * t - arc * heightScale * 0.18, arc * heightScale);
115
+ vec3 localNormal = normalize(vec3(0.0, -instanceBend * bladeBendScale * t, 1.0));
116
+ float bladeYaw = instanceYaw + bladeVary.x;
117
+ float cy = cos(bladeYaw);
118
+ float sy = sin(bladeYaw);
77
119
  vec3 yawedPosition = vec3(localPosition.x * cy + localPosition.z * sy, localPosition.y, -localPosition.x * sy + localPosition.z * cy);
78
120
  vec3 yawedNormal = normalize(vec3(localNormal.x * cy + localNormal.z * sy, localNormal.y, -localNormal.x * sy + localNormal.z * cy));
121
+ float ct = cos(instanceYaw);
122
+ float st = sin(instanceYaw);
123
+ vec3 tuftOffset = vec3(bladeOffset.x * ct + bladeOffset.y * st, 0.0, -bladeOffset.x * st + bladeOffset.y * ct);
79
124
  float gustPhase = dot(instanceOffset.xz, uWindDirection) * uWindGustScale + uTime * uWindSpeed + instancePhase;
80
125
  float gust = sin(gustPhase) * 0.7 + sin(gustPhase * 0.43 + 2.4) * 0.3;
81
- float flutter = sin(uTime * 7.5 + instancePhase * 2.7) * uWindFlutter;
82
- vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t;
83
- jgGrassPosition = instanceOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
84
- jgGrassNormal = yawedNormal;
126
+ float flutter = sin(uTime * 7.5 + instancePhase * 2.7 + bladeVary.w * 6.2832) * uWindFlutter;
127
+ vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t * (0.4 + 0.6 * bladeVary.y);
128
+ jgGrassPosition = instanceOffset + tuftOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
129
+ // Lifting normals toward straight-up lights the field like a continuous meadow surface —
130
+ // side-lit individual blade planes read as dark stubble, not turf.
131
+ jgGrassNormal = normalize(mix(yawedNormal, vec3(0.0, 1.0, 0.0), uNormalLift));
85
132
  vGrassBladeT = t;
86
133
  vGrassColorMix = instanceColorMix;
134
+ vGrassFade = fadeT;
87
135
  }
88
136
  `)
89
137
  .replace("#include <beginnormal_vertex>", "jgComputeGrassBlade();\nvec3 objectNormal = jgGrassNormal;")
@@ -93,15 +141,20 @@ void jgComputeGrassBlade() {
93
141
  #include <common>
94
142
  uniform vec3 uColorBase;
95
143
  uniform vec3 uColorTip;
144
+ uniform vec3 uColorGround;
96
145
  uniform float uColorVariation;
97
146
  varying float vGrassBladeT;
98
147
  varying float vGrassColorMix;
148
+ varying float vGrassFade;
99
149
  `)
100
150
  .replace("#include <color_fragment>", `
101
151
  #include <color_fragment>
102
152
  vec3 grassColor = mix(uColorBase, uColorTip, smoothstep(0.0, 1.0, vGrassBladeT));
103
153
  grassColor *= mix(1.0 - uColorVariation, 1.0 + uColorVariation, vGrassColorMix);
104
- grassColor *= mix(0.58, 1.0, smoothstep(0.05, 0.45, vGrassBladeT));
154
+ // Roots take the ground color so blades grow out of the terrain instead of floating on it,
155
+ // and distant blades converge back toward it so the fade band blends into the mottled ground.
156
+ grassColor = mix(uColorGround, grassColor, smoothstep(0.0, 0.55, vGrassBladeT));
157
+ grassColor = mix(grassColor, uColorGround * 1.04, vGrassFade * 0.6);
105
158
  diffuseColor.rgb = grassColor;
106
159
  `);
107
160
  };
@@ -0,0 +1,34 @@
1
+ import * as THREE from "three";
2
+ /** XZ rectangle in world units — matches the shape of a terraform snapshot's `bounds`. */
3
+ export interface HeightfieldRect {
4
+ minX: number;
5
+ minZ: number;
6
+ maxX: number;
7
+ maxZ: number;
8
+ }
9
+ /** Per-vertex color hook for {@link displaceHeightfieldGeometry}; write the tone into `out`. */
10
+ export type HeightfieldColorFn = (x: number, z: number, height: number, out: THREE.Color) => void;
11
+ /** Options for {@link displaceHeightfieldGeometry}. */
12
+ export interface HeightfieldDisplaceOptions {
13
+ /** World-space rect the plane spans (the plane is centered on it, XZ, +Y up). */
14
+ bounds: HeightfieldRect;
15
+ /**
16
+ * Dirty world-space region to re-sample; only the vertex window covering it (plus a one-vertex
17
+ * normal ring) updates. Omit or pass null for a full rebuild.
18
+ */
19
+ region?: HeightfieldRect | null;
20
+ /** When given, writes a `color` attribute (creating it on demand) via this per-vertex hook. */
21
+ color?: HeightfieldColorFn;
22
+ }
23
+ /**
24
+ * Re-samples a `PlaneGeometry(width, depth, segments, segments).rotateX(-π/2)` heightfield mesh
25
+ * from a `sampleHeight` field in place: vertex Y, optional vertex color, grid central-difference
26
+ * normals, and an analytically maintained bounding sphere. With a dirty `region`, the work is
27
+ * O(region vertices) — heights/colors update inside the covering vertex window, normals inside the
28
+ * window plus a one-vertex ring — never a whole-mesh `computeVertexNormals`/`computeBoundingSphere`
29
+ * pass, which is what keeps per-frame brush stamps inside the editor's frame budget. The bounding
30
+ * sphere derives from the fixed plane extents plus a running height range kept on
31
+ * `geometry.userData`; partial passes only expand it, a full pass resets it exactly.
32
+ * @capability heightfield-mesh-update in-place partial update of a displaced ground plane mesh
33
+ */
34
+ export declare function displaceHeightfieldGeometry(geometry: THREE.BufferGeometry, sampleHeight: (x: number, z: number) => number, options: HeightfieldDisplaceOptions): void;