@jgengine/shell 0.19.0 → 0.22.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 (215) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +113 -15
  5. package/dist/Shell3dPresentation.d.ts +2 -2
  6. package/dist/Shell3dPresentation.js +49 -7
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +11 -1
  10. package/dist/ShellHudPresentation.js +20 -6
  11. package/dist/audio/audioEngine.d.ts +16 -0
  12. package/dist/audio/audioEngine.js +178 -89
  13. package/dist/audio/audioWire.js +2 -2
  14. package/dist/audio/synthEngine.d.ts +7 -1
  15. package/dist/audio/synthEngine.js +103 -38
  16. package/dist/camera/GameCameraRig.js +2 -1
  17. package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
  18. package/dist/camera/GameFirstPersonCamera.js +17 -13
  19. package/dist/camera/cameraRigs.js +8 -2
  20. package/dist/camera/chaseHeading.d.ts +9 -0
  21. package/dist/camera/chaseHeading.js +10 -0
  22. package/dist/camera/customMuzzle.d.ts +14 -0
  23. package/dist/camera/customMuzzle.js +68 -0
  24. package/dist/camera/index.d.ts +1 -0
  25. package/dist/camera/index.js +1 -0
  26. package/dist/camera/rigResolve.d.ts +3 -0
  27. package/dist/camera/rigResolve.js +11 -0
  28. package/dist/combat/ProjectileModels.d.ts +31 -0
  29. package/dist/combat/ProjectileModels.js +70 -0
  30. package/dist/commandSink.d.ts +1 -2
  31. package/dist/commandSink.js +3 -2
  32. package/dist/defineGame.d.ts +12 -5
  33. package/dist/defineGame.js +25 -10
  34. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  35. package/dist/devtools/DevtoolsOverlay.js +14 -168
  36. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  37. package/dist/devtools/DevtoolsRuntime.js +61 -0
  38. package/dist/devtools/agentBridge.js +1 -1
  39. package/dist/devtools/devtoolsReports.d.ts +68 -0
  40. package/dist/devtools/devtoolsReports.js +100 -0
  41. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  42. package/dist/devtools/latencyInstrumentation.js +11 -0
  43. package/dist/devtools/playtestProbe.d.ts +4 -0
  44. package/dist/devtools/playtestProbe.js +22 -0
  45. package/dist/drivers/FrameDriver.js +25 -21
  46. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  47. package/dist/drivers/HudOnlyDriver.js +32 -14
  48. package/dist/environment/EnvironmentScene.js +19 -15
  49. package/dist/environment/terrainGroundColor.d.ts +5 -4
  50. package/dist/environment/terrainGroundColor.js +19 -8
  51. package/dist/inputSink.d.ts +6 -6
  52. package/dist/inputSink.js +60 -21
  53. package/dist/lazyShell3dPresentation.d.ts +9 -0
  54. package/dist/lazyShell3dPresentation.js +24 -0
  55. package/dist/materialOverride.d.ts +42 -0
  56. package/dist/materialOverride.js +95 -36
  57. package/dist/multiplayer.d.ts +33 -3
  58. package/dist/multiplayer.js +105 -22
  59. package/dist/pointer/pointerService.d.ts +2 -0
  60. package/dist/pointer/pointerService.js +12 -2
  61. package/dist/postfx/PostProcessing.d.ts +10 -0
  62. package/dist/postfx/PostProcessing.js +9 -41
  63. package/dist/postfx/gradeShader.d.ts +1 -1
  64. package/dist/postfx/gradeShader.js +30 -2
  65. package/dist/postfx/shadowPrepass.d.ts +3 -0
  66. package/dist/postfx/shadowPrepass.js +18 -0
  67. package/dist/presentationRecovery.d.ts +75 -0
  68. package/dist/presentationRecovery.js +204 -0
  69. package/dist/presentationRecovery3d.d.ts +4 -0
  70. package/dist/presentationRecovery3d.js +32 -0
  71. package/dist/presentationResolve.d.ts +8 -1
  72. package/dist/presentationResolve.js +5 -2
  73. package/dist/render/CascadedShadows.d.ts +7 -2
  74. package/dist/render/CascadedShadows.js +98 -20
  75. package/dist/render/EnvironmentLighting.js +74 -35
  76. package/dist/render/HairCards.d.ts +7 -0
  77. package/dist/render/HairCards.js +23 -0
  78. package/dist/render/SceneModels.js +51 -77
  79. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  80. package/dist/render/authoredSurfaceMaterial.js +108 -0
  81. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  82. package/dist/render/materialAppearanceSignals.js +58 -0
  83. package/dist/render/materialAsset.d.ts +98 -0
  84. package/dist/render/materialAsset.js +454 -0
  85. package/dist/render/measureBounds.js +16 -3
  86. package/dist/render/measureCollisionMesh.js +8 -1
  87. package/dist/render/modelAssets.d.ts +8 -0
  88. package/dist/render/modelAssets.js +43 -0
  89. package/dist/render/modelBindPose.d.ts +5 -0
  90. package/dist/render/modelBindPose.js +35 -0
  91. package/dist/render/modelLoad.d.ts +3 -0
  92. package/dist/render/modelLoad.js +9 -0
  93. package/dist/render/modelNodeVisibility.d.ts +6 -0
  94. package/dist/render/modelNodeVisibility.js +18 -0
  95. package/dist/render/modelRender.d.ts +18 -1
  96. package/dist/render/modelRender.js +81 -3
  97. package/dist/render/projectedOverdraw.d.ts +35 -0
  98. package/dist/render/projectedOverdraw.js +204 -0
  99. package/dist/render/resolveModel.d.ts +12 -2
  100. package/dist/render/resolveModel.js +21 -4
  101. package/dist/render/rigNode.d.ts +26 -0
  102. package/dist/render/rigNode.js +32 -0
  103. package/dist/render/spriteRender.d.ts +4 -0
  104. package/dist/render/spriteRender.js +5 -0
  105. package/dist/render/staticShapeInstances.d.ts +34 -0
  106. package/dist/render/staticShapeInstances.js +78 -0
  107. package/dist/render/useFootIk.js +76 -11
  108. package/dist/render/useModelAnimation.d.ts +28 -2
  109. package/dist/render/useModelAnimation.js +194 -69
  110. package/dist/render/useModelInstance.d.ts +25 -0
  111. package/dist/render/useModelInstance.js +43 -0
  112. package/dist/scatter/scatterModels.d.ts +3 -2
  113. package/dist/scatter/scatterModels.js +15 -7
  114. package/dist/scene/AuthoredScene.d.ts +14 -6
  115. package/dist/scene/AuthoredScene.js +30 -9
  116. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  117. package/dist/scene/GrassFieldRenderer.js +20 -2
  118. package/dist/settings/SettingsMenu.js +11 -18
  119. package/dist/settings/SettingsRuntime.d.ts +5 -0
  120. package/dist/settings/SettingsRuntime.js +29 -2
  121. package/dist/shellControlSuspension.d.ts +18 -0
  122. package/dist/shellControlSuspension.js +28 -0
  123. package/dist/shellInputPublication.d.ts +28 -0
  124. package/dist/shellInputPublication.js +49 -0
  125. package/dist/shellPointerInput.d.ts +18 -0
  126. package/dist/shellPointerInput.js +49 -0
  127. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  128. package/dist/structures/BuildingKitBatch.js +34 -59
  129. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  130. package/dist/structures/BuildingRenderBudget.js +54 -0
  131. package/dist/structures/GeneratedBuilding.js +13 -8
  132. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  133. package/dist/structures/buildingKitMaterials.js +70 -0
  134. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  135. package/dist/structures/buildingSpatialBatch.js +45 -0
  136. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  137. package/dist/terrain/CarvedTerrain.js +2 -2
  138. package/dist/terrain/GrassField.d.ts +3 -1
  139. package/dist/terrain/GrassField.js +61 -6
  140. package/dist/terrain/grassGeometry.d.ts +13 -0
  141. package/dist/terrain/grassGeometry.js +57 -1
  142. package/dist/terrain/grassMaterial.js +2 -2
  143. package/dist/terrain/index.d.ts +1 -0
  144. package/dist/terrain/index.js +1 -0
  145. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  146. package/dist/terrain/terrainDetailMaterial.js +7 -7
  147. package/dist/terrain/terrainMath.d.ts +2 -0
  148. package/dist/terrain/terrainMath.js +2 -0
  149. package/dist/terrain/terrainPaintColor.d.ts +11 -0
  150. package/dist/terrain/terrainPaintColor.js +47 -0
  151. package/dist/touch/TouchControlsOverlay.js +4 -4
  152. package/dist/useShellMultiplayerSync.js +4 -0
  153. package/dist/vfx/ParticleField.d.ts +23 -20
  154. package/dist/vfx/ParticleField.js +168 -50
  155. package/dist/visibility/CullingProvider.js +2 -62
  156. package/dist/visibility/cullingDriver.d.ts +9 -0
  157. package/dist/visibility/cullingDriver.js +68 -0
  158. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  159. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  160. package/dist/weather/DustField.d.ts +2 -1
  161. package/dist/weather/DustField.js +17 -3
  162. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  163. package/dist/weather/FireSpreadLayer.js +5 -11
  164. package/dist/weather/LightningStrike.d.ts +4 -1
  165. package/dist/weather/LightningStrike.js +13 -17
  166. package/dist/weather/RainField.d.ts +2 -1
  167. package/dist/weather/RainField.js +17 -3
  168. package/dist/weather/RainImpactField.d.ts +16 -0
  169. package/dist/weather/RainImpactField.js +89 -0
  170. package/dist/weather/SnowField.d.ts +2 -1
  171. package/dist/weather/SnowField.js +17 -3
  172. package/dist/weather/WeatherLayer.d.ts +11 -2
  173. package/dist/weather/WeatherLayer.js +30 -6
  174. package/dist/weather/index.d.ts +2 -0
  175. package/dist/weather/index.js +2 -0
  176. package/dist/weather/weatherMath.d.ts +2 -0
  177. package/dist/weather/weatherMath.js +8 -0
  178. package/dist/weather/weatherShelter.d.ts +18 -0
  179. package/dist/weather/weatherShelter.js +75 -0
  180. package/dist/weather/weatherUniforms.d.ts +40 -1
  181. package/dist/weather/weatherUniforms.js +29 -9
  182. package/dist/world/AuthoredFlocks.d.ts +22 -0
  183. package/dist/world/AuthoredFlocks.js +87 -0
  184. package/dist/world/StaticObjectModels.d.ts +8 -0
  185. package/dist/world/StaticObjectModels.js +75 -0
  186. package/dist/world/WorldHud.d.ts +14 -6
  187. package/dist/world/WorldHud.js +25 -12
  188. package/dist/world/WorldParticles.d.ts +10 -23
  189. package/dist/world/WorldParticles.js +209 -61
  190. package/dist/world/WorldScene.d.ts +3 -1
  191. package/dist/world/WorldScene.js +41 -26
  192. package/dist/world/staticObjectBatches.d.ts +35 -0
  193. package/dist/world/staticObjectBatches.js +163 -0
  194. package/dist/world/worldBarSamples.d.ts +37 -33
  195. package/dist/world/worldBarSamples.js +117 -75
  196. package/package.json +4 -4
  197. package/skills/ce-handoff/SKILL.md +1 -1
  198. package/skills/game-design/SKILL.md +2 -0
  199. package/skills/jgengine/SKILL.md +23 -2
  200. package/skills/jgengine/api.md +28 -17
  201. package/skills/jgengine/recipes/minimal-game.md +7 -2
  202. package/skills/jgengine-ui/SKILL.md +3 -1
  203. package/skills/jgengine-ui/api.md +219 -70
  204. package/skills/jgengine-ui/capabilities.md +119 -1
  205. package/skills/jgengine-ui/reference-primitives.md +93 -3
  206. package/skills/jgengine-ui/reference.md +2 -0
  207. package/skills/jgengine-world/SKILL.md +12 -3
  208. package/skills/jgengine-world/api.md +201 -58
  209. package/skills/jgengine-world/capabilities.md +116 -3
  210. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  211. package/skills/jgengine-world/recipes/controls.md +4 -0
  212. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  213. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  214. package/skills/jgengine-world/reference.md +167 -5
  215. package/skills/jgengine-world/visibility.md +28 -1
