@jgengine/shell 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -12
- package/dist/GamePlayerShell.js +67 -1332
- package/dist/GameUiPreview.js +3 -2
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +128 -21
- package/dist/audio/audioWire.d.ts +40 -0
- package/dist/audio/audioWire.js +62 -0
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +8 -3
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +114 -24
- package/dist/camera/rigMath.js +129 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +50 -9
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
- package/dist/devtools/DevtoolsOverlay.js +47 -462
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +266 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +44 -9
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +56 -11
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +4 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +76 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +54 -3
- package/dist/render/resolveModel.js +116 -4
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +135 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +118 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +84 -0
- package/dist/scene/AuthoredScene.js +178 -0
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +73 -0
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +84 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +61 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +26 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +30 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +7 -1
- package/dist/settings/appliedSettings.js +13 -2
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +53 -0
- package/dist/structures/TransformGizmo.js +59 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +35 -0
- package/dist/terrain/grassGeometry.js +85 -20
- package/dist/terrain/grassMaterial.d.ts +20 -0
- package/dist/terrain/grassMaterial.js +68 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +23 -0
- package/dist/terrain/soilPatchMaterial.js +147 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +87 -7
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +11 -0
- package/dist/water/OceanConfig.js +8 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +49 -2
- package/dist/world/WorldHud.js +87 -2
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +186 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +11 -0
- package/dist/worldSky.js +21 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +51 -0
- package/skills/jgengine-world/api.md +3056 -0
- package/skills/jgengine-world/capabilities.md +476 -0
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +261 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/llms.txt +0 -2761
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useMemo } from "react";
|
|
4
|
-
import { createGrassBladeGeometry } from "./grassGeometry.js";
|
|
5
|
-
import { createGrassMaterial } from "./grassMaterial.js";
|
|
4
|
+
import { createGrassBladeGeometry, grassTuftCount, GRASS_TUFT_BLADES, } from "./grassGeometry.js";
|
|
5
|
+
import { createGrassMaterial, } from "./grassMaterial.js";
|
|
6
6
|
import { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget, } from "./grassBudget.js";
|
|
7
7
|
export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
|
|
8
|
-
export
|
|
8
|
+
export { GRASS_TUFT_BLADES } from "./grassGeometry.js";
|
|
9
|
+
export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, ...meshProps }) {
|
|
9
10
|
const geometry = useMemo(() => createGrassBladeGeometry({
|
|
10
11
|
count,
|
|
11
12
|
area,
|
|
@@ -14,16 +15,25 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
|
|
|
14
15
|
height: bladeHeight,
|
|
15
16
|
width: bladeWidth,
|
|
16
17
|
bend: bladeBend,
|
|
18
|
+
...(tuftBlades === undefined ? {} : { tuftBlades }),
|
|
19
|
+
...(tuftRadius === undefined ? {} : { tuftRadius }),
|
|
20
|
+
...(edgeFeather === undefined ? {} : { edgeFeather }),
|
|
21
|
+
...(exclude === undefined ? {} : { exclude }),
|
|
17
22
|
heightAt,
|
|
18
|
-
}), [area, bladeBend, bladeHeight, bladeWidth, count, heightAt, seed, segments]);
|
|
23
|
+
}), [area, bladeBend, bladeHeight, bladeWidth, count, edgeFeather, exclude, heightAt, seed, segments, tuftBlades, tuftRadius]);
|
|
19
24
|
const handle = useMemo(() => createGrassMaterial({
|
|
20
25
|
colorBase,
|
|
21
26
|
colorTip,
|
|
27
|
+
colorGround,
|
|
22
28
|
colorVariation,
|
|
23
29
|
wind,
|
|
30
|
+
distanceFade,
|
|
31
|
+
normalLift,
|
|
24
32
|
roughness,
|
|
25
|
-
}), [colorBase, colorTip, colorVariation, roughness, wind]);
|
|
26
|
-
|
|
33
|
+
}), [colorBase, colorTip, colorGround, colorVariation, distanceFade, normalLift, roughness, wind]);
|
|
34
|
+
// Budgets stay in blades (the public unit); the instance buffer carries tufts.
|
|
35
|
+
const instanceCount = useMemo(() => grassTuftCount(resolveGrassInstanceBudget(count, density, area, budget), tuftBlades), [count, density, area, budget, tuftBlades]);
|
|
36
|
+
geometry.instanceCount = Math.min(instanceCount, grassTuftCount(count, tuftBlades));
|
|
27
37
|
useFrame((state) => {
|
|
28
38
|
handle.uniforms.uTime.value = state.clock.elapsedTime;
|
|
29
39
|
});
|
|
@@ -2,7 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
const MODE_COLOR = {
|
|
3
3
|
raise: "#38bdf8",
|
|
4
4
|
lower: "#fb923c",
|
|
5
|
+
smooth: "#c084fc",
|
|
5
6
|
flatten: "#a3e635",
|
|
7
|
+
noise: "#facc15",
|
|
8
|
+
ramp: "#2dd4bf",
|
|
6
9
|
paint: "#e879f9",
|
|
7
10
|
};
|
|
8
11
|
export function TerraformBrushCursor({ center, y = 0.05, radius, mode }) {
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { type TerrainArea } from "./terrainMath.js";
|
|
1
2
|
export declare const DEFAULT_GRASS_COUNT = 1500;
|
|
2
|
-
|
|
3
|
-
export declare
|
|
3
|
+
/** Blades per square meter — matches `@jgengine/core/world/vegetation`'s `VEGETATION_DEFAULTS.density`, so an editor-placed grass volume and a bare `<GrassField>` agree on what "4" means. */
|
|
4
|
+
export declare const DEFAULT_GRASS_DENSITY = 4;
|
|
5
|
+
/**
|
|
6
|
+
* Blade instance count for a patch: `density` (blades/m²) times the patch's
|
|
7
|
+
* `area`, capped at `budget` (defaulting to `count`, the buffer's allocated
|
|
8
|
+
* capacity) so a big field never exceeds the perf ceiling — it just renders
|
|
9
|
+
* sparser than requested and logs a devtools warning when that happens.
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveGrassInstanceBudget(count: number, density: number, area: TerrainArea, budget?: number): number;
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
2
|
+
import { resolveTerrainSize } from "./terrainMath.js";
|
|
1
3
|
export const DEFAULT_GRASS_COUNT = 1500;
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/** Blades per square meter — matches `@jgengine/core/world/vegetation`'s `VEGETATION_DEFAULTS.density`, so an editor-placed grass volume and a bare `<GrassField>` agree on what "4" means. */
|
|
5
|
+
export const DEFAULT_GRASS_DENSITY = 4;
|
|
6
|
+
/**
|
|
7
|
+
* Blade instance count for a patch: `density` (blades/m²) times the patch's
|
|
8
|
+
* `area`, capped at `budget` (defaulting to `count`, the buffer's allocated
|
|
9
|
+
* capacity) so a big field never exceeds the perf ceiling — it just renders
|
|
10
|
+
* sparser than requested and logs a devtools warning when that happens.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export function resolveGrassInstanceBudget(count, density, area, budget) {
|
|
14
|
+
const size = resolveTerrainSize(area);
|
|
15
|
+
const areaSqMeters = Math.max(0, size.width) * Math.max(0, size.depth);
|
|
16
|
+
const requested = Math.max(0, density) * areaSqMeters;
|
|
17
|
+
const cap = budget === undefined ? count : Math.min(count, Math.max(0, Math.floor(budget)));
|
|
18
|
+
if (requested > cap) {
|
|
19
|
+
devtools.logs.push("warn", `[jgengine:grass] density ${density} blades/m² over ${areaSqMeters.toFixed(0)}m² wants ${Math.floor(requested)} blades — clamped to budget ${cap}`);
|
|
20
|
+
}
|
|
21
|
+
return Math.floor(Math.min(Math.max(0, cap), requested));
|
|
6
22
|
}
|
|
@@ -2,7 +2,19 @@ import * as THREE from "three";
|
|
|
2
2
|
import { type TerrainSeed } from "./random.js";
|
|
3
3
|
import { type TerrainArea, type TerrainHeightSampler } from "./terrainMath.js";
|
|
4
4
|
export type GrassRange = number | readonly [min: number, max: number];
|
|
5
|
+
/** Blades baked into each tuft instance — one instance reads as a clump, not a lone blade. */
|
|
6
|
+
export declare const GRASS_TUFT_BLADES = 5;
|
|
7
|
+
/** A rectangle (patch-local XZ) the grass bake keeps clear — soil patches, water, plazas. */
|
|
8
|
+
export interface GrassExclusion {
|
|
9
|
+
/** Rect center in patch-local coordinates. */
|
|
10
|
+
center: readonly [x: number, z: number];
|
|
11
|
+
/** Rect half extents. */
|
|
12
|
+
half: readonly [x: number, z: number];
|
|
13
|
+
/** Ramp width (m) outside the rect over which blades regrow to full height. Default 1.5. */
|
|
14
|
+
feather?: number;
|
|
15
|
+
}
|
|
5
16
|
export interface GrassBladeGeometryOptions {
|
|
17
|
+
/** Total blade budget for the patch (instances are tufts of `tuftBlades` blades each). */
|
|
6
18
|
count?: number;
|
|
7
19
|
area?: TerrainArea;
|
|
8
20
|
seed?: TerrainSeed;
|
|
@@ -10,6 +22,14 @@ export interface GrassBladeGeometryOptions {
|
|
|
10
22
|
height?: GrassRange;
|
|
11
23
|
width?: GrassRange;
|
|
12
24
|
bend?: GrassRange;
|
|
25
|
+
/** Blades merged into each instanced tuft. Default {@link GRASS_TUFT_BLADES}. */
|
|
26
|
+
tuftBlades?: number;
|
|
27
|
+
/** Tuft spread radius in meters — how far clump blades splay from the tuft root. Default 0.16. */
|
|
28
|
+
tuftRadius?: number;
|
|
29
|
+
/** Meters of density/height feathering at the patch border so the field ends in a soft fringe, not a mowed rectangle. Default 1.5; 0 disables. */
|
|
30
|
+
edgeFeather?: number;
|
|
31
|
+
/** Patch-local rectangles kept clear of blades (soil patches, water) with a soft regrow ramp. */
|
|
32
|
+
exclude?: readonly GrassExclusion[];
|
|
13
33
|
heightAt?: TerrainHeightSampler;
|
|
14
34
|
}
|
|
15
35
|
export interface ResolvedGrassBladeGeometryOptions {
|
|
@@ -20,8 +40,23 @@ export interface ResolvedGrassBladeGeometryOptions {
|
|
|
20
40
|
height: readonly [min: number, max: number];
|
|
21
41
|
width: readonly [min: number, max: number];
|
|
22
42
|
bend: readonly [min: number, max: number];
|
|
43
|
+
tuftBlades: number;
|
|
44
|
+
tuftRadius: number;
|
|
45
|
+
edgeFeather: number;
|
|
46
|
+
exclude: readonly GrassExclusion[];
|
|
23
47
|
heightAt: TerrainHeightSampler;
|
|
24
48
|
}
|
|
49
|
+
/** @internal */
|
|
25
50
|
export declare function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number];
|
|
51
|
+
/** @internal */
|
|
26
52
|
export declare function resolveGrassBladeGeometryOptions(options?: GrassBladeGeometryOptions): ResolvedGrassBladeGeometryOptions;
|
|
53
|
+
/** Tuft instances needed to carry `bladeCount` blades at `tuftBlades` blades per tuft. @internal */
|
|
54
|
+
export declare function grassTuftCount(bladeCount: number, tuftBlades?: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Instanced tuft geometry: the template mesh is a clump of `tuftBlades` blades splayed around the
|
|
57
|
+
* tuft root (per-blade yaw/offset/scale baked as vertex attributes), and each instance places one
|
|
58
|
+
* whole clump. Clumps read as turf at a fraction of the instance count single blades need, and the
|
|
59
|
+
* per-instance yaw/scale jitter hides the shared clump layout.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
27
62
|
export declare function createGrassBladeGeometry(options?: GrassBladeGeometryOptions): THREE.InstancedBufferGeometry;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { createSeededRandom } from "./random.js";
|
|
3
3
|
import { resolveTerrainSize } from "./terrainMath.js";
|
|
4
|
+
/** Blades baked into each tuft instance — one instance reads as a clump, not a lone blade. */
|
|
5
|
+
export const GRASS_TUFT_BLADES = 5;
|
|
6
|
+
/** @internal */
|
|
4
7
|
export function resolveGrassRange(value, fallback) {
|
|
5
8
|
if (value === undefined)
|
|
6
9
|
return fallback;
|
|
7
10
|
return typeof value === "number" ? [value, value] : value;
|
|
8
11
|
}
|
|
12
|
+
/** @internal */
|
|
9
13
|
export function resolveGrassBladeGeometryOptions(options = {}) {
|
|
10
14
|
return {
|
|
11
15
|
count: Math.max(0, Math.floor(options.count ?? 1500)),
|
|
@@ -13,52 +17,113 @@ export function resolveGrassBladeGeometryOptions(options = {}) {
|
|
|
13
17
|
seed: options.seed ?? 1,
|
|
14
18
|
segments: Math.max(1, Math.floor(options.segments ?? 4)),
|
|
15
19
|
height: resolveGrassRange(options.height, [0.55, 1.1]),
|
|
16
|
-
width: resolveGrassRange(options.width, [0.
|
|
17
|
-
bend: resolveGrassRange(options.bend, [0.
|
|
20
|
+
width: resolveGrassRange(options.width, [0.05, 0.1]),
|
|
21
|
+
bend: resolveGrassRange(options.bend, [0.12, 0.42]),
|
|
22
|
+
tuftBlades: Math.max(1, Math.floor(options.tuftBlades ?? GRASS_TUFT_BLADES)),
|
|
23
|
+
tuftRadius: Math.max(0, options.tuftRadius ?? 0.16),
|
|
24
|
+
edgeFeather: Math.max(0, options.edgeFeather ?? 1.5),
|
|
25
|
+
exclude: options.exclude ?? [],
|
|
18
26
|
heightAt: options.heightAt ?? (() => 0),
|
|
19
27
|
};
|
|
20
28
|
}
|
|
29
|
+
/** Tuft instances needed to carry `bladeCount` blades at `tuftBlades` blades per tuft. @internal */
|
|
30
|
+
export function grassTuftCount(bladeCount, tuftBlades = GRASS_TUFT_BLADES) {
|
|
31
|
+
return Math.ceil(Math.max(0, bladeCount) / Math.max(1, tuftBlades));
|
|
32
|
+
}
|
|
21
33
|
function randomRange(random, range) {
|
|
22
34
|
return THREE.MathUtils.lerp(range[0], range[1], random());
|
|
23
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Instanced tuft geometry: the template mesh is a clump of `tuftBlades` blades splayed around the
|
|
38
|
+
* tuft root (per-blade yaw/offset/scale baked as vertex attributes), and each instance places one
|
|
39
|
+
* whole clump. Clumps read as turf at a fraction of the instance count single blades need, and the
|
|
40
|
+
* per-instance yaw/scale jitter hides the shared clump layout.
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
24
43
|
export function createGrassBladeGeometry(options = {}) {
|
|
25
44
|
const resolved = resolveGrassBladeGeometryOptions(options);
|
|
26
45
|
const size = resolveTerrainSize(resolved.area);
|
|
27
46
|
const positions = [];
|
|
28
47
|
const normals = [];
|
|
29
48
|
const uvs = [];
|
|
49
|
+
const bladeOffsets = [];
|
|
50
|
+
const bladeVary = [];
|
|
51
|
+
const bladeBendScale = [];
|
|
30
52
|
const indices = [];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
// Blade layout inside the tuft is baked once from the seed, shared by every instance.
|
|
54
|
+
const templateRandom = createSeededRandom(typeof resolved.seed === "number" ? resolved.seed + 7919 : `${resolved.seed}/tuft`);
|
|
55
|
+
const vertsPerBlade = (resolved.segments + 1) * 2;
|
|
56
|
+
for (let blade = 0; blade < resolved.tuftBlades; blade += 1) {
|
|
57
|
+
// First blade anchors the root; the rest ring outward with jittered angle/radius.
|
|
58
|
+
const angle = blade === 0 ? 0 : ((blade - 1) / Math.max(1, resolved.tuftBlades - 1)) * Math.PI * 2 + templateRandom() * 1.4;
|
|
59
|
+
const radius = blade === 0 ? 0 : resolved.tuftRadius * (0.35 + 0.65 * templateRandom());
|
|
60
|
+
const offsetX = Math.cos(angle) * radius;
|
|
61
|
+
const offsetZ = Math.sin(angle) * radius;
|
|
62
|
+
// Bend direction points away from the tuft center (plus jitter) so the clump splays open.
|
|
63
|
+
const yaw = angle + Math.PI / 2 + (templateRandom() - 0.5) * 1.6;
|
|
64
|
+
const heightScale = 0.72 + templateRandom() * 0.45;
|
|
65
|
+
const widthScale = 0.8 + templateRandom() * 0.4;
|
|
66
|
+
const phase = templateRandom();
|
|
67
|
+
const bendScale = 0.7 + templateRandom() * 0.7;
|
|
68
|
+
for (let segment = 0; segment <= resolved.segments; segment += 1) {
|
|
69
|
+
const t = segment / resolved.segments;
|
|
70
|
+
positions.push(-0.5, t, 0, 0.5, t, 0);
|
|
71
|
+
normals.push(0, 0, 1, 0, 0, 1);
|
|
72
|
+
uvs.push(0, t, 1, t);
|
|
73
|
+
for (let corner = 0; corner < 2; corner += 1) {
|
|
74
|
+
bladeOffsets.push(offsetX, offsetZ);
|
|
75
|
+
bladeVary.push(yaw, heightScale, widthScale, phase);
|
|
76
|
+
bladeBendScale.push(bendScale);
|
|
77
|
+
}
|
|
78
|
+
if (segment < resolved.segments) {
|
|
79
|
+
const base = blade * vertsPerBlade + segment * 2;
|
|
80
|
+
indices.push(base, base + 1, base + 3, base, base + 3, base + 2);
|
|
81
|
+
}
|
|
39
82
|
}
|
|
40
83
|
}
|
|
41
84
|
const geometry = new THREE.InstancedBufferGeometry();
|
|
42
85
|
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
|
43
86
|
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
|
|
44
87
|
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
|
|
88
|
+
geometry.setAttribute("bladeOffset", new THREE.Float32BufferAttribute(bladeOffsets, 2));
|
|
89
|
+
geometry.setAttribute("bladeVary", new THREE.Float32BufferAttribute(bladeVary, 4));
|
|
90
|
+
geometry.setAttribute("bladeBendScale", new THREE.Float32BufferAttribute(bladeBendScale, 1));
|
|
45
91
|
geometry.setIndex(indices);
|
|
92
|
+
const tuftCount = grassTuftCount(resolved.count, resolved.tuftBlades);
|
|
46
93
|
const random = createSeededRandom(resolved.seed);
|
|
47
|
-
const offsets = new Float32Array(
|
|
48
|
-
const yaw = new Float32Array(
|
|
49
|
-
const heights = new Float32Array(
|
|
50
|
-
const widths = new Float32Array(
|
|
51
|
-
const bend = new Float32Array(
|
|
52
|
-
const phase = new Float32Array(
|
|
53
|
-
const colorMix = new Float32Array(
|
|
54
|
-
|
|
94
|
+
const offsets = new Float32Array(tuftCount * 3);
|
|
95
|
+
const yaw = new Float32Array(tuftCount);
|
|
96
|
+
const heights = new Float32Array(tuftCount);
|
|
97
|
+
const widths = new Float32Array(tuftCount);
|
|
98
|
+
const bend = new Float32Array(tuftCount);
|
|
99
|
+
const phase = new Float32Array(tuftCount);
|
|
100
|
+
const colorMix = new Float32Array(tuftCount);
|
|
101
|
+
const halfW = size.width / 2;
|
|
102
|
+
const halfD = size.depth / 2;
|
|
103
|
+
for (let index = 0; index < tuftCount; index += 1) {
|
|
55
104
|
const x = (random() - 0.5) * size.width;
|
|
56
105
|
const z = (random() - 0.5) * size.depth;
|
|
57
106
|
offsets[index * 3] = x;
|
|
58
107
|
offsets[index * 3 + 1] = resolved.heightAt(x, z);
|
|
59
108
|
offsets[index * 3 + 2] = z;
|
|
60
109
|
yaw[index] = random() * Math.PI * 2;
|
|
61
|
-
|
|
110
|
+
let heightScale = randomRange(random, resolved.height);
|
|
111
|
+
// Two random draws happen on every path so the seeded stream stays aligned regardless of feather.
|
|
112
|
+
const gate = random();
|
|
113
|
+
const settle = random();
|
|
114
|
+
if (resolved.edgeFeather > 0) {
|
|
115
|
+
// Tufts drop out probabilistically across the feather band (mostly keeping full height),
|
|
116
|
+
// so the border reads as grass growing sparser — not a mowed short-pile strip.
|
|
117
|
+
const edge = Math.min(halfW - Math.abs(x), halfD - Math.abs(z));
|
|
118
|
+
const inside = Math.min(1, Math.max(0, edge / resolved.edgeFeather));
|
|
119
|
+
heightScale *= gate < inside * inside ? 0.7 + 0.3 * settle : 0;
|
|
120
|
+
}
|
|
121
|
+
for (const zone of resolved.exclude) {
|
|
122
|
+
// Signed distance outside the rect: blades vanish inside and regrow across the feather ramp.
|
|
123
|
+
const d = Math.max(Math.abs(x - zone.center[0]) - zone.half[0], Math.abs(z - zone.center[1]) - zone.half[1]);
|
|
124
|
+
heightScale *= Math.min(1, Math.max(0, d / Math.max(0.001, zone.feather ?? 1.5)));
|
|
125
|
+
}
|
|
126
|
+
heights[index] = heightScale;
|
|
62
127
|
widths[index] = randomRange(random, resolved.width);
|
|
63
128
|
bend[index] = randomRange(random, resolved.bend);
|
|
64
129
|
phase[index] = random() * Math.PI * 2;
|
|
@@ -71,7 +136,7 @@ export function createGrassBladeGeometry(options = {}) {
|
|
|
71
136
|
geometry.setAttribute("instanceBend", new THREE.InstancedBufferAttribute(bend, 1));
|
|
72
137
|
geometry.setAttribute("instancePhase", new THREE.InstancedBufferAttribute(phase, 1));
|
|
73
138
|
geometry.setAttribute("instanceColorMix", new THREE.InstancedBufferAttribute(colorMix, 1));
|
|
74
|
-
geometry.instanceCount =
|
|
139
|
+
geometry.instanceCount = tuftCount;
|
|
75
140
|
geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), Math.max(size.width, size.depth) * 0.75 + resolved.height[1]);
|
|
76
141
|
return geometry;
|
|
77
142
|
}
|
|
@@ -6,11 +6,22 @@ export interface GrassWindOptions {
|
|
|
6
6
|
gustScale?: number;
|
|
7
7
|
flutter?: number;
|
|
8
8
|
}
|
|
9
|
+
/** Camera-distance fade band: tufts thin out between `start` and `end` meters, so the instance budget spends where the camera lives. */
|
|
10
|
+
export interface GrassDistanceFadeOptions {
|
|
11
|
+
start?: number;
|
|
12
|
+
end?: number;
|
|
13
|
+
}
|
|
9
14
|
export interface GrassMaterialOptions {
|
|
10
15
|
colorBase?: THREE.ColorRepresentation;
|
|
11
16
|
colorTip?: THREE.ColorRepresentation;
|
|
17
|
+
/** Ground color the blade roots blend into (pass the terrain color under the patch so blades grow *out of* the ground instead of floating on it). Defaults to a darkened `colorBase`. */
|
|
18
|
+
colorGround?: THREE.ColorRepresentation;
|
|
12
19
|
colorVariation?: number;
|
|
13
20
|
wind?: GrassWindOptions | false;
|
|
21
|
+
/** Distance thinning; `false` disables. Default fades between 35 m and 95 m. */
|
|
22
|
+
distanceFade?: GrassDistanceFadeOptions | false;
|
|
23
|
+
/** 0..1 blend of blade normals toward straight-up — lit like a soft carpet instead of dark individual planes. Default 0.6. */
|
|
24
|
+
normalLift?: number;
|
|
14
25
|
roughness?: number;
|
|
15
26
|
}
|
|
16
27
|
export interface GrassShaderUniforms {
|
|
@@ -22,12 +33,21 @@ export interface GrassShaderUniforms {
|
|
|
22
33
|
uWindFlutter: THREE.IUniform<number>;
|
|
23
34
|
uColorBase: THREE.IUniform<THREE.Color>;
|
|
24
35
|
uColorTip: THREE.IUniform<THREE.Color>;
|
|
36
|
+
uColorGround: THREE.IUniform<THREE.Color>;
|
|
25
37
|
uColorVariation: THREE.IUniform<number>;
|
|
38
|
+
uDistanceFade: THREE.IUniform<THREE.Vector2>;
|
|
39
|
+
uNormalLift: THREE.IUniform<number>;
|
|
26
40
|
}
|
|
27
41
|
export interface GrassMaterialHandle {
|
|
28
42
|
material: THREE.MeshStandardMaterial;
|
|
29
43
|
uniforms: GrassShaderUniforms;
|
|
30
44
|
}
|
|
31
45
|
export declare const DEFAULT_GRASS_WIND: Required<GrassWindOptions>;
|
|
46
|
+
/** Default camera-distance fade band: tufts start thinning at 35 m and are gone by 95 m. */
|
|
47
|
+
export declare const DEFAULT_GRASS_DISTANCE_FADE: Required<GrassDistanceFadeOptions>;
|
|
48
|
+
/** @internal */
|
|
32
49
|
export declare function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>;
|
|
50
|
+
/** @internal */
|
|
51
|
+
export declare function resolveGrassDistanceFade(fade: GrassDistanceFadeOptions | false | undefined): Required<GrassDistanceFadeOptions>;
|
|
52
|
+
/** @internal */
|
|
33
53
|
export declare function createGrassMaterial(options?: GrassMaterialOptions): GrassMaterialHandle;
|
|
@@ -6,10 +6,16 @@ export const DEFAULT_GRASS_WIND = {
|
|
|
6
6
|
gustScale: 0.16,
|
|
7
7
|
flutter: 0.08,
|
|
8
8
|
};
|
|
9
|
+
/** Default camera-distance fade band: tufts start thinning at 35 m and are gone by 95 m. */
|
|
10
|
+
export const DEFAULT_GRASS_DISTANCE_FADE = {
|
|
11
|
+
start: 35,
|
|
12
|
+
end: 95,
|
|
13
|
+
};
|
|
9
14
|
function normalizeWindDirection(direction) {
|
|
10
15
|
const vector = new THREE.Vector2(direction[0], direction[1]);
|
|
11
16
|
return vector.lengthSq() === 0 ? new THREE.Vector2(1, 0) : vector.normalize();
|
|
12
17
|
}
|
|
18
|
+
/** @internal */
|
|
13
19
|
export function resolveGrassWind(wind) {
|
|
14
20
|
if (wind === false)
|
|
15
21
|
return { ...DEFAULT_GRASS_WIND, strength: 0, flutter: 0 };
|
|
@@ -21,8 +27,24 @@ export function resolveGrassWind(wind) {
|
|
|
21
27
|
flutter: wind?.flutter ?? DEFAULT_GRASS_WIND.flutter,
|
|
22
28
|
};
|
|
23
29
|
}
|
|
30
|
+
/** @internal */
|
|
31
|
+
export function resolveGrassDistanceFade(fade) {
|
|
32
|
+
// start >= end disables the fade in the shader, so "off" is just an empty band.
|
|
33
|
+
if (fade === false)
|
|
34
|
+
return { start: 0, end: 0 };
|
|
35
|
+
return {
|
|
36
|
+
start: fade?.start ?? DEFAULT_GRASS_DISTANCE_FADE.start,
|
|
37
|
+
end: fade?.end ?? DEFAULT_GRASS_DISTANCE_FADE.end,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** @internal */
|
|
24
41
|
export function createGrassMaterial(options = {}) {
|
|
25
42
|
const wind = resolveGrassWind(options.wind);
|
|
43
|
+
const fade = resolveGrassDistanceFade(options.distanceFade);
|
|
44
|
+
const colorBase = new THREE.Color(options.colorBase ?? "#2d431f");
|
|
45
|
+
const colorGround = options.colorGround === undefined
|
|
46
|
+
? colorBase.clone().multiplyScalar(0.82)
|
|
47
|
+
: new THREE.Color(options.colorGround);
|
|
26
48
|
const uniforms = {
|
|
27
49
|
uTime: { value: 0 },
|
|
28
50
|
uWindDirection: { value: normalizeWindDirection(wind.direction) },
|
|
@@ -30,9 +52,12 @@ export function createGrassMaterial(options = {}) {
|
|
|
30
52
|
uWindSpeed: { value: wind.speed },
|
|
31
53
|
uWindGustScale: { value: wind.gustScale },
|
|
32
54
|
uWindFlutter: { value: wind.flutter },
|
|
33
|
-
uColorBase: { value:
|
|
55
|
+
uColorBase: { value: colorBase },
|
|
34
56
|
uColorTip: { value: new THREE.Color(options.colorTip ?? "#a6cc58") },
|
|
57
|
+
uColorGround: { value: colorGround },
|
|
35
58
|
uColorVariation: { value: options.colorVariation ?? 0.28 },
|
|
59
|
+
uDistanceFade: { value: new THREE.Vector2(fade.start, fade.end) },
|
|
60
|
+
uNormalLift: { value: THREE.MathUtils.clamp(options.normalLift ?? 0.6, 0, 1) },
|
|
36
61
|
};
|
|
37
62
|
const material = new THREE.MeshStandardMaterial({
|
|
38
63
|
color: "#ffffff",
|
|
@@ -52,36 +77,61 @@ attribute float instanceWidth;
|
|
|
52
77
|
attribute float instanceBend;
|
|
53
78
|
attribute float instancePhase;
|
|
54
79
|
attribute float instanceColorMix;
|
|
80
|
+
attribute vec2 bladeOffset;
|
|
81
|
+
attribute vec4 bladeVary;
|
|
82
|
+
attribute float bladeBendScale;
|
|
55
83
|
uniform float uTime;
|
|
56
84
|
uniform vec2 uWindDirection;
|
|
57
85
|
uniform float uWindStrength;
|
|
58
86
|
uniform float uWindSpeed;
|
|
59
87
|
uniform float uWindGustScale;
|
|
60
88
|
uniform float uWindFlutter;
|
|
89
|
+
uniform vec2 uDistanceFade;
|
|
90
|
+
uniform float uNormalLift;
|
|
61
91
|
varying float vGrassBladeT;
|
|
62
92
|
varying float vGrassColorMix;
|
|
93
|
+
varying float vGrassFade;
|
|
63
94
|
vec3 jgGrassPosition;
|
|
64
95
|
vec3 jgGrassNormal;
|
|
65
96
|
void jgComputeGrassBlade() {
|
|
66
97
|
float t = position.y;
|
|
67
98
|
float side = position.x;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
float
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
float
|
|
99
|
+
// Camera-distance thinning: each tuft draws a random threshold, so the field dissolves
|
|
100
|
+
// progressively across the fade band instead of ending at a hard curtain.
|
|
101
|
+
float camDist = distance((modelMatrix * vec4(instanceOffset, 1.0)).xyz, cameraPosition);
|
|
102
|
+
float fadeT = uDistanceFade.y > uDistanceFade.x
|
|
103
|
+
? smoothstep(uDistanceFade.x, uDistanceFade.y, camDist)
|
|
104
|
+
: 0.0;
|
|
105
|
+
float threshold = fract(instanceColorMix * 61.7) * 0.85;
|
|
106
|
+
float keep = 1.0 - smoothstep(threshold, threshold + 0.15, fadeT);
|
|
107
|
+
float heightScale = instanceHeight * bladeVary.y * keep;
|
|
108
|
+
// Paddle taper: hold width through the mid-blade and only pinch near the tip — thin
|
|
109
|
+
// triangle blades leave the ground visible between tufts and read as stubble.
|
|
110
|
+
float taper = (1.0 - 0.3 * t) * (1.0 - smoothstep(0.5, 1.0, t) * 0.95);
|
|
111
|
+
// Surviving distant tufts widen to hold silhouette coverage as neighbors thin out.
|
|
112
|
+
float width = instanceWidth * bladeVary.z * taper * (1.0 + fadeT * 1.4);
|
|
113
|
+
float arc = instanceBend * bladeBendScale * t * t;
|
|
114
|
+
vec3 localPosition = vec3(side * width, heightScale * t - arc * heightScale * 0.18, arc * heightScale);
|
|
115
|
+
vec3 localNormal = normalize(vec3(0.0, -instanceBend * bladeBendScale * t, 1.0));
|
|
116
|
+
float bladeYaw = instanceYaw + bladeVary.x;
|
|
117
|
+
float cy = cos(bladeYaw);
|
|
118
|
+
float sy = sin(bladeYaw);
|
|
75
119
|
vec3 yawedPosition = vec3(localPosition.x * cy + localPosition.z * sy, localPosition.y, -localPosition.x * sy + localPosition.z * cy);
|
|
76
120
|
vec3 yawedNormal = normalize(vec3(localNormal.x * cy + localNormal.z * sy, localNormal.y, -localNormal.x * sy + localNormal.z * cy));
|
|
121
|
+
float ct = cos(instanceYaw);
|
|
122
|
+
float st = sin(instanceYaw);
|
|
123
|
+
vec3 tuftOffset = vec3(bladeOffset.x * ct + bladeOffset.y * st, 0.0, -bladeOffset.x * st + bladeOffset.y * ct);
|
|
77
124
|
float gustPhase = dot(instanceOffset.xz, uWindDirection) * uWindGustScale + uTime * uWindSpeed + instancePhase;
|
|
78
125
|
float gust = sin(gustPhase) * 0.7 + sin(gustPhase * 0.43 + 2.4) * 0.3;
|
|
79
|
-
float flutter = sin(uTime * 7.5 + instancePhase * 2.7) * uWindFlutter;
|
|
80
|
-
vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t;
|
|
81
|
-
jgGrassPosition = instanceOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
|
|
82
|
-
|
|
126
|
+
float flutter = sin(uTime * 7.5 + instancePhase * 2.7 + bladeVary.w * 6.2832) * uWindFlutter;
|
|
127
|
+
vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t * (0.4 + 0.6 * bladeVary.y);
|
|
128
|
+
jgGrassPosition = instanceOffset + tuftOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
|
|
129
|
+
// Lifting normals toward straight-up lights the field like a continuous meadow surface —
|
|
130
|
+
// side-lit individual blade planes read as dark stubble, not turf.
|
|
131
|
+
jgGrassNormal = normalize(mix(yawedNormal, vec3(0.0, 1.0, 0.0), uNormalLift));
|
|
83
132
|
vGrassBladeT = t;
|
|
84
133
|
vGrassColorMix = instanceColorMix;
|
|
134
|
+
vGrassFade = fadeT;
|
|
85
135
|
}
|
|
86
136
|
`)
|
|
87
137
|
.replace("#include <beginnormal_vertex>", "jgComputeGrassBlade();\nvec3 objectNormal = jgGrassNormal;")
|
|
@@ -91,15 +141,20 @@ void jgComputeGrassBlade() {
|
|
|
91
141
|
#include <common>
|
|
92
142
|
uniform vec3 uColorBase;
|
|
93
143
|
uniform vec3 uColorTip;
|
|
144
|
+
uniform vec3 uColorGround;
|
|
94
145
|
uniform float uColorVariation;
|
|
95
146
|
varying float vGrassBladeT;
|
|
96
147
|
varying float vGrassColorMix;
|
|
148
|
+
varying float vGrassFade;
|
|
97
149
|
`)
|
|
98
150
|
.replace("#include <color_fragment>", `
|
|
99
151
|
#include <color_fragment>
|
|
100
152
|
vec3 grassColor = mix(uColorBase, uColorTip, smoothstep(0.0, 1.0, vGrassBladeT));
|
|
101
153
|
grassColor *= mix(1.0 - uColorVariation, 1.0 + uColorVariation, vGrassColorMix);
|
|
102
|
-
|
|
154
|
+
// Roots take the ground color so blades grow out of the terrain instead of floating on it,
|
|
155
|
+
// and distant blades converge back toward it so the fade band blends into the mottled ground.
|
|
156
|
+
grassColor = mix(uColorGround, grassColor, smoothstep(0.0, 0.55, vGrassBladeT));
|
|
157
|
+
grassColor = mix(grassColor, uColorGround * 1.04, vGrassFade * 0.6);
|
|
103
158
|
diffuseColor.rgb = grassColor;
|
|
104
159
|
`);
|
|
105
160
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/** XZ rectangle in world units — matches the shape of a terraform snapshot's `bounds`. */
|
|
3
|
+
export interface HeightfieldRect {
|
|
4
|
+
minX: number;
|
|
5
|
+
minZ: number;
|
|
6
|
+
maxX: number;
|
|
7
|
+
maxZ: number;
|
|
8
|
+
}
|
|
9
|
+
/** Per-vertex color hook for {@link displaceHeightfieldGeometry}; write the tone into `out`. */
|
|
10
|
+
export type HeightfieldColorFn = (x: number, z: number, height: number, out: THREE.Color) => void;
|
|
11
|
+
/** Options for {@link displaceHeightfieldGeometry}. */
|
|
12
|
+
export interface HeightfieldDisplaceOptions {
|
|
13
|
+
/** World-space rect the plane spans (the plane is centered on it, XZ, +Y up). */
|
|
14
|
+
bounds: HeightfieldRect;
|
|
15
|
+
/**
|
|
16
|
+
* Dirty world-space region to re-sample; only the vertex window covering it (plus a one-vertex
|
|
17
|
+
* normal ring) updates. Omit or pass null for a full rebuild.
|
|
18
|
+
*/
|
|
19
|
+
region?: HeightfieldRect | null;
|
|
20
|
+
/** When given, writes a `color` attribute (creating it on demand) via this per-vertex hook. */
|
|
21
|
+
color?: HeightfieldColorFn;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Re-samples a `PlaneGeometry(width, depth, segments, segments).rotateX(-π/2)` heightfield mesh
|
|
25
|
+
* from a `sampleHeight` field in place: vertex Y, optional vertex color, grid central-difference
|
|
26
|
+
* normals, and an analytically maintained bounding sphere. With a dirty `region`, the work is
|
|
27
|
+
* O(region vertices) — heights/colors update inside the covering vertex window, normals inside the
|
|
28
|
+
* window plus a one-vertex ring — never a whole-mesh `computeVertexNormals`/`computeBoundingSphere`
|
|
29
|
+
* pass, which is what keeps per-frame brush stamps inside the editor's frame budget. The bounding
|
|
30
|
+
* sphere derives from the fixed plane extents plus a running height range kept on
|
|
31
|
+
* `geometry.userData`; partial passes only expand it, a full pass resets it exactly.
|
|
32
|
+
* @capability heightfield-mesh-update in-place partial update of a displaced ground plane mesh
|
|
33
|
+
*/
|
|
34
|
+
export declare function displaceHeightfieldGeometry(geometry: THREE.BufferGeometry, sampleHeight: (x: number, z: number) => number, options: HeightfieldDisplaceOptions): void;
|