@jgengine/shell 0.19.0 → 0.21.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.
- package/CHANGELOG.md +346 -0
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +113 -15
- package/dist/Shell3dPresentation.d.ts +2 -2
- package/dist/Shell3dPresentation.js +49 -7
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +11 -1
- package/dist/ShellHudPresentation.js +20 -6
- package/dist/audio/audioEngine.d.ts +16 -0
- package/dist/audio/audioEngine.js +178 -89
- package/dist/audio/audioWire.js +2 -2
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +2 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
- package/dist/camera/GameFirstPersonCamera.js +17 -13
- package/dist/camera/cameraRigs.js +8 -2
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +12 -5
- package/dist/defineGame.js +24 -9
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +14 -168
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.js +1 -1
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/drivers/FrameDriver.js +25 -21
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +9 -41
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +51 -77
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +3 -0
- package/dist/render/modelLoad.js +9 -0
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +21 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.js +76 -11
- package/dist/render/useModelAnimation.d.ts +28 -2
- package/dist/render/useModelAnimation.js +194 -69
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +14 -6
- package/dist/scene/AuthoredScene.js +30 -9
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +34 -59
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.js +13 -8
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.js +4 -0
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +209 -61
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/jgengine/SKILL.md +22 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-ui/SKILL.md +3 -1
- package/skills/jgengine-ui/api.md +204 -67
- package/skills/jgengine-ui/capabilities.md +115 -1
- package/skills/jgengine-ui/reference-primitives.md +93 -3
- package/skills/jgengine-ui/reference.md +2 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +200 -57
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +152 -5
- 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 {
|
|
8
|
-
import {
|
|
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
|
-
|
|
19
|
+
useBuildingChunkBudget(meshRef, chunk.bounds);
|
|
20
|
+
useLayoutEffect(() => {
|
|
12
21
|
const mesh = meshRef.current;
|
|
13
22
|
if (mesh === null)
|
|
14
23
|
return;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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({
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
}
|
|
@@ -28,4 +28,5 @@ export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "c
|
|
|
28
28
|
normalLift?: number;
|
|
29
29
|
roughness?: number;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
/** Seeded grass tufts with bounded per-camera chunk submission; density and budgets stay in blades. */
|
|
32
|
+
export declare function GrassField({ count, density, budget, area, seed, segments, bladeHeight, bladeWidth, bladeBend, tuftBlades, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow, receiveShadow, frustumCulled, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
|
|
@@ -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 {
|
|
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
|
-
|
|
11
|
-
|
|
11
|
+
/** Seeded grass tufts with bounded per-camera chunk submission; density and budgets stay in blades. */
|
|
12
|
+
export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }) {
|
|
13
|
+
const chunks = useMemo(() => createGrassGeometryChunks({
|
|
12
14
|
count,
|
|
13
15
|
area,
|
|
14
16
|
seed,
|
|
@@ -22,6 +24,7 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
|
|
|
22
24
|
...(exclude === undefined ? {} : { exclude }),
|
|
23
25
|
heightAt,
|
|
24
26
|
}), [area, bladeBend, bladeHeight, bladeWidth, count, edgeFeather, exclude, heightAt, seed, segments, tuftBlades, tuftRadius]);
|
|
27
|
+
useDisposable(() => chunks.map((chunk) => chunk.geometry), [chunks]);
|
|
25
28
|
const handle = useMemo(() => createGrassMaterial({
|
|
26
29
|
colorBase,
|
|
27
30
|
colorTip,
|
|
@@ -34,10 +37,61 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
|
|
|
34
37
|
}), [colorBase, colorTip, colorGround, colorVariation, distanceFade, normalLift, roughness, wind]);
|
|
35
38
|
// Budgets stay in blades (the public unit); the instance buffer carries tufts.
|
|
36
39
|
const instanceCount = useMemo(() => grassTuftCount(resolveGrassInstanceBudget(count, density, area, budget), tuftBlades), [count, density, area, budget, tuftBlades]);
|
|
37
|
-
|
|
40
|
+
const draws = useMemo(() => {
|
|
41
|
+
const counts = chunks.map((chunk) => {
|
|
42
|
+
let low = 0, high = chunk.indices.length;
|
|
43
|
+
while (low < high) {
|
|
44
|
+
const mid = (low + high) >>> 1;
|
|
45
|
+
if (chunk.indices[mid] < instanceCount)
|
|
46
|
+
low = mid + 1;
|
|
47
|
+
else
|
|
48
|
+
high = mid;
|
|
49
|
+
}
|
|
50
|
+
return low;
|
|
51
|
+
});
|
|
52
|
+
const patchBounds = new THREE.Box3(new THREE.Vector3(-0.5, 0, 0), new THREE.Vector3(0.5, 1, 0));
|
|
53
|
+
const windExtent = (Math.abs(handle.uniforms.uWindStrength.value) + Math.abs(handle.uniforms.uWindFlutter.value)) * 1.2;
|
|
54
|
+
for (const chunk of chunks) {
|
|
55
|
+
chunk.bounds.copy(chunk.roots).expandByScalar(chunk.bladeExtent + windExtent);
|
|
56
|
+
chunk.geometry.boundingBox = chunk.bounds;
|
|
57
|
+
patchBounds.union(chunk.bounds);
|
|
58
|
+
}
|
|
59
|
+
const sphere = patchBounds.getBoundingSphere(new THREE.Sphere());
|
|
60
|
+
// All chunks retain the patch's shadow bounds; the factor also covers affine parent shear.
|
|
61
|
+
sphere.radius *= Math.sqrt(3);
|
|
62
|
+
const worldBounds = new THREE.Box3();
|
|
63
|
+
const cameraPosition = new THREE.Vector3();
|
|
64
|
+
const projection = new THREE.Matrix4();
|
|
65
|
+
const frustum = new THREE.Frustum();
|
|
66
|
+
return chunks.map((chunk, index) => {
|
|
67
|
+
chunk.geometry.boundingSphere = sphere;
|
|
68
|
+
chunk.geometry.instanceCount = counts[index];
|
|
69
|
+
const beforeRender = function (renderer, scene, camera, geometry, material, group) {
|
|
70
|
+
let visible = true;
|
|
71
|
+
if (frustumCulled) {
|
|
72
|
+
projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
73
|
+
frustum.setFromProjectionMatrix(projection, camera.coordinateSystem, camera.reversedDepth);
|
|
74
|
+
visible = frustum.intersectsBox(worldBounds.copy(chunk.bounds).applyMatrix4(this.matrixWorld));
|
|
75
|
+
}
|
|
76
|
+
const fade = handle.uniforms.uDistanceFade.value;
|
|
77
|
+
if (visible && fade.y > fade.x) {
|
|
78
|
+
camera.getWorldPosition(cameraPosition);
|
|
79
|
+
visible = worldBounds.copy(chunk.roots).applyMatrix4(this.matrixWorld).distanceToPoint(cameraPosition) <= fade.y;
|
|
80
|
+
}
|
|
81
|
+
chunk.geometry.instanceCount = visible ? counts[index] : 0;
|
|
82
|
+
onBeforeRender?.call(this, renderer, scene, camera, geometry, material, group);
|
|
83
|
+
};
|
|
84
|
+
const beforeShadow = function (...args) {
|
|
85
|
+
chunk.geometry.instanceCount = counts[index];
|
|
86
|
+
onBeforeShadow?.apply(this, args);
|
|
87
|
+
};
|
|
88
|
+
return { geometry: chunk.geometry, beforeRender, beforeShadow };
|
|
89
|
+
});
|
|
90
|
+
}, [chunks, frustumCulled, handle, instanceCount, onBeforeRender, onBeforeShadow]);
|
|
38
91
|
useFrame((state) => {
|
|
39
92
|
handle.uniforms.uTime.value = state.clock.elapsedTime;
|
|
40
93
|
});
|
|
41
94
|
useEffect(() => () => handle.material.dispose(), [handle]);
|
|
42
|
-
|
|
95
|
+
const root = draws[0];
|
|
96
|
+
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
97
|
}
|
|
@@ -60,3 +60,13 @@ export declare function grassTuftCount(bladeCount: number, tuftBlades?: number):
|
|
|
60
60
|
* @internal
|
|
61
61
|
*/
|
|
62
62
|
export declare function createGrassBladeGeometry(options?: GrassBladeGeometryOptions): THREE.InstancedBufferGeometry;
|
|
63
|
+
/** One bounded draw, retaining the original tuft indices for density/budget prefix selection. @internal */
|
|
64
|
+
export interface GrassGeometryChunk {
|
|
65
|
+
geometry: THREE.InstancedBufferGeometry;
|
|
66
|
+
indices: Uint32Array;
|
|
67
|
+
roots: THREE.Box3;
|
|
68
|
+
bounds: THREE.Box3;
|
|
69
|
+
bladeExtent: number;
|
|
70
|
+
}
|
|
71
|
+
/** Partition one existing seeded bake into at most 64 draws without resampling any tuft. @internal */
|
|
72
|
+
export declare function createGrassGeometryChunks(options?: GrassBladeGeometryOptions): GrassGeometryChunk[];
|
|
@@ -140,3 +140,58 @@ export function createGrassBladeGeometry(options = {}) {
|
|
|
140
140
|
geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), Math.max(size.width, size.depth) * 0.75 + resolved.height[1]);
|
|
141
141
|
return geometry;
|
|
142
142
|
}
|
|
143
|
+
/** Partition one existing seeded bake into at most 64 draws without resampling any tuft. @internal */
|
|
144
|
+
export function createGrassGeometryChunks(options = {}) {
|
|
145
|
+
const source = createGrassBladeGeometry(options);
|
|
146
|
+
const size = resolveTerrainSize(options.area ?? 40);
|
|
147
|
+
const columns = Math.max(1, Math.min(8, Math.ceil(size.width / 16)));
|
|
148
|
+
const rows = Math.max(1, Math.min(8, Math.ceil(size.depth / 16)));
|
|
149
|
+
const buckets = Array.from({ length: columns * rows }, () => []);
|
|
150
|
+
const offsets = source.getAttribute("instanceOffset");
|
|
151
|
+
if (source.instanceCount === 0)
|
|
152
|
+
return [{ geometry: source, indices: new Uint32Array(), roots: new THREE.Box3(), bounds: new THREE.Box3(), bladeExtent: 0 }];
|
|
153
|
+
for (let i = 0; i < source.instanceCount; i++) {
|
|
154
|
+
const x = Math.max(0, Math.min(columns - 1, Math.floor(size.width > 0 ? (offsets.getX(i) / size.width + 0.5) * columns : 0)));
|
|
155
|
+
const z = Math.max(0, Math.min(rows - 1, Math.floor(size.depth > 0 ? (offsets.getZ(i) / size.depth + 0.5) * rows : 0)));
|
|
156
|
+
buckets[z * columns + x].push(i);
|
|
157
|
+
}
|
|
158
|
+
const vary = source.getAttribute("bladeVary");
|
|
159
|
+
const bendScale = source.getAttribute("bladeBendScale");
|
|
160
|
+
const bladeOffset = source.getAttribute("bladeOffset");
|
|
161
|
+
let heightScale = 0, widthScale = 0, bendFactor = 0, radius = 0;
|
|
162
|
+
for (let i = 0; i < vary.count; i++) {
|
|
163
|
+
heightScale = Math.max(heightScale, Math.abs(vary.getY(i)));
|
|
164
|
+
widthScale = Math.max(widthScale, Math.abs(vary.getZ(i)));
|
|
165
|
+
bendFactor = Math.max(bendFactor, Math.abs(bendScale.getX(i)));
|
|
166
|
+
radius = Math.max(radius, Math.hypot(bladeOffset.getX(i), bladeOffset.getY(i)));
|
|
167
|
+
}
|
|
168
|
+
const point = new THREE.Vector3();
|
|
169
|
+
const chunks = buckets.filter((indices) => indices.length > 0).map((indices) => {
|
|
170
|
+
const geometry = new THREE.InstancedBufferGeometry();
|
|
171
|
+
geometry.setIndex(source.index);
|
|
172
|
+
for (const [name, attribute] of Object.entries(source.attributes)) {
|
|
173
|
+
if (!(attribute instanceof THREE.InstancedBufferAttribute)) {
|
|
174
|
+
geometry.setAttribute(name, attribute);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const values = new Float32Array(indices.length * attribute.itemSize);
|
|
178
|
+
for (let i = 0; i < indices.length; i++) {
|
|
179
|
+
values.set(attribute.array.subarray(indices[i] * attribute.itemSize, (indices[i] + 1) * attribute.itemSize), i * attribute.itemSize);
|
|
180
|
+
}
|
|
181
|
+
geometry.setAttribute(name, new THREE.InstancedBufferAttribute(values, attribute.itemSize));
|
|
182
|
+
}
|
|
183
|
+
const roots = new THREE.Box3();
|
|
184
|
+
let extent = radius;
|
|
185
|
+
for (const i of indices) {
|
|
186
|
+
roots.expandByPoint(point.set(offsets.getX(i), offsets.getY(i), offsets.getZ(i)));
|
|
187
|
+
const height = Math.abs(source.getAttribute("instanceHeight").getX(i)) * heightScale;
|
|
188
|
+
const bend = Math.abs(source.getAttribute("instanceBend").getX(i)) * bendFactor;
|
|
189
|
+
const width = Math.abs(source.getAttribute("instanceWidth").getX(i)) * widthScale * 1.2;
|
|
190
|
+
extent = Math.max(extent, radius + width + height * (1 + bend * 1.18));
|
|
191
|
+
}
|
|
192
|
+
geometry.instanceCount = indices.length;
|
|
193
|
+
return { geometry, indices: Uint32Array.from(indices), roots, bounds: roots.clone(), bladeExtent: extent };
|
|
194
|
+
});
|
|
195
|
+
source.dispose();
|
|
196
|
+
return chunks;
|
|
197
|
+
}
|
|
@@ -111,7 +111,7 @@ void jgComputeGrassBlade() {
|
|
|
111
111
|
// triangle blades leave the ground visible between tufts and read as stubble.
|
|
112
112
|
float taper = (1.0 - 0.3 * t) * (1.0 - smoothstep(0.5, 1.0, t) * 0.95);
|
|
113
113
|
// Surviving distant tufts widen to hold silhouette coverage as neighbors thin out.
|
|
114
|
-
float width = instanceWidth * bladeVary.z * taper * (1.0 + fadeT * 1.4);
|
|
114
|
+
float width = instanceWidth * bladeVary.z * taper * (1.0 + fadeT * 1.4) * keep;
|
|
115
115
|
float arc = instanceBend * bladeBendScale * t * t;
|
|
116
116
|
vec3 localPosition = vec3(side * width, heightScale * t - arc * heightScale * 0.18, arc * heightScale);
|
|
117
117
|
vec3 localNormal = normalize(vec3(0.0, -instanceBend * bladeBendScale * t, 1.0));
|
|
@@ -134,7 +134,7 @@ ${wind.layered
|
|
|
134
134
|
+ sin(gustPhase * 1.7 + crossPhase * 0.4 + 4.3) * 0.13;`
|
|
135
135
|
: ` float gust = sin(gustPhase) * 0.7 + sin(gustPhase * 0.43 + 2.4) * 0.3;`}
|
|
136
136
|
float flutter = sin(uTime * 7.5 + instancePhase * 2.7 + bladeVary.w * 6.2832) * uWindFlutter;
|
|
137
|
-
vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t * (0.4 + 0.6 * bladeVary.y);
|
|
137
|
+
vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t * (0.4 + 0.6 * bladeVary.y) * keep;
|
|
138
138
|
jgGrassPosition = instanceOffset + tuftOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
|
|
139
139
|
// Lifting normals toward straight-up lights the field like a continuous meadow surface —
|
|
140
140
|
// side-lit individual blade planes read as dark stubble, not turf.
|
|
@@ -7,13 +7,13 @@ export interface TerrainDetailMaterialHandle {
|
|
|
7
7
|
/**
|
|
8
8
|
* Loaded PBR textures matching a resolved `ResolvedTerrainDetailMaterial.maps`' roles. The caller
|
|
9
9
|
* (a React component, via `useTexture`/`useLoader`) owns loading and disposal; `core` never touches
|
|
10
|
-
* `THREE.Texture` and this shell function never fetches a URL itself.
|
|
10
|
+
* `THREE.Texture` and this shell function never fetches a URL itself. Omitted AO/roughness skip sampling.
|
|
11
11
|
*/
|
|
12
12
|
export interface TerrainDetailMaterialTextures {
|
|
13
13
|
color: THREE.Texture;
|
|
14
14
|
normal: THREE.Texture;
|
|
15
|
-
roughness
|
|
16
|
-
ao
|
|
15
|
+
roughness?: THREE.Texture;
|
|
16
|
+
ao?: THREE.Texture;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex
|