@@ -1,50 +1,29 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef } from "react";
2
+ import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
3
3
  import { useLoader } from "@react-three/fiber";
4
4
  import * as THREE from "three";
5
+ import { useModelMaterialTextures } from "../render/materialAsset.js";
5
6
  import { sharedGltfLoader } from "../render/modelLoad.js";
6
7
  import { buildScatterModelSources, disposeScatterModelSources, } from "../scatter/scatterModels.js";
7
- import { composeBuildingKitMatrix, measureBuildingKitModel, } from "./buildingKitFit.js";
8
- import { applyBuildingSurface, surfaceKey, useBuildingSurfaceTextures } from "./buildingSurface.js";
8
+ import { measureBuildingKitModel, } from "./buildingKitFit.js";
9
+ import { useBuildingSurfaceTextures } from "./buildingSurface.js";
10
+ import { groupBuildingKitInstances, buildBuildingKitSources, validateBuildingKitSource } from "./buildingKitMaterials.js";
11
+ import { useBuildingChunkBudget } from "./BuildingRenderBudget.js";
12
+ import { applyBuildingChunk, partitionBuildingMatrices } from "./buildingSpatialBatch.js";
9
13
  function KitSourceInstances({ source, material, matrices, }) {
14
+ const chunks = useMemo(() => partitionBuildingMatrices(source.geometry, matrices, source.localMatrix), [source, matrices]);
15
+ return _jsx(_Fragment, { children: chunks.map((chunk) => _jsx(KitSourceChunk, { source: source, material: material, chunk: chunk }, chunk.key)) });
16
+ }
17
+ function KitSourceChunk({ source, material, chunk }) {
10
18
  const meshRef = useRef(null);
11
- useEffect(() => {
19
+ useBuildingChunkBudget(meshRef, chunk.bounds);
20
+ useLayoutEffect(() => {
12
21
  const mesh = meshRef.current;
13
22
  if (mesh === null)
14
23
  return;
15
- const composed = new THREE.Matrix4();
16
- for (let i = 0; i < matrices.length; i += 1) {
17
- composed.multiplyMatrices(matrices[i], source.localMatrix);
18
- mesh.setMatrixAt(i, composed);
19
- }
20
- mesh.count = matrices.length;
21
- mesh.instanceMatrix.needsUpdate = true;
22
- mesh.computeBoundingSphere();
23
- }, [matrices, source]);
24
- return (_jsx("instancedMesh", { ref: meshRef, args: [source.geometry, material, matrices.length], castShadow: true, receiveShadow: true }, matrices.length));
25
- }
26
- function styleOne(material, tint, surface, textures) {
27
- const clone = material.clone();
28
- const tintable = clone;
29
- if (tint !== undefined && tintable.color !== undefined)
30
- tintable.color.copy(tint);
31
- if (surface !== undefined && clone.isMeshStandardMaterial === true) {
32
- applyBuildingSurface(clone, surface, textures);
33
- }
34
- return clone;
35
- }
36
- function styleMaterial(material, tint, surface, textures) {
37
- const color = tint === "" ? undefined : new THREE.Color(tint);
38
- return Array.isArray(material)
39
- ? material.map((entry) => styleOne(entry, color, surface, textures))
40
- : styleOne(material, color, surface, textures);
41
- }
42
- function disposeMaterial(material) {
43
- if (Array.isArray(material))
44
- for (const entry of material)
45
- entry.dispose();
46
- else
47
- material.dispose();
24
+ return applyBuildingChunk(mesh, chunk);
25
+ }, [chunk, source, material]);
26
+ return (_jsx("instancedMesh", { ref: meshRef, args: [source.geometry, material, chunk.matrices.length], castShadow: true, receiveShadow: true, dispose: null }, chunk.matrices.length));
48
27
  }
