@jgengine/shell 0.11.0 → 0.13.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 +52 -0
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +27 -1
- package/dist/GameHost.js +83 -4
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -0
- package/dist/GamePlayerShell.js +63 -359
- package/dist/GameUiPreview.js +3 -0
- 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 +111 -26
- package/dist/audio/audioWire.d.ts +22 -4
- package/dist/audio/audioWire.js +46 -3
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/camera/GameCameraRig.js +6 -1
- package/dist/camera/GameOrbitCamera.js +7 -1
- package/dist/camera/cameraCollision.d.ts +28 -0
- package/dist/camera/cameraCollision.js +34 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +24 -10
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +27 -0
- package/dist/camera/rigMath.js +45 -0
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +26 -4
- package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
- package/dist/devtools/DevtoolsOverlay.js +28 -12
- package/dist/devtools/agentBridge.js +4 -0
- package/dist/devtools/perfDiagnose.js +1 -1
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
- package/dist/environment/EnvironmentScene.js +17 -3
- package/dist/environment/RoadJunctions.d.ts +12 -0
- package/dist/environment/RoadJunctions.js +69 -0
- package/dist/environment/RoadRibbons.d.ts +8 -2
- package/dist/environment/RoadRibbons.js +26 -8
- 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/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +11 -4
- package/dist/presentationResolve.d.ts +39 -0
- package/dist/presentationResolve.js +53 -0
- package/dist/registry.d.ts +1 -0
- package/dist/render/PartMotion.d.ts +18 -0
- package/dist/render/PartMotion.js +91 -0
- package/dist/render/SceneModels.d.ts +9 -2
- package/dist/render/SceneModels.js +31 -180
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/measureBounds.d.ts +34 -0
- package/dist/render/measureBounds.js +127 -0
- package/dist/render/modelLoad.js +4 -1
- package/dist/render/resolveModel.d.ts +15 -0
- package/dist/render/resolveModel.js +43 -2
- package/dist/render/useModelAnimation.d.ts +10 -0
- package/dist/render/useModelAnimation.js +198 -0
- package/dist/scatter/InstancedScatter.js +5 -0
- package/dist/scatter/scatterProxies.js +24 -0
- package/dist/scene/AuthoredScene.d.ts +3 -1
- package/dist/scene/AuthoredScene.js +7 -2
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GrassFieldRenderer.js +53 -5
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.js +70 -7
- package/dist/scene/WaterRenderer.js +34 -10
- package/dist/scene/builtinSceneKindRenderers.js +4 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +2 -0
- package/dist/settings/appliedSettings.d.ts +2 -1
- package/dist/settings/appliedSettings.js +4 -2
- package/dist/structures/GeneratedBuilding.d.ts +4 -0
- package/dist/structures/GeneratedBuilding.js +17 -7
- package/dist/structures/TransformGizmo.d.ts +11 -1
- package/dist/structures/TransformGizmo.js +9 -5
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/grassGeometry.d.ts +33 -1
- package/dist/terrain/grassGeometry.js +83 -21
- package/dist/terrain/grassMaterial.d.ts +18 -0
- package/dist/terrain/grassMaterial.js +66 -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/soilPatchMaterial.d.ts +15 -2
- package/dist/terrain/soilPatchMaterial.js +46 -8
- package/dist/terrain/terrainDetailMaterial.js +15 -2
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +9 -0
- package/dist/water/OceanConfig.js +6 -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/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/WorldHud.d.ts +3 -1
- package/dist/world/WorldHud.js +2 -2
- package/dist/world/WorldScene.js +32 -4
- package/dist/worldSky.d.ts +7 -1
- package/dist/worldSky.js +11 -1
- package/package.json +4 -4
- 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 +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +17 -10
- package/skills/jgengine-ui/api.md +316 -51
- package/skills/jgengine-ui/capabilities.md +135 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +59 -9
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- 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/runtime.d.ts +0 -15
- package/dist/cartridge/runtime.js +0 -556
- package/dist/cartridge/spec.d.ts +0 -188
- package/dist/cartridge/spec.js +0 -19
- package/dist/cartridge/validate.d.ts +0 -2
- package/dist/cartridge/validate.js +0 -78
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
|
|
2
|
-
import { type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
2
|
+
import { type GraphicsQuality, type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
3
3
|
import { type TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
4
4
|
import type { AudioEngine } from "../audio/audioEngine.js";
|
|
5
5
|
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
@@ -11,6 +11,7 @@ export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault
|
|
|
11
11
|
shadows: boolean;
|
|
12
12
|
dpr: number;
|
|
13
13
|
uiScale: number;
|
|
14
|
+
quality: GraphicsQuality;
|
|
14
15
|
};
|
|
15
16
|
export declare function AudioSettingsBridge({ store, engine, buses, }: {
|
|
16
17
|
store: SettingsStore;
|
|
@@ -16,12 +16,14 @@ export function useSettingsRevision(store) {
|
|
|
16
16
|
}
|
|
17
17
|
export function useGraphicsSettings(store, shadowsDefault) {
|
|
18
18
|
useSettingsRevision(store);
|
|
19
|
-
const
|
|
19
|
+
const rawQuality = store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY);
|
|
20
|
+
const quality = GRAPHICS_QUALITY_DPR[rawQuality] !== undefined ? rawQuality : "high";
|
|
20
21
|
const rawUiScale = store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE);
|
|
21
22
|
return {
|
|
22
23
|
shadows: store.get(SETTING_IDS.graphicsShadows, shadowsDefault),
|
|
23
|
-
dpr: GRAPHICS_QUALITY_DPR[quality]
|
|
24
|
+
dpr: GRAPHICS_QUALITY_DPR[quality],
|
|
24
25
|
uiScale: Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, rawUiScale)),
|
|
26
|
+
quality,
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
29
|
export function AudioSettingsBridge({ store, engine, buses, }) {
|
|
@@ -43,6 +43,10 @@ export interface GeneratedBuildingProps {
|
|
|
43
43
|
export interface InstancedBuildingPlacement {
|
|
44
44
|
building: GeneratedBuildingData;
|
|
45
45
|
position?: readonly [number, number, number];
|
|
46
|
+
/** Building yaw (radians) applied to the whole massing about `pivot` — street-aware facing. */
|
|
47
|
+
rotationY?: number;
|
|
48
|
+
/** World XZ the `rotationY` turns about (the building center); defaults to the world origin. */
|
|
49
|
+
pivot?: readonly [number, number];
|
|
46
50
|
}
|
|
47
51
|
export interface InstancedBuildingsProps {
|
|
48
52
|
buildings: readonly InstancedBuildingPlacement[];
|
|
@@ -70,25 +70,35 @@ function bucketPartMatrices(buildings, visible) {
|
|
|
70
70
|
const buckets = new Map();
|
|
71
71
|
for (const placement of buildings) {
|
|
72
72
|
const [ox, oy, oz] = placement.position ?? [0, 0, 0];
|
|
73
|
+
// Building yaw turns the whole massing about its center. Engine Y-rotation convention (matches
|
|
74
|
+
// dummy.rotation.y below): a local vector (x, z) maps to (x·cos + z·sin, −x·sin + z·cos).
|
|
75
|
+
const yaw = placement.rotationY ?? 0;
|
|
76
|
+
const [px0, pz0] = placement.pivot ?? [0, 0];
|
|
77
|
+
const cos = Math.cos(yaw);
|
|
78
|
+
const sin = Math.sin(yaw);
|
|
73
79
|
for (const part of placement.building.parts) {
|
|
74
80
|
if (visible !== null && !visible.has(part.kind))
|
|
75
81
|
continue;
|
|
76
82
|
const [nx, nz] = normalFor(part.facade);
|
|
77
83
|
const offset = outwardOffset(part);
|
|
78
|
-
|
|
84
|
+
// Local offset from the pivot (part position + facade-normal nudge), then rotated by the yaw.
|
|
85
|
+
const lx = part.position[0] - px0 + nx * offset;
|
|
86
|
+
const lz = part.position[2] - pz0 + nz * offset;
|
|
87
|
+
const px = ox + px0 + lx * cos + lz * sin;
|
|
79
88
|
const py = oy + part.position[1];
|
|
80
|
-
const pz = oz +
|
|
89
|
+
const pz = oz + pz0 - lx * sin + lz * cos;
|
|
90
|
+
const partYaw = part.rotationY + yaw;
|
|
81
91
|
let bucket = buckets.get(part.kind);
|
|
82
92
|
if (bucket === undefined) {
|
|
83
93
|
bucket = [];
|
|
84
94
|
buckets.set(part.kind, bucket);
|
|
85
95
|
}
|
|
86
96
|
if (part.kind === "clothesline") {
|
|
87
|
-
const
|
|
88
|
-
const
|
|
97
|
+
const rowSin = Math.sin(partYaw);
|
|
98
|
+
const rowCos = Math.cos(partYaw);
|
|
89
99
|
for (const row of CLOTHESLINE_ROW_OFFSETS) {
|
|
90
|
-
dummy.position.set(px + row *
|
|
91
|
-
dummy.rotation.set(0,
|
|
100
|
+
dummy.position.set(px + row * rowSin, py, pz + row * rowCos);
|
|
101
|
+
dummy.rotation.set(0, partYaw, 0);
|
|
92
102
|
dummy.scale.set(part.scale[0], Math.max(part.scale[1], 0.025), 0.025);
|
|
93
103
|
dummy.updateMatrix();
|
|
94
104
|
bucket.push(dummy.matrix.clone());
|
|
@@ -96,7 +106,7 @@ function bucketPartMatrices(buildings, visible) {
|
|
|
96
106
|
continue;
|
|
97
107
|
}
|
|
98
108
|
dummy.position.set(px, py, pz);
|
|
99
|
-
dummy.rotation.set(0,
|
|
109
|
+
dummy.rotation.set(0, partYaw, 0);
|
|
100
110
|
dummy.scale.set(part.scale[0], part.scale[1], part.scale[2]);
|
|
101
111
|
dummy.updateMatrix();
|
|
102
112
|
bucket.push(dummy.matrix.clone());
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
export type TransformGizmoMode = "translate" | "rotate" | "scale";
|
|
3
3
|
/** Snap policy: grid quantize, free drag, or ground-height sample on release. */
|
|
4
4
|
export type TransformGizmoSnap = "grid" | "free" | "ground";
|
|
5
|
+
/** Gizmo handle orientation: world axes, or the target's local (yaw-rotated) axes. */
|
|
6
|
+
export type TransformGizmoSpace = "world" | "local";
|
|
7
|
+
/** Default rotation snap increment (radians) when callers don't configure one. */
|
|
8
|
+
export declare const DEFAULT_ROTATION_SNAP: number;
|
|
5
9
|
/** Pose reported by {@link TransformGizmo} when the user releases a drag. */
|
|
6
10
|
export interface TransformGizmoPose {
|
|
7
11
|
position: {
|
|
@@ -27,6 +31,12 @@ export interface TransformGizmoProps {
|
|
|
27
31
|
mode?: TransformGizmoMode;
|
|
28
32
|
snapMode?: TransformGizmoSnap;
|
|
29
33
|
gridSize?: number;
|
|
34
|
+
/** Handle orientation; `"local"` aligns handles to the target's yaw. */
|
|
35
|
+
space?: TransformGizmoSpace;
|
|
36
|
+
/** Rotation snap increment in radians; `null` rotates freely. */
|
|
37
|
+
rotationSnap?: number | null;
|
|
38
|
+
/** Scale snap increment; `null` (default) scales freely. */
|
|
39
|
+
scaleSnap?: number | null;
|
|
30
40
|
/** Vertical offset so the gizmo sits above ground markers. */
|
|
31
41
|
lift?: number;
|
|
32
42
|
size?: number;
|
|
@@ -40,4 +50,4 @@ export interface TransformGizmoProps {
|
|
|
40
50
|
* Wraps TransformControls; callers own selection and commit side-effects.
|
|
41
51
|
* @capability transform-gizmo in-game select/move/rotate/scale gizmo
|
|
42
52
|
*/
|
|
43
|
-
export declare const TransformGizmo: import("react").MemoExoticComponent<({ position, rotationY, mode, snapMode, gridSize, lift, size, groundSnap, onDraggingChange, onRelease, }: TransformGizmoProps) => import("react").JSX.Element>;
|
|
53
|
+
export declare const TransformGizmo: import("react").MemoExoticComponent<({ position, rotationY, mode, snapMode, gridSize, space, rotationSnap, scaleSnap, lift, size, groundSnap, onDraggingChange, onRelease, }: TransformGizmoProps) => import("react").JSX.Element>;
|
|
@@ -2,12 +2,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { TransformControls } from "@react-three/drei";
|
|
3
3
|
import { useThree } from "@react-three/fiber";
|
|
4
4
|
import { memo, useEffect, useRef, useState } from "react";
|
|
5
|
+
/** Default rotation snap increment (radians) when callers don't configure one. */
|
|
6
|
+
export const DEFAULT_ROTATION_SNAP = Math.PI / 12;
|
|
5
7
|
/**
|
|
6
8
|
* Runtime selection/move gizmo shared by games and the editor.
|
|
7
9
|
* Wraps TransformControls; callers own selection and commit side-effects.
|
|
8
10
|
* @capability transform-gizmo in-game select/move/rotate/scale gizmo
|
|
9
11
|
*/
|
|
10
|
-
export const TransformGizmo = memo(function TransformGizmo({ position, rotationY = 0, mode = "translate", snapMode = "free", gridSize = 1, lift = 0, size = 0.85, groundSnap, onDraggingChange, onRelease, }) {
|
|
12
|
+
export const TransformGizmo = memo(function TransformGizmo({ position, rotationY = 0, mode = "translate", snapMode = "free", gridSize = 1, space = "world", rotationSnap = DEFAULT_ROTATION_SNAP, scaleSnap = null, lift = 0, size = 0.85, groundSnap, onDraggingChange, onRelease, }) {
|
|
11
13
|
const groupRef = useRef(null);
|
|
12
14
|
const [object, setObject] = useState(null);
|
|
13
15
|
const draggingRef = useRef(false);
|
|
@@ -24,9 +26,11 @@ export const TransformGizmo = memo(function TransformGizmo({ position, rotationY
|
|
|
24
26
|
group.rotation.set(0, rotationY, 0);
|
|
25
27
|
group.scale.set(1, 1, 1);
|
|
26
28
|
}, [position.x, position.y, position.z, rotationY, lift, anchorKey, mode]);
|
|
27
|
-
const snapProps =
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const snapProps = {
|
|
30
|
+
translationSnap: snapMode === "grid" ? gridSize : null,
|
|
31
|
+
rotationSnap,
|
|
32
|
+
scaleSnap,
|
|
33
|
+
};
|
|
30
34
|
const handleRelease = () => {
|
|
31
35
|
draggingRef.current = false;
|
|
32
36
|
if (controls !== null && "enabled" in controls)
|
|
@@ -46,7 +50,7 @@ export const TransformGizmo = memo(function TransformGizmo({ position, rotationY
|
|
|
46
50
|
scale: { x: current.scale.x, y: current.scale.y, z: current.scale.z },
|
|
47
51
|
});
|
|
48
52
|
};
|
|
49
|
-
return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef }), object !== null ? (_jsx(TransformControls, { object: object, mode: mode, size: size, ...snapProps, onMouseDown: () => {
|
|
53
|
+
return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef }), object !== null ? (_jsx(TransformControls, { object: object, mode: mode, size: size, space: space, ...snapProps, onMouseDown: () => {
|
|
50
54
|
draggingRef.current = true;
|
|
51
55
|
if (controls !== null && "enabled" in controls)
|
|
52
56
|
controls.enabled = false;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type ThreeElements } from "@react-three/fiber";
|
|
2
|
-
import { type GrassBladeGeometryOptions, type GrassRange } from "./grassGeometry.js";
|
|
3
|
-
import { type GrassMaterialOptions, type GrassWindOptions } from "./grassMaterial.js";
|
|
2
|
+
import { type GrassBladeGeometryOptions, type GrassExclusion, type GrassRange } from "./grassGeometry.js";
|
|
3
|
+
import { type GrassDistanceFadeOptions, type GrassMaterialOptions, type GrassWindOptions } from "./grassMaterial.js";
|
|
4
4
|
import type { TerrainArea, TerrainHeightSampler } from "./terrainMath.js";
|
|
5
5
|
export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
|
|
6
|
+
export { GRASS_TUFT_BLADES } from "./grassGeometry.js";
|
|
6
7
|
export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
|
|
7
8
|
count?: number;
|
|
8
9
|
density?: number;
|
|
@@ -13,11 +14,18 @@ export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "c
|
|
|
13
14
|
bladeHeight?: GrassRange;
|
|
14
15
|
bladeWidth?: GrassRange;
|
|
15
16
|
bladeBend?: GrassRange;
|
|
17
|
+
tuftBlades?: number;
|
|
18
|
+
tuftRadius?: number;
|
|
19
|
+
edgeFeather?: number;
|
|
20
|
+
exclude?: readonly GrassExclusion[];
|
|
16
21
|
heightAt?: TerrainHeightSampler;
|
|
17
22
|
colorBase?: GrassMaterialOptions["colorBase"];
|
|
18
23
|
colorTip?: GrassMaterialOptions["colorTip"];
|
|
24
|
+
colorGround?: GrassMaterialOptions["colorGround"];
|
|
19
25
|
colorVariation?: number;
|
|
20
26
|
wind?: GrassWindOptions | false;
|
|
27
|
+
distanceFade?: GrassDistanceFadeOptions | false;
|
|
28
|
+
normalLift?: number;
|
|
21
29
|
roughness?: number;
|
|
22
30
|
}
|
|
23
|
-
export declare function GrassField({ count, density, budget, area, seed, segments, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow, receiveShadow, frustumCulled, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
|
|
31
|
+
export declare function GrassField({ count, density, budget, area, seed, segments, bladeHeight, bladeWidth, bladeBend, tuftBlades, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow, receiveShadow, frustumCulled, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useMemo } from "react";
|
|
4
|
-
import { createGrassBladeGeometry } from "./grassGeometry.js";
|
|
5
|
-
import { createGrassMaterial } from "./grassMaterial.js";
|
|
4
|
+
import { createGrassBladeGeometry, grassTuftCount, GRASS_TUFT_BLADES, } from "./grassGeometry.js";
|
|
5
|
+
import { createGrassMaterial, } from "./grassMaterial.js";
|
|
6
6
|
import { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget, } from "./grassBudget.js";
|
|
7
7
|
export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
|
|
8
|
-
export
|
|
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,19 @@ import * as THREE from "three";
|
|
|
2
2
|
import { type TerrainSeed } from "./random.js";
|
|
3
3
|
import { type TerrainArea, type TerrainHeightSampler } from "./terrainMath.js";
|
|
4
4
|
export type GrassRange = number | readonly [min: number, max: number];
|
|
5
|
+
/** Blades baked into each tuft instance — one instance reads as a clump, not a lone blade. */
|
|
6
|
+
export declare const GRASS_TUFT_BLADES = 5;
|
|
7
|
+
/** A rectangle (patch-local XZ) the grass bake keeps clear — soil patches, water, plazas. */
|
|
8
|
+
export interface GrassExclusion {
|
|
9
|
+
/** Rect center in patch-local coordinates. */
|
|
10
|
+
center: readonly [x: number, z: number];
|
|
11
|
+
/** Rect half extents. */
|
|
12
|
+
half: readonly [x: number, z: number];
|
|
13
|
+
/** Ramp width (m) outside the rect over which blades regrow to full height. Default 1.5. */
|
|
14
|
+
feather?: number;
|
|
15
|
+
}
|
|
5
16
|
export interface GrassBladeGeometryOptions {
|
|
17
|
+
/** Total blade budget for the patch (instances are tufts of `tuftBlades` blades each). */
|
|
6
18
|
count?: number;
|
|
7
19
|
area?: TerrainArea;
|
|
8
20
|
seed?: TerrainSeed;
|
|
@@ -10,6 +22,14 @@ export interface GrassBladeGeometryOptions {
|
|
|
10
22
|
height?: GrassRange;
|
|
11
23
|
width?: GrassRange;
|
|
12
24
|
bend?: GrassRange;
|
|
25
|
+
/** Blades merged into each instanced tuft. Default {@link GRASS_TUFT_BLADES}. */
|
|
26
|
+
tuftBlades?: number;
|
|
27
|
+
/** Tuft spread radius in meters — how far clump blades splay from the tuft root. Default 0.16. */
|
|
28
|
+
tuftRadius?: number;
|
|
29
|
+
/** Meters of density/height feathering at the patch border so the field ends in a soft fringe, not a mowed rectangle. Default 1.5; 0 disables. */
|
|
30
|
+
edgeFeather?: number;
|
|
31
|
+
/** Patch-local rectangles kept clear of blades (soil patches, water) with a soft regrow ramp. */
|
|
32
|
+
exclude?: readonly GrassExclusion[];
|
|
13
33
|
heightAt?: TerrainHeightSampler;
|
|
14
34
|
}
|
|
15
35
|
export interface ResolvedGrassBladeGeometryOptions {
|
|
@@ -20,11 +40,23 @@ export interface ResolvedGrassBladeGeometryOptions {
|
|
|
20
40
|
height: readonly [min: number, max: number];
|
|
21
41
|
width: readonly [min: number, max: number];
|
|
22
42
|
bend: readonly [min: number, max: number];
|
|
43
|
+
tuftBlades: number;
|
|
44
|
+
tuftRadius: number;
|
|
45
|
+
edgeFeather: number;
|
|
46
|
+
exclude: readonly GrassExclusion[];
|
|
23
47
|
heightAt: TerrainHeightSampler;
|
|
24
48
|
}
|
|
25
49
|
/** @internal */
|
|
26
50
|
export declare function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number];
|
|
27
51
|
/** @internal */
|
|
28
52
|
export declare function resolveGrassBladeGeometryOptions(options?: GrassBladeGeometryOptions): ResolvedGrassBladeGeometryOptions;
|
|
29
|
-
/** @internal */
|
|
53
|
+
/** Tuft instances needed to carry `bladeCount` blades at `tuftBlades` blades per tuft. @internal */
|
|
54
|
+
export declare function grassTuftCount(bladeCount: number, tuftBlades?: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Instanced tuft geometry: the template mesh is a clump of `tuftBlades` blades splayed around the
|
|
57
|
+
* tuft root (per-blade yaw/offset/scale baked as vertex attributes), and each instance places one
|
|
58
|
+
* whole clump. Clumps read as turf at a fraction of the instance count single blades need, and the
|
|
59
|
+
* per-instance yaw/scale jitter hides the shared clump layout.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
30
62
|
export declare function createGrassBladeGeometry(options?: GrassBladeGeometryOptions): THREE.InstancedBufferGeometry;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { createSeededRandom } from "./random.js";
|
|
3
3
|
import { resolveTerrainSize } from "./terrainMath.js";
|
|
4
|
+
/** Blades baked into each tuft instance — one instance reads as a clump, not a lone blade. */
|
|
5
|
+
export const GRASS_TUFT_BLADES = 5;
|
|
4
6
|
/** @internal */
|
|
5
7
|
export function resolveGrassRange(value, fallback) {
|
|
6
8
|
if (value === undefined)
|
|
@@ -15,53 +17,113 @@ export function resolveGrassBladeGeometryOptions(options = {}) {
|
|
|
15
17
|
seed: options.seed ?? 1,
|
|
16
18
|
segments: Math.max(1, Math.floor(options.segments ?? 4)),
|
|
17
19
|
height: resolveGrassRange(options.height, [0.55, 1.1]),
|
|
18
|
-
width: resolveGrassRange(options.width, [0.
|
|
19
|
-
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 ?? [],
|
|
20
26
|
heightAt: options.heightAt ?? (() => 0),
|
|
21
27
|
};
|
|
22
28
|
}
|
|
29
|
+
/** Tuft instances needed to carry `bladeCount` blades at `tuftBlades` blades per tuft. @internal */
|
|
30
|
+
export function grassTuftCount(bladeCount, tuftBlades = GRASS_TUFT_BLADES) {
|
|
31
|
+
return Math.ceil(Math.max(0, bladeCount) / Math.max(1, tuftBlades));
|
|
32
|
+
}
|
|
23
33
|
function randomRange(random, range) {
|
|
24
34
|
return THREE.MathUtils.lerp(range[0], range[1], random());
|
|
25
35
|
}
|
|
26
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Instanced tuft geometry: the template mesh is a clump of `tuftBlades` blades splayed around the
|
|
38
|
+
* tuft root (per-blade yaw/offset/scale baked as vertex attributes), and each instance places one
|
|
39
|
+
* whole clump. Clumps read as turf at a fraction of the instance count single blades need, and the
|
|
40
|
+
* per-instance yaw/scale jitter hides the shared clump layout.
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
27
43
|
export function createGrassBladeGeometry(options = {}) {
|
|
28
44
|
const resolved = resolveGrassBladeGeometryOptions(options);
|
|
29
45
|
const size = resolveTerrainSize(resolved.area);
|
|
30
46
|
const positions = [];
|
|
31
47
|
const normals = [];
|
|
32
48
|
const uvs = [];
|
|
49
|
+
const bladeOffsets = [];
|
|
50
|
+
const bladeVary = [];
|
|
51
|
+
const bladeBendScale = [];
|
|
33
52
|
const indices = [];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
// Blade layout inside the tuft is baked once from the seed, shared by every instance.
|
|
54
|
+
const templateRandom = createSeededRandom(typeof resolved.seed === "number" ? resolved.seed + 7919 : `${resolved.seed}/tuft`);
|
|
55
|
+
const vertsPerBlade = (resolved.segments + 1) * 2;
|
|
56
|
+
for (let blade = 0; blade < resolved.tuftBlades; blade += 1) {
|
|
57
|
+
// First blade anchors the root; the rest ring outward with jittered angle/radius.
|
|
58
|
+
const angle = blade === 0 ? 0 : ((blade - 1) / Math.max(1, resolved.tuftBlades - 1)) * Math.PI * 2 + templateRandom() * 1.4;
|
|
59
|
+
const radius = blade === 0 ? 0 : resolved.tuftRadius * (0.35 + 0.65 * templateRandom());
|
|
60
|
+
const offsetX = Math.cos(angle) * radius;
|
|
61
|
+
const offsetZ = Math.sin(angle) * radius;
|
|
62
|
+
// Bend direction points away from the tuft center (plus jitter) so the clump splays open.
|
|
63
|
+
const yaw = angle + Math.PI / 2 + (templateRandom() - 0.5) * 1.6;
|
|
64
|
+
const heightScale = 0.72 + templateRandom() * 0.45;
|
|
65
|
+
const widthScale = 0.8 + templateRandom() * 0.4;
|
|
66
|
+
const phase = templateRandom();
|
|
67
|
+
const bendScale = 0.7 + templateRandom() * 0.7;
|
|
68
|
+
for (let segment = 0; segment <= resolved.segments; segment += 1) {
|
|
69
|
+
const t = segment / resolved.segments;
|
|
70
|
+
positions.push(-0.5, t, 0, 0.5, t, 0);
|
|
71
|
+
normals.push(0, 0, 1, 0, 0, 1);
|
|
72
|
+
uvs.push(0, t, 1, t);
|
|
73
|
+
for (let corner = 0; corner < 2; corner += 1) {
|
|
74
|
+
bladeOffsets.push(offsetX, offsetZ);
|
|
75
|
+
bladeVary.push(yaw, heightScale, widthScale, phase);
|
|
76
|
+
bladeBendScale.push(bendScale);
|
|
77
|
+
}
|
|
78
|
+
if (segment < resolved.segments) {
|
|
79
|
+
const base = blade * vertsPerBlade + segment * 2;
|
|
80
|
+
indices.push(base, base + 1, base + 3, base, base + 3, base + 2);
|
|
81
|
+
}
|
|
42
82
|
}
|
|
43
83
|
}
|
|
44
84
|
const geometry = new THREE.InstancedBufferGeometry();
|
|
45
85
|
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
|
46
86
|
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
|
|
47
87
|
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
|
|
88
|
+
geometry.setAttribute("bladeOffset", new THREE.Float32BufferAttribute(bladeOffsets, 2));
|
|
89
|
+
geometry.setAttribute("bladeVary", new THREE.Float32BufferAttribute(bladeVary, 4));
|
|
90
|
+
geometry.setAttribute("bladeBendScale", new THREE.Float32BufferAttribute(bladeBendScale, 1));
|
|
48
91
|
geometry.setIndex(indices);
|
|
92
|
+
const tuftCount = grassTuftCount(resolved.count, resolved.tuftBlades);
|
|
49
93
|
const random = createSeededRandom(resolved.seed);
|
|
50
|
-
const offsets = new Float32Array(
|
|
51
|
-
const yaw = new Float32Array(
|
|
52
|
-
const heights = new Float32Array(
|
|
53
|
-
const widths = new Float32Array(
|
|
54
|
-
const bend = new Float32Array(
|
|
55
|
-
const phase = new Float32Array(
|
|
56
|
-
const colorMix = new Float32Array(
|
|
57
|
-
|
|
94
|
+
const offsets = new Float32Array(tuftCount * 3);
|
|
95
|
+
const yaw = new Float32Array(tuftCount);
|
|
96
|
+
const heights = new Float32Array(tuftCount);
|
|
97
|
+
const widths = new Float32Array(tuftCount);
|
|
98
|
+
const bend = new Float32Array(tuftCount);
|
|
99
|
+
const phase = new Float32Array(tuftCount);
|
|
100
|
+
const colorMix = new Float32Array(tuftCount);
|
|
101
|
+
const halfW = size.width / 2;
|
|
102
|
+
const halfD = size.depth / 2;
|
|
103
|
+
for (let index = 0; index < tuftCount; index += 1) {
|
|
58
104
|
const x = (random() - 0.5) * size.width;
|
|
59
105
|
const z = (random() - 0.5) * size.depth;
|
|
60
106
|
offsets[index * 3] = x;
|
|
61
107
|
offsets[index * 3 + 1] = resolved.heightAt(x, z);
|
|
62
108
|
offsets[index * 3 + 2] = z;
|
|
63
109
|
yaw[index] = random() * Math.PI * 2;
|
|
64
|
-
|
|
110
|
+
let heightScale = randomRange(random, resolved.height);
|
|
111
|
+
// Two random draws happen on every path so the seeded stream stays aligned regardless of feather.
|
|
112
|
+
const gate = random();
|
|
113
|
+
const settle = random();
|
|
114
|
+
if (resolved.edgeFeather > 0) {
|
|
115
|
+
// Tufts drop out probabilistically across the feather band (mostly keeping full height),
|
|
116
|
+
// so the border reads as grass growing sparser — not a mowed short-pile strip.
|
|
117
|
+
const edge = Math.min(halfW - Math.abs(x), halfD - Math.abs(z));
|
|
118
|
+
const inside = Math.min(1, Math.max(0, edge / resolved.edgeFeather));
|
|
119
|
+
heightScale *= gate < inside * inside ? 0.7 + 0.3 * settle : 0;
|
|
120
|
+
}
|
|
121
|
+
for (const zone of resolved.exclude) {
|
|
122
|
+
// Signed distance outside the rect: blades vanish inside and regrow across the feather ramp.
|
|
123
|
+
const d = Math.max(Math.abs(x - zone.center[0]) - zone.half[0], Math.abs(z - zone.center[1]) - zone.half[1]);
|
|
124
|
+
heightScale *= Math.min(1, Math.max(0, d / Math.max(0.001, zone.feather ?? 1.5)));
|
|
125
|
+
}
|
|
126
|
+
heights[index] = heightScale;
|
|
65
127
|
widths[index] = randomRange(random, resolved.width);
|
|
66
128
|
bend[index] = randomRange(random, resolved.bend);
|
|
67
129
|
phase[index] = random() * Math.PI * 2;
|
|
@@ -74,7 +136,7 @@ export function createGrassBladeGeometry(options = {}) {
|
|
|
74
136
|
geometry.setAttribute("instanceBend", new THREE.InstancedBufferAttribute(bend, 1));
|
|
75
137
|
geometry.setAttribute("instancePhase", new THREE.InstancedBufferAttribute(phase, 1));
|
|
76
138
|
geometry.setAttribute("instanceColorMix", new THREE.InstancedBufferAttribute(colorMix, 1));
|
|
77
|
-
geometry.instanceCount =
|
|
139
|
+
geometry.instanceCount = tuftCount;
|
|
78
140
|
geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), Math.max(size.width, size.depth) * 0.75 + resolved.height[1]);
|
|
79
141
|
return geometry;
|
|
80
142
|
}
|
|
@@ -6,11 +6,22 @@ export interface GrassWindOptions {
|
|
|
6
6
|
gustScale?: number;
|
|
7
7
|
flutter?: number;
|
|
8
8
|
}
|
|
9
|
+
/** Camera-distance fade band: tufts thin out between `start` and `end` meters, so the instance budget spends where the camera lives. */
|
|
10
|
+
export interface GrassDistanceFadeOptions {
|
|
11
|
+
start?: number;
|
|
12
|
+
end?: number;
|
|
13
|
+
}
|
|
9
14
|
export interface GrassMaterialOptions {
|
|
10
15
|
colorBase?: THREE.ColorRepresentation;
|
|
11
16
|
colorTip?: THREE.ColorRepresentation;
|
|
17
|
+
/** Ground color the blade roots blend into (pass the terrain color under the patch so blades grow *out of* the ground instead of floating on it). Defaults to a darkened `colorBase`. */
|
|
18
|
+
colorGround?: THREE.ColorRepresentation;
|
|
12
19
|
colorVariation?: number;
|
|
13
20
|
wind?: GrassWindOptions | false;
|
|
21
|
+
/** Distance thinning; `false` disables. Default fades between 35 m and 95 m. */
|
|
22
|
+
distanceFade?: GrassDistanceFadeOptions | false;
|
|
23
|
+
/** 0..1 blend of blade normals toward straight-up — lit like a soft carpet instead of dark individual planes. Default 0.6. */
|
|
24
|
+
normalLift?: number;
|
|
14
25
|
roughness?: number;
|
|
15
26
|
}
|
|
16
27
|
export interface GrassShaderUniforms {
|
|
@@ -22,14 +33,21 @@ export interface GrassShaderUniforms {
|
|
|
22
33
|
uWindFlutter: THREE.IUniform<number>;
|
|
23
34
|
uColorBase: THREE.IUniform<THREE.Color>;
|
|
24
35
|
uColorTip: THREE.IUniform<THREE.Color>;
|
|
36
|
+
uColorGround: THREE.IUniform<THREE.Color>;
|
|
25
37
|
uColorVariation: THREE.IUniform<number>;
|
|
38
|
+
uDistanceFade: THREE.IUniform<THREE.Vector2>;
|
|
39
|
+
uNormalLift: THREE.IUniform<number>;
|
|
26
40
|
}
|
|
27
41
|
export interface GrassMaterialHandle {
|
|
28
42
|
material: THREE.MeshStandardMaterial;
|
|
29
43
|
uniforms: GrassShaderUniforms;
|
|
30
44
|
}
|
|
31
45
|
export declare const DEFAULT_GRASS_WIND: Required<GrassWindOptions>;
|
|
46
|
+
/** Default camera-distance fade band: tufts start thinning at 35 m and are gone by 95 m. */
|
|
47
|
+
export declare const DEFAULT_GRASS_DISTANCE_FADE: Required<GrassDistanceFadeOptions>;
|
|
32
48
|
/** @internal */
|
|
33
49
|
export declare function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>;
|
|
34
50
|
/** @internal */
|
|
51
|
+
export declare function resolveGrassDistanceFade(fade: GrassDistanceFadeOptions | false | undefined): Required<GrassDistanceFadeOptions>;
|
|
52
|
+
/** @internal */
|
|
35
53
|
export declare function createGrassMaterial(options?: GrassMaterialOptions): GrassMaterialHandle;
|