49
28
  /**
50
29
  * GPU-instances one kit model across every building part bound to it: the GLB loads once, its meshes
@@ -52,31 +31,27 @@ function disposeMaterial(material) {
52
31
  * while the GLB loads — mount it inside a `<Suspense>` so one slow model never blanks the rest.
53
32
  * @internal
54
33
  */
55
- export function BuildingKitBatch({ url, instances }) {
34
+ export function BuildingKitBatch({ url, instances, materialAssets }) {
56
35
  const gltf = useLoader(sharedGltfLoader, url);
36
+ useMemo(() => {
37
+ const checked = new Set();
38
+ for (const instance of instances)
39
+ if (!checked.has(instance.part)) {
40
+ validateBuildingKitSource(gltf.scene, instance.part, materialAssets);
41
+ checked.add(instance.part);
42
+ }
43
+ }, [gltf, instances, materialAssets]);
57
44
  const { sources, root } = useMemo(() => buildScatterModelSources(gltf.scene, { url }), [gltf, url]);
58
45
  useEffect(() => () => disposeScatterModelSources(root), [root]);
59
46
  const bounds = useMemo(() => measureBuildingKitModel(root), [root]);
60
- const groups = useMemo(() => {
61
- const byKey = new Map();
62
- for (const instance of instances) {
63
- const matrix = composeBuildingKitMatrix(instance, bounds, new THREE.Matrix4());
64
- const tint = instance.part.tint ?? "";
65
- const surface = instance.part.material;
66
- const key = `${tint}|${surfaceKey(surface)}`;
67
- const bucket = byKey.get(key);
68
- if (bucket === undefined)
69
- byKey.set(key, { key, tint, surface, matrices: [matrix] });
70
- else
71
- bucket.matrices.push(matrix);
72
- }
73
- return [...byKey.values()];
74
- }, [instances, bounds]);
75
- return (_jsx(_Fragment, { children: groups.map((group) => group.tint === "" && group.surface === undefined ? (sources.map((source, index) => (_jsx(KitSourceInstances, { source: source, material: source.material, matrices: group.matrices }, `${group.key}:${index}`)))) : (_jsx(StyledKitSources, { sources: sources, group: group }, group.key))) }));
47
+ const groups = useMemo(() => groupBuildingKitInstances(instances, bounds, materialAssets), [instances, bounds, materialAssets]);
48
+ return (_jsx(_Fragment, { children: groups.map((group) => (group.part.tint ?? "") === "" && group.part.material === undefined && !group.part.materialAssignments?.length ? (sources.map((source, index) => (_jsx(KitSourceInstances, { source: source, material: source.material, matrices: group.matrices }, `${group.key}:${index}`)))) : (_jsx(StyledKitSources, { scene: gltf.scene, materialAssets: materialAssets, group: group }, group.key))) }));
76
49
  }
77
- function StyledKitSources({ sources, group }) {
78
- const textures = useBuildingSurfaceTextures(group.surface);
79
- const styled = useMemo(() => sources.map((source) => styleMaterial(source.material, group.tint, group.surface, textures)), [sources, group.tint, group.surface, textures]);
80
- useEffect(() => () => styled.forEach(disposeMaterial), [styled]);
81
- return (_jsx(_Fragment, { children: sources.map((source, index) => (_jsx(KitSourceInstances, { source: source, material: styled[index], matrices: group.matrices }, index))) }));
50
+ function StyledKitSources({ scene, materialAssets, group }) {
51
+ useMemo(() => validateBuildingKitSource(scene, group.part, materialAssets), [scene, group.key]);
52
+ const textures = useBuildingSurfaceTextures(group.part.material);
53
+ const materialTextures = useModelMaterialTextures({ materialAssets, materialAssignments: group.part.materialAssignments });
54
+ const { sources, root } = useMemo(() => buildBuildingKitSources(scene, group.part, materialAssets, textures, materialTextures.assets), [scene, group.key, textures, materialTextures]);
55
+ useEffect(() => () => disposeScatterModelSources(root), [root]);
56
+ return (_jsx(_Fragment, { children: sources.map((source, index) => (_jsx(KitSourceInstances, { source: source, material: source.material, matrices: group.matrices }, index))) }));
82
57
  }
@@ -0,0 +1,23 @@
1
+ import { type ReactNode, type RefObject } from "react";
2
+ import * as THREE from "three";
3
+ import type { VisibilityConfig } from "@jgengine/core/visibility/config";
4
+ /** Resolved shell distance policy for generated facade batches. @internal */
5
+ export interface BuildingRenderPolicy {
6
+ enabled: boolean;
7
+ minDistance: number;
8
+ maxDistance: number;
9
+ hysteresis: number;
10
+ }
11
+ /** Resolve existing visibility overrides over the already resolved graphics-tier distance. @internal */
12
+ export declare function resolveBuildingRenderPolicy(drawDistance: number, config?: VisibilityConfig, cullingDisabled?: boolean): BuildingRenderPolicy;
13
+ /** Forward the shell's resolved graphics distance without registering generated parts as scene objects. @internal */
14
+ export declare function BuildingRenderBudget({ drawDistance, visibility, cullingDisabled, children }: {
15
+ drawDistance: number;
16
+ visibility?: VisibilityConfig;
17
+ cullingDisabled?: boolean;
18
+ children: ReactNode;
19
+ }): import("react").JSX.Element;
20
+ /** Test world bounds; transforming the box first also conservatively encloses affine shear. @internal */
21
+ export declare function buildingChunkWithinBudget(bounds: THREE.Box3, matrixWorld: THREE.Matrix4, cameraPosition: THREE.Vector3, policy: BuildingRenderPolicy, previouslyVisible: boolean, worldBounds: THREE.Box3, worldSphere: THREE.Sphere, frustumSphere?: THREE.Sphere | null): boolean;
22
+ /** Distance-gate chunks only; Three retains independent main-camera and shadow-camera frustum tests. @internal */
23
+ export declare function useBuildingChunkBudget(meshRef: RefObject<THREE.InstancedMesh | null>, bounds: THREE.Box3): void;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo, useRef } from "react";
3
+ import { useFrame } from "@react-three/fiber";
4
+ import * as THREE from "three";
5
+ import { culledByDistance } from "@jgengine/core/visibility/distance";
6
+ import { DEFAULT_CULLING_SETTINGS } from "@jgengine/core/visibility/settings";
7
+ import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
8
+ const UNLIMITED = { enabled: false, minDistance: 0, maxDistance: Infinity, hysteresis: 0 };
9
+ const BuildingRenderContext = createContext(UNLIMITED);
10
+ /** Resolve existing visibility overrides over the already resolved graphics-tier distance. @internal */
11
+ export function resolveBuildingRenderPolicy(drawDistance, config, cullingDisabled = false) {
12
+ const culling = config?.culling;
13
+ const scene = config?.scene;
14
+ return {
15
+ // Scene callbacks address visibility rows; generated parts have no row to evaluate safely.
16
+ enabled: !cullingDisabled && config?.enabled !== false && culling?.enabled !== false && culling?.distanceCulling !== false
17
+ && scene?.alwaysVisible !== true && scene?.cullingDisabled !== true && scene?.customVisibility === undefined,
18
+ minDistance: scene?.minRenderDistance ?? culling?.defaultMinRenderDistance ?? DEFAULT_CULLING_SETTINGS.defaultMinRenderDistance,
19
+ maxDistance: scene?.maxRenderDistance ?? culling?.defaultMaxRenderDistance ?? drawDistance,
20
+ hysteresis: culling?.hysteresis ?? DEFAULT_CULLING_SETTINGS.hysteresis,
21
+ };
22
+ }
23
+ /** Forward the shell's resolved graphics distance without registering generated parts as scene objects. @internal */
24
+ export function BuildingRenderBudget({ drawDistance, visibility, cullingDisabled = false, children }) {
25
+ const policy = useMemo(() => resolveBuildingRenderPolicy(drawDistance, visibility, cullingDisabled), [drawDistance, visibility, cullingDisabled]);
26
+ return _jsx(BuildingRenderContext.Provider, { value: policy, children: children });
27
+ }
28
+ /** Test world bounds; transforming the box first also conservatively encloses affine shear. @internal */
29
+ export function buildingChunkWithinBudget(bounds, matrixWorld, cameraPosition, policy, previouslyVisible, worldBounds, worldSphere, frustumSphere) {
30
+ worldBounds.copy(bounds).applyMatrix4(matrixWorld).getBoundingSphere(worldSphere);
31
+ if (frustumSphere !== undefined && frustumSphere !== null) {
32
+ bounds.getCenter(frustumSphere.center);
33
+ const worldScale = matrixWorld.getMaxScaleOnAxis();
34
+ // Three transforms this sphere using the largest matrix column, which can underbound shear.
35
+ frustumSphere.radius = Math.max(bounds.min.distanceTo(bounds.max) / 2, worldScale === 0 ? 0 : worldSphere.radius / worldScale);
36
+ }
37
+ if (!policy.enabled)
38
+ return true;
39
+ const { center, radius } = worldSphere;
40
+ return !culledByDistance(cameraPosition.x, cameraPosition.y, cameraPosition.z, center.x, center.y, center.z, policy.minDistance, policy.maxDistance, radius, previouslyVisible ? policy.hysteresis : 0);
41
+ }
42
+ /** Distance-gate chunks only; Three retains independent main-camera and shadow-camera frustum tests. @internal */
43
+ export function useBuildingChunkBudget(meshRef, bounds) {
44
+ const policy = useContext(BuildingRenderContext);
45
+ const scratch = useRef({ box: new THREE.Box3(), sphere: new THREE.Sphere(), camera: new THREE.Vector3() });
46
+ useFrame(({ camera }) => {
47
+ const mesh = meshRef.current;
48
+ if (mesh === null)
49
+ return;
50
+ mesh.updateWorldMatrix(true, false);
51
+ camera.getWorldPosition(scratch.current.camera);
52
+ mesh.visible = buildingChunkWithinBudget(bounds, mesh.matrixWorld, scratch.current.camera, policy, mesh.visible, scratch.current.box, scratch.current.sphere, mesh.boundingSphere);
53
+ }, CAMERA_POST_FRAME_PRIORITY);
54
+ }
@@ -1,9 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Suspense, useLayoutEffect, useMemo, useRef } from "react";
3
3
  import * as THREE from "three";
4
4
  import { buildingSurfaceColor, resolveBuildingSurface, } from "@jgengine/core/world/buildings";
5
5
  import { useDisposable } from "../render/useDisposable.js";
6
6
  import { BuildingKitBatch } from "./BuildingKitBatch.js";
7
+ import { useBuildingChunkBudget } from "./BuildingRenderBudget.js";
8
+ import { applyBuildingChunk, partitionBuildingMatrices } from "./buildingSpatialBatch.js";
7
9
  import { bucketBuildingParts, normalFor, outwardOffset } from "./buildingKitFit.js";
8
10
  import { applyBuildingSurface, useBuildingSurfaceTextures } from "./buildingSurface.js";
9
11
  const DEFAULT_PALETTE = {
@@ -54,17 +56,20 @@ function batchMaterialFor(kind, surface, textures) {
54
56
  function BuildingKindBatch({ kind, matrices, palette, geometry }) {
55
57
  const surface = useMemo(() => surfaceFor(kind, palette), [kind, palette]);
56
58
  const textures = useBuildingSurfaceTextures(surface);
57
- const meshRef = useRef(null);
58
59
  const material = useDisposable(() => batchMaterialFor(kind, surface, textures), [kind, surface, textures]);
60
+ const chunks = useMemo(() => partitionBuildingMatrices(geometry, matrices), [geometry, matrices]);
61
+ return _jsx(_Fragment, { children: chunks.map((chunk) => _jsx(BuildingChunk, { chunk: chunk, material: material, geometry: geometry }, chunk.key)) });
62
+ }
63
+ function BuildingChunk({ chunk, material, geometry }) {
64
+ const meshRef = useRef(null);
65
+ useBuildingChunkBudget(meshRef, chunk.bounds);
59
66
  useLayoutEffect(() => {
60
67
  const mesh = meshRef.current;
61
68
  if (mesh === null)
62
69
  return;
63
- matrices.forEach((matrix, index) => mesh.setMatrixAt(index, matrix));
64
- mesh.instanceMatrix.needsUpdate = true;
65
- mesh.computeBoundingSphere();
66
- }, [matrices, material]);
67
- return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, matrices.length], castShadow: true, receiveShadow: true }, matrices.length));
70
+ return applyBuildingChunk(mesh, chunk);
71
+ }, [chunk, geometry, material]);
72
+ return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, chunk.matrices.length], castShadow: true, receiveShadow: true, dispose: null }, chunk.matrices.length));
68
73
  }
69
74
  export function InstancedBuildings({ buildings, palette, kit, resolveModelUrl, visibleKinds, }) {
70
75
  const geometry = useDisposable(() => new THREE.BoxGeometry(1, 1, 1), []);
@@ -74,7 +79,7 @@ export function InstancedBuildings({ buildings, palette, kit, resolveModelUrl, v
74
79
  }, [buildings, visibleKinds, kit, resolveModelUrl]);
75
80
  if (boxes.size === 0 && models.size === 0)
76
81
  return null;
77
- return (_jsxs("group", { children: [[...boxes.entries()].map(([kind, matrices]) => (_jsx(BuildingKindBatch, { kind: kind, matrices: matrices, palette: palette, geometry: geometry }, kind))), [...models.entries()].map(([url, instances]) => (_jsx(Suspense, { fallback: null, children: _jsx(BuildingKitBatch, { url: url, instances: instances }) }, url)))] }));
82
+ return (_jsxs("group", { children: [[...boxes.entries()].map(([kind, matrices]) => (_jsx(BuildingKindBatch, { kind: kind, matrices: matrices, palette: palette, geometry: geometry }, kind))), [...models.entries()].map(([url, instances]) => (_jsx(Suspense, { fallback: null, children: _jsx(BuildingKitBatch, { url: url, instances: instances, materialAssets: kit?.materialAssets }) }, url)))] }));
78
83
  }
79
84
  function BlockMesh({ part, palette }) {
80
85
  const [nx, nz] = normalFor(part.facade);
@@ -0,0 +1,20 @@
1
+ import * as THREE from "three";
2
+ import type { BuildingKitPart } from "@jgengine/core/world/buildingKit";
3
+ import { type MaterialAsset } from "@jgengine/core/material/materialAsset";
4
+ import type { MaterialOverrideTextures } from "../materialOverride.js";
5
+ import { buildScatterModelSources } from "../scatter/scatterModels.js";
6
+ import { type BuildingKitInstance, type BuildingKitModelBounds } from "./buildingKitFit.js";
7
+ /** The material values used by one kit variant, excluding unrelated document library entries. @internal */
8
+ export declare function buildingKitMaterialKey(part: BuildingKitPart, assets?: readonly MaterialAsset[]): string;
9
+ /** One material style shared by every fitted instance using it. @internal */
10
+ export interface BuildingKitStyleGroup {
11
+ key: string;
12
+ part: BuildingKitPart;
13
+ matrices: THREE.Matrix4[];
14
+ }
15
+ /** Group fitted instances while resolving each repeated variant's material identity only once. @internal */
16
+ export declare function groupBuildingKitInstances(instances: readonly BuildingKitInstance[], bounds: BuildingKitModelBounds, assets?: readonly MaterialAsset[]): BuildingKitStyleGroup[];
17
+ /** Validate named kit materials and native targets before any owned model or texture allocation. @internal */
18
+ export declare function validateBuildingKitSource(scene: THREE.Object3D, part: BuildingKitPart, assets?: readonly MaterialAsset[]): void;
19
+ /** Prepare one style's owned model materials before harvesting shared geometry for bounded instanced chunks. @internal */
20
+ export declare function buildBuildingKitSources(scene: THREE.Object3D, part: BuildingKitPart, assets?: readonly MaterialAsset[], surfaceTextures?: MaterialOverrideTextures, assetTextures?: ReadonlyMap<string, MaterialOverrideTextures>): ReturnType<typeof buildScatterModelSources>;
@@ -0,0 +1,70 @@
1
+ import * as THREE from "three";
2
+ import { validateMaterialAssignments } from "@jgengine/core/material/materialAsset";
3
+ import { applyMaterialAssignments, validateMaterialAssignmentTargets } from "../render/materialAsset.js";
4
+ import { buildScatterModelSources, disposeScatterModelSources } from "../scatter/scatterModels.js";
5
+ import { composeBuildingKitMatrix } from "./buildingKitFit.js";
6
+ import { applyBuildingSurface, surfaceKey } from "./buildingSurface.js";
7
+ /** The material values used by one kit variant, excluding unrelated document library entries. @internal */
8
+ export function buildingKitMaterialKey(part, assets = []) {
9
+ const ids = new Set(part.materialAssignments?.map(assignment => assignment.materialId));
10
+ return JSON.stringify([part.tint ?? "", surfaceKey(part.material), part.materialAssignments ?? [], assets.filter(asset => ids.has(asset.id))]);
11
+ }
12
+ /** Group fitted instances while resolving each repeated variant's material identity only once. @internal */
13
+ export function groupBuildingKitInstances(instances, bounds, assets = []) {
14
+ const byKey = new Map();
15
+ const styleKeys = new Map();
16
+ for (const instance of instances) {
17
+ const matrix = composeBuildingKitMatrix(instance, bounds, new THREE.Matrix4());
18
+ let key = styleKeys.get(instance.part);
19
+ if (key === undefined) {
20
+ key = buildingKitMaterialKey(instance.part, assets);
21
+ styleKeys.set(instance.part, key);
22
+ }
23
+ const bucket = byKey.get(key);
24
+ if (bucket === undefined)
25
+ byKey.set(key, { key, part: instance.part, matrices: [matrix] });
26
+ else
27
+ bucket.matrices.push(matrix);
28
+ }
29
+ return [...byKey.values()];
30
+ }
31
+ /** Validate named kit materials and native targets before any owned model or texture allocation. @internal */
32
+ export function validateBuildingKitSource(scene, part, assets = []) {
33
+ const assignments = part.materialAssignments ?? [];
34
+ const errors = validateMaterialAssignments(assignments, assets, { disallowedTextureRoles: ["height"] }).filter(item => item.severity === "error");
35
+ if (errors.length > 0)
36
+ throw new Error(`Building kit ${part.model}: ${errors.map(item => `${item.path}: ${item.message}`).join("; ")}`);
37
+ validateMaterialAssignmentTargets(scene, assets, assignments);
38
+ }
39
+ /** Prepare one style's owned model materials before harvesting shared geometry for bounded instanced chunks. @internal */
40
+ export function buildBuildingKitSources(scene, part, assets = [], surfaceTextures, assetTextures = new Map()) {
41
+ validateBuildingKitSource(scene, part, assets);
42
+ const prepared = buildScatterModelSources(scene, { url: part.model });
43
+ try {
44
+ const tint = part.tint === undefined || part.tint === "" ? undefined : new THREE.Color(part.tint);
45
+ prepared.root.traverseVisible(node => {
46
+ const mesh = node;
47
+ if (!mesh.isMesh)
48
+ return;
49
+ for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
50
+ if (tint !== undefined && "color" in material)
51
+ material.color.copy(tint);
52
+ if (part.material !== undefined && material.isMeshStandardMaterial)
53
+ applyBuildingSurface(material, part.material, surfaceTextures);
54
+ }
55
+ });
56
+ if (part.materialAssignments?.length)
57
+ applyMaterialAssignments(prepared.root, assets, part.materialAssignments, assetTextures);
58
+ let index = 0;
59
+ prepared.root.traverseVisible(node => {
60
+ const mesh = node;
61
+ if (mesh.isMesh)
62
+ prepared.sources[index++].material = mesh.material;
63
+ });
64
+ return prepared;
65
+ }
66
+ catch (error) {
67
+ disposeScatterModelSources(prepared.root);
68
+ throw error;
69
+ }
70
+ }
@@ -0,0 +1,11 @@
1
+ import * as THREE from "three";
2
+ /** A spatially bounded group of final, geometry-local instance matrices. @internal */
3
+ export interface BuildingSpatialChunk {
4
+ key: string;
5
+ matrices: THREE.Matrix4[];
6
+ bounds: THREE.Box3;
7
+ }
8
+ /** Partition final geometry bounds without changing instance transforms or material groups. @internal */
9
+ export declare function partitionBuildingMatrices(geometry: THREE.BufferGeometry, matrices: readonly THREE.Matrix4[], sourceMatrix?: THREE.Matrix4, cellSize?: number): BuildingSpatialChunk[];
10
+ /** Install matrices and bounds; cleanup releases only the mesh's instance buffers, retaining shared assets. @internal */
11
+ export declare function applyBuildingChunk(mesh: THREE.InstancedMesh, chunk: BuildingSpatialChunk): () => void;
@@ -0,0 +1,45 @@
1
+ import * as THREE from "three";
2
+ /** Partition final geometry bounds without changing instance transforms or material groups. @internal */
3
+ export function partitionBuildingMatrices(geometry, matrices, sourceMatrix, cellSize = 64) {
4
+ if (!Number.isFinite(cellSize) || cellSize <= 0)
5
+ throw new RangeError("Building cell size must be positive and finite");
6
+ if (geometry.boundingBox === null)
7
+ geometry.computeBoundingBox();
8
+ const geometryBounds = geometry.boundingBox;
9
+ if (geometryBounds === null || geometryBounds.isEmpty())
10
+ return [];
11
+ const chunks = new Map();
12
+ const aggregateBounds = new THREE.Box3();
13
+ const finalMatrices = [];
14
+ const bounds = new THREE.Box3();
15
+ const center = new THREE.Vector3();
16
+ for (const matrix of matrices) {
17
+ const finalMatrix = sourceMatrix === undefined ? matrix : new THREE.Matrix4().multiplyMatrices(matrix, sourceMatrix);
18
+ bounds.copy(geometryBounds).applyMatrix4(finalMatrix);
19
+ aggregateBounds.union(bounds);
20
+ finalMatrices.push(finalMatrix);
21
+ bounds.getCenter(center);
22
+ const key = `${Math.floor(center.x / cellSize)}:${Math.floor(center.z / cellSize)}`;
23
+ let chunk = chunks.get(key);
24
+ if (chunk === undefined) {
25
+ chunk = { key, matrices: [], bounds: new THREE.Box3() };
26
+ chunks.set(key, chunk);
27
+ }
28
+ chunk.matrices.push(finalMatrix);
29
+ chunk.bounds.union(bounds);
30
+ }
31
+ if (finalMatrices.length > 0 && aggregateBounds.max.x - aggregateBounds.min.x <= cellSize
32
+ && aggregateBounds.max.z - aggregateBounds.min.z <= cellSize) {
33
+ return [{ key: "compact", matrices: finalMatrices, bounds: aggregateBounds }];
34
+ }
35
+ return [...chunks.values()];
36
+ }
37
+ /** Install matrices and bounds; cleanup releases only the mesh's instance buffers, retaining shared assets. @internal */
38
+ export function applyBuildingChunk(mesh, chunk) {
39
+ chunk.matrices.forEach((matrix, index) => mesh.setMatrixAt(index, matrix));
40
+ mesh.count = chunk.matrices.length;
41
+ mesh.instanceMatrix.needsUpdate = true;
42
+ mesh.boundingBox = chunk.bounds.clone();
43
+ mesh.boundingSphere = chunk.bounds.getBoundingSphere(new THREE.Sphere());
44
+ return () => mesh.dispose();
45
+ }
@@ -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"];
@@ -28,4 +29,5 @@ export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "c
28
29
  normalLift?: number;
29
30
  roughness?: number;
30
31
  }
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;
32
+ /** Seeded grass tufts with bounded per-camera chunk submission; density and budgets stay in blades. */
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;
@@ -1,14 +1,16 @@
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 * as THREE from "three";
4
5
  import { useDisposable } from "../render/useDisposable.js";
5
- import { createGrassBladeGeometry, grassTuftCount, GRASS_TUFT_BLADES, } from "./grassGeometry.js";
6
+ import { createGrassGeometryChunks, grassTuftCount, GRASS_TUFT_BLADES, } from "./grassGeometry.js";
6
7
  import { createGrassMaterial, } from "./grassMaterial.js";
7
8
  import { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget, } from "./grassBudget.js";
8
9
  export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
9
10
  export { GRASS_TUFT_BLADES } from "./grassGeometry.js";
10
- 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 }) {
11
- const geometry = useDisposable(() => createGrassBladeGeometry({
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, coverageAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }) {
13
+ const chunks = useMemo(() => createGrassGeometryChunks({
12
14
  count,
13
15
  area,
14
16
  seed,
@@ -21,7 +23,9 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
21
23
  ...(edgeFeather === undefined ? {} : { edgeFeather }),
22
24
  ...(exclude === undefined ? {} : { exclude }),
23
25
  heightAt,
24
- }), [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]);
28
+ useDisposable(() => chunks.map((chunk) => chunk.geometry), [chunks]);
25
29
  const handle = useMemo(() => createGrassMaterial({
26
30
  colorBase,
27
31
  colorTip,
@@ -34,10 +38,61 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
34
38
  }), [colorBase, colorTip, colorGround, colorVariation, distanceFade, normalLift, roughness, wind]);
35
39
  // Budgets stay in blades (the public unit); the instance buffer carries tufts.
36
40
  const instanceCount = useMemo(() => grassTuftCount(resolveGrassInstanceBudget(count, density, area, budget), tuftBlades), [count, density, area, budget, tuftBlades]);
37
- geometry.instanceCount = Math.min(instanceCount, grassTuftCount(count, tuftBlades));
41
+ const draws = useMemo(() => {
42
+ const counts = chunks.map((chunk) => {
43
+ let low = 0, high = chunk.indices.length;
44
+ while (low < high) {
45
+ const mid = (low + high) >>> 1;
46
+ if (chunk.indices[mid] < instanceCount)
47
+ low = mid + 1;
48
+ else
49
+ high = mid;
50
+ }
51
+ return low;
52
+ });
53
+ const patchBounds = new THREE.Box3(new THREE.Vector3(-0.5, 0, 0), new THREE.Vector3(0.5, 1, 0));
54
+ const windExtent = (Math.abs(handle.uniforms.uWindStrength.value) + Math.abs(handle.uniforms.uWindFlutter.value)) * 1.2;
55
+ for (const chunk of chunks) {
56
+ chunk.bounds.copy(chunk.roots).expandByScalar(chunk.bladeExtent + windExtent);
57
+ chunk.geometry.boundingBox = chunk.bounds;
58
+ patchBounds.union(chunk.bounds);
59
+ }
60
+ const sphere = patchBounds.getBoundingSphere(new THREE.Sphere());
61
+ // All chunks retain the patch's shadow bounds; the factor also covers affine parent shear.
62
+ sphere.radius *= Math.sqrt(3);
63
+ const worldBounds = new THREE.Box3();
64
+ const cameraPosition = new THREE.Vector3();
65
+ const projection = new THREE.Matrix4();
66
+ const frustum = new THREE.Frustum();
67
+ return chunks.map((chunk, index) => {
68
+ chunk.geometry.boundingSphere = sphere;
69
+ chunk.geometry.instanceCount = counts[index];
70
+ const beforeRender = function (renderer, scene, camera, geometry, material, group) {
71
+ let visible = true;
72
+ if (frustumCulled) {
73
+ projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
74
+ frustum.setFromProjectionMatrix(projection, camera.coordinateSystem, camera.reversedDepth);
75
+ visible = frustum.intersectsBox(worldBounds.copy(chunk.bounds).applyMatrix4(this.matrixWorld));
76
+ }
77
+ const fade = handle.uniforms.uDistanceFade.value;
78
+ if (visible && fade.y > fade.x) {
79
+ camera.getWorldPosition(cameraPosition);
80
+ visible = worldBounds.copy(chunk.roots).applyMatrix4(this.matrixWorld).distanceToPoint(cameraPosition) <= fade.y;
81
+ }
82
+ chunk.geometry.instanceCount = visible ? counts[index] : 0;
83
+ onBeforeRender?.call(this, renderer, scene, camera, geometry, material, group);
84
+ };
85
+ const beforeShadow = function (...args) {
86
+ chunk.geometry.instanceCount = counts[index];
87
+ onBeforeShadow?.apply(this, args);
88
+ };
89
+ return { geometry: chunk.geometry, beforeRender, beforeShadow };
90
+ });
91
+ }, [chunks, frustumCulled, handle, instanceCount, onBeforeRender, onBeforeShadow]);
38
92
  useFrame((state) => {
39
93
  handle.uniforms.uTime.value = state.clock.elapsedTime;
40
94
  });
41
95
  useEffect(() => () => handle.material.dispose(), [handle]);
42
- return (_jsx("mesh", { ...meshProps, geometry: geometry, material: handle.material, castShadow: castShadow, receiveShadow: receiveShadow, frustumCulled: frustumCulled }));
96
+ const root = draws[0];
97
+ return (_jsx("mesh", { ...meshProps, geometry: root.geometry, material: handle.material, castShadow: castShadow, receiveShadow: receiveShadow, frustumCulled: frustumCulled, onBeforeRender: root.beforeRender, onBeforeShadow: root.beforeShadow, onAfterRender: onAfterRender, onAfterShadow: onAfterShadow, dispose: null, children: draws.slice(1).map((draw, index) => (_jsx("mesh", { geometry: draw.geometry, material: handle.material, castShadow: castShadow, receiveShadow: receiveShadow, frustumCulled: frustumCulled, onBeforeRender: draw.beforeRender, onBeforeShadow: draw.beforeShadow, onAfterRender: onAfterRender, onAfterShadow: onAfterShadow, dispose: null }, index))) }));
43
98
  }
@@ -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];
@@ -60,3 +63,13 @@ export declare function grassTuftCount(bladeCount: number, tuftBlades?: number):
60
63
  * @internal
61
64
  */
62
65
  export declare function createGrassBladeGeometry(options?: GrassBladeGeometryOptions): THREE.InstancedBufferGeometry;
66
+ /** One bounded draw, retaining the original tuft indices for density/budget prefix selection. @internal */
67
+ export interface GrassGeometryChunk {
68
+ geometry: THREE.InstancedBufferGeometry;
69
+ indices: Uint32Array;
70
+ roots: THREE.Box3;
71
+ bounds: THREE.Box3;
72
+ bladeExtent: number;
73
+ }
74
+ /** Partition one existing seeded bake into at most 64 draws without resampling any tuft. @internal */
75
+ export declare function createGrassGeometryChunks(options?: GrassBladeGeometryOptions): GrassGeometryChunk[];