Skip to content

Commit 28c1cad

Browse files
feat: Enhance terrain generation with new canyon, dunes, and badlands nodes
- Updated terrain-store to adjust default resolutions for vertex and texture. - Expanded TerrainNodeType to include 'canyon', 'dunes', and 'badlands'. - Implemented evaluateCanyon, evaluateDunes, and evaluateBadlands functions for advanced terrain generation. - Added detailed parameters for canyon and dune characteristics, including meanders, branches, and wind dynamics. - Introduced complex erosion and stratification processes for badlands generation. - Improved noise functions for more realistic terrain features.
1 parent 90c724f commit 28c1cad

File tree

8 files changed

+870
-7
lines changed

8 files changed

+870
-7
lines changed

src/features/terrain/components/terrain-editor.tsx

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,59 @@ const BaseNode: React.FC<any> = ({ id, data, selected }) => {
130130
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Amount</span><DragInput compact value={(n as any).data?.amount ?? 1} step={0.05} precision={2} onChange={(v) => setData({ amount: Math.max(0, Math.min(1, v)) })} /></div>
131131
</div>
132132
)}
133+
{n.type === 'canyon' && (
134+
<div className="space-y-1">
135+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Start X</span><DragInput compact value={(n as any).data?.startX ?? 0.2} step={0.01} precision={2} onChange={(v) => setData({ startX: Math.max(0, Math.min(1, v)) })} /></div>
136+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Start Y</span><DragInput compact value={(n as any).data?.startY ?? 0.8} step={0.01} precision={2} onChange={(v) => setData({ startY: Math.max(0, Math.min(1, v)) })} /></div>
137+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">End X</span><DragInput compact value={(n as any).data?.endX ?? 0.8} step={0.01} precision={2} onChange={(v) => setData({ endX: Math.max(0, Math.min(1, v)) })} /></div>
138+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">End Y</span><DragInput compact value={(n as any).data?.endY ?? 0.2} step={0.01} precision={2} onChange={(v) => setData({ endY: Math.max(0, Math.min(1, v)) })} /></div>
139+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Width</span><DragInput compact value={(n as any).data?.width ?? 0.1} step={0.01} precision={3} onChange={(v) => setData({ width: Math.max(0.01, v) })} /></div>
140+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Depth</span><DragInput compact value={(n as any).data?.depth ?? 0.8} step={0.05} precision={2} onChange={(v) => setData({ depth: Math.max(0, v) })} /></div>
141+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Meander</span><DragInput compact value={(n as any).data?.meander ?? 0.3} step={0.02} precision={2} onChange={(v) => setData({ meander: Math.max(0, Math.min(1, v)) })} /></div>
142+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Roughness</span><DragInput compact value={(n as any).data?.roughness ?? 0.5} step={0.05} precision={2} onChange={(v) => setData({ roughness: Math.max(0, Math.min(1, v)) })} /></div>
143+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Side Slope</span><DragInput compact value={(n as any).data?.sideSlope ?? 1.5} step={0.1} precision={2} onChange={(v) => setData({ sideSlope: Math.max(0.1, v) })} /></div>
144+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Op</span>
145+
<select className="bg-black/40 border border-white/10 rounded px-1 py-0.5" value={(n as any).data?.operation ?? 'add'} onChange={(e) => setData({ operation: e.target.value })}>
146+
{['add','mix','max','min','replace'].map((op) => <option key={op} value={op}>{op}</option>)}
147+
</select>
148+
</div>
149+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Amount</span><DragInput compact value={(n as any).data?.amount ?? 1} step={0.05} precision={2} onChange={(v) => setData({ amount: Math.max(0, Math.min(1, v)) })} /></div>
150+
</div>
151+
)}
152+
{n.type === 'dunes' && (
153+
<div className="space-y-1">
154+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Direction</span><DragInput compact value={(n as any).data?.direction ?? 45} step={5} precision={0} onChange={(v) => setData({ direction: v % 360 })} /></div>
155+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Wavelength</span><DragInput compact value={(n as any).data?.wavelength ?? 0.15} step={0.01} precision={3} onChange={(v) => setData({ wavelength: Math.max(0.01, v) })} /></div>
156+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Height</span><DragInput compact value={(n as any).data?.height ?? 0.4} step={0.02} precision={2} onChange={(v) => setData({ height: Math.max(0, v) })} /></div>
157+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Asymmetry</span><DragInput compact value={(n as any).data?.asymmetry ?? 0.7} step={0.05} precision={2} onChange={(v) => setData({ asymmetry: Math.max(0.1, Math.min(0.9, v)) })} /></div>
158+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Variation</span><DragInput compact value={(n as any).data?.variation ?? 0.3} step={0.02} precision={2} onChange={(v) => setData({ variation: Math.max(0, Math.min(1, v)) })} /></div>
159+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Coverage</span><DragInput compact value={(n as any).data?.coverage ?? 0.8} step={0.05} precision={2} onChange={(v) => setData({ coverage: Math.max(0, Math.min(1, v)) })} /></div>
160+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Spacing</span><DragInput compact value={(n as any).data?.spacing ?? 0.6} step={0.05} precision={2} onChange={(v) => setData({ spacing: Math.max(0.1, Math.min(2, v)) })} /></div>
161+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Op</span>
162+
<select className="bg-black/40 border border-white/10 rounded px-1 py-0.5" value={(n as any).data?.operation ?? 'add'} onChange={(e) => setData({ operation: e.target.value })}>
163+
{['add','mix','max','min','replace'].map((op) => <option key={op} value={op}>{op}</option>)}
164+
</select>
165+
</div>
166+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Amount</span><DragInput compact value={(n as any).data?.amount ?? 1} step={0.05} precision={2} onChange={(v) => setData({ amount: Math.max(0, Math.min(1, v)) })} /></div>
167+
</div>
168+
)}
169+
{n.type === 'badlands' && (
170+
<div className="space-y-1">
171+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Scale</span><DragInput compact value={(n as any).data?.scale ?? 3} step={0.1} precision={2} onChange={(v) => setData({ scale: Math.max(0.1, v) })} /></div>
172+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Erosion</span><DragInput compact value={(n as any).data?.erosion ?? 0.7} step={0.05} precision={2} onChange={(v) => setData({ erosion: Math.max(0, Math.min(1, v)) })} /></div>
173+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Layers</span><DragInput compact value={(n as any).data?.layers ?? 8} step={1} precision={0} onChange={(v) => setData({ layers: Math.max(2, Math.round(v)) })} /></div>
174+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Hardness</span><DragInput compact value={(n as any).data?.hardness ?? 0.6} step={0.05} precision={2} onChange={(v) => setData({ hardness: Math.max(0.1, Math.min(1, v)) })} /></div>
175+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Steepness</span><DragInput compact value={(n as any).data?.steepness ?? 1.2} step={0.1} precision={2} onChange={(v) => setData({ steepness: Math.max(0.1, v) })} /></div>
176+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Roughness</span><DragInput compact value={(n as any).data?.roughness ?? 0.4} step={0.05} precision={2} onChange={(v) => setData({ roughness: Math.max(0, Math.min(1, v)) })} /></div>
177+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Octaves</span><DragInput compact value={(n as any).data?.octaves ?? 5} step={1} precision={0} onChange={(v) => setData({ octaves: Math.max(1, Math.round(v)) })} /></div>
178+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Op</span>
179+
<select className="bg-black/40 border border-white/10 rounded px-1 py-0.5" value={(n as any).data?.operation ?? 'add'} onChange={(e) => setData({ operation: e.target.value })}>
180+
{['add','mix','max','min','replace'].map((op) => <option key={op} value={op}>{op}</option>)}
181+
</select>
182+
</div>
183+
<div className="grid grid-cols-2 gap-1 items-center"><span className="text-gray-400">Amount</span><DragInput compact value={(n as any).data?.amount ?? 1} step={0.05} precision={2} onChange={(v) => setData({ amount: Math.max(0, Math.min(1, v)) })} /></div>
184+
</div>
185+
)}
133186
{(n as any).data?.seed != null && (
134187
<div className="text-[10px] text-gray-500">seed: {(n as any).data.seed}</div>
135188
)}
@@ -154,7 +207,7 @@ export const TerrainEditor: React.FC<Props> = ({ open }) => {
154207
const [cmFlipY, setCmFlipY] = useState(false);
155208
useEffect(() => { setPortalContainer(document.body); }, []);
156209

157-
const nodeTypes = useMemo(() => ({ default: BaseNode, input: BaseNode, output: BaseNode, perlin: BaseNode, voronoi: BaseNode, mountain: BaseNode, crater: BaseNode } as unknown as NodeTypes), []);
210+
const nodeTypes = useMemo(() => ({ default: BaseNode, input: BaseNode, output: BaseNode, perlin: BaseNode, voronoi: BaseNode, mountain: BaseNode, crater: BaseNode, canyon: BaseNode, dunes: BaseNode, badlands: BaseNode } as unknown as NodeTypes), []);
158211

159212
const defaultNodes = useMemo(() => (graph?.nodes ?? []).map((n: any) => ({ id: n.id, type: n.type as any, position: n.position as any, data: { terrainId: teTerrainId }, dragHandle: '.rf-drag', draggable: true })), [graph?.nodes, teTerrainId]);
160213
const defaultEdges = useMemo(() => (graph?.edges ?? []).map((e: any) => ({ id: e.id, source: e.source, target: e.target, sourceHandle: e.sourceHandle, targetHandle: e.targetHandle })), [graph?.edges]);
@@ -218,7 +271,13 @@ export const TerrainEditor: React.FC<Props> = ({ open }) => {
218271
? { seed: Math.floor(Math.random() * 1e9), centerX: 0.5, centerY: 0.5, radius: 0.35, peak: 1.0, falloff: 2.0, sharpness: 1.5, ridges: 0.2, octaves: 4, gain: 0.5, lacunarity: 2.0, operation: 'add', amount: 1 }
219272
: type === 'crater'
220273
? { centerX: 0.5, centerY: 0.5, radius: 0.25, depth: 0.6, rimHeight: 0.2, rimWidth: 0.1, floor: 0.1, smooth: 0.5, operation: 'add', amount: 1 }
221-
: {};
274+
: type === 'canyon'
275+
? { startX: 0.2, startY: 0.8, endX: 0.8, endY: 0.2, width: 0.1, depth: 0.8, meander: 0.3, roughness: 0.5, sideSlope: 1.5, operation: 'add', amount: 1 }
276+
: type === 'dunes'
277+
? { seed: Math.floor(Math.random() * 1e9), direction: 45, wavelength: 0.15, height: 0.4, asymmetry: 0.7, variation: 0.3, coverage: 0.8, spacing: 0.6, operation: 'add', amount: 1 }
278+
: type === 'badlands'
279+
? { seed: Math.floor(Math.random() * 1e9), scale: 3, erosion: 0.7, layers: 8, hardness: 0.6, steepness: 1.2, roughness: 0.4, octaves: 5, operation: 'add', amount: 1 }
280+
: {};
222281
const node: any = { id, type, position: basePos, data };
223282
if (rf) rf.addNodes([{ id, type: type as any, position: basePos, data: { terrainId: teTerrainId }, dragHandle: '.rf-drag', draggable: true }]);
224283
updateGraph(teTerrainId, (g) => { g.nodes = [...g.nodes, node]; });
@@ -320,6 +379,9 @@ export const TerrainEditor: React.FC<Props> = ({ open }) => {
320379
<div className="px-1 pb-1">
321380
<button className="w-full text-left px-2 py-1 rounded hover:bg-white/10" onClick={() => { addNodeAt('mountain', cmPos); setCmOpen(false); }}>Mountain</button>
322381
<button className="w-full text-left px-2 py-1 rounded hover:bg-white/10" onClick={() => { addNodeAt('crater', cmPos); setCmOpen(false); }}>Crater</button>
382+
<button className="w-full text-left px-2 py-1 rounded hover:bg-white/10" onClick={() => { addNodeAt('canyon', cmPos); setCmOpen(false); }}>Canyon</button>
383+
<button className="w-full text-left px-2 py-1 rounded hover:bg-white/10" onClick={() => { addNodeAt('dunes', cmPos); setCmOpen(false); }}>Dunes</button>
384+
<button className="w-full text-left px-2 py-1 rounded hover:bg-white/10" onClick={() => { addNodeAt('badlands', cmPos); setCmOpen(false); }}>Badlands</button>
323385
</div>
324386
</div>
325387
</ContextMenu.Popup>

src/stores/terrain-store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export const useTerrainStore = create<TerrainStore>()(immer((set, get) => ({
3434
const id = nanoid();
3535
const defaults: Omit<TerrainResource, 'id' | 'meshId' | 'maps'> = {
3636
name: 'Terrain',
37-
vertexResolution: { x: 129, y: 129 }, // power-of-two+1 grid
38-
textureResolution: { width: 512, height: 512 },
37+
vertexResolution: { x: 128, y: 128 }, // power-of-two+1 grid
38+
textureResolution: { width: 128, height: 128 },
3939
width: 10,
4040
height: 10,
4141
heightScale: 3.0, // Elevation multiplier for visible displacement

src/types/terrain.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ export type TerrainNodeType =
88
| 'perlin' // noise-based height modifier
99
| 'voronoi' // cellular noise-based modifier
1010
| 'mountain' // geoprimitive
11-
| 'crater'; // geoprimitive
11+
| 'crater' // geoprimitive
12+
| 'canyon' // erosional geoprimitive
13+
| 'dunes' // aeolian sand dunes
14+
| 'badlands'; // stratified erosional terrain
1215

1316
export interface TerrainNodeBase {
1417
id: string;
@@ -88,13 +91,68 @@ export interface TerrainCraterNode extends TerrainNodeBase {
8891
};
8992
}
9093

94+
export interface TerrainCanyonNode extends TerrainNodeBase {
95+
type: 'canyon';
96+
data: {
97+
seed: number;
98+
centerX: number; centerY: number; // uv
99+
width: number; // canyon width in UV
100+
length: number; // canyon length in UV
101+
depth: number; // max canyon depth
102+
angle: number; // canyon orientation in radians
103+
meanders: number; // sinuosity amount
104+
branches: number; // number of side branches
105+
erosion: number; // erosion complexity
106+
stratification: number; // rock layer visibility
107+
operation: 'add' | 'mix' | 'max' | 'min' | 'replace';
108+
amount: number;
109+
};
110+
}
111+
112+
export interface TerrainDunesNode extends TerrainNodeBase {
113+
type: 'dunes';
114+
data: {
115+
seed: number;
116+
density: number; // dunes per UV unit
117+
height: number; // max dune height
118+
wavelength: number; // dominant dune spacing
119+
asymmetry: number; // windward/leeward slope ratio
120+
slipface: number; // steep face sharpness
121+
complexity: number; // secondary ripple detail
122+
windDirection: number; // wind direction in radians
123+
migration: number; // dune shape variation
124+
operation: 'add' | 'mix' | 'max' | 'min' | 'replace';
125+
amount: number;
126+
};
127+
}
128+
129+
export interface TerrainBadlandsNode extends TerrainNodeBase {
130+
type: 'badlands';
131+
data: {
132+
seed: number;
133+
scale: number; // overall feature scale
134+
stratification: number; // horizontal layering strength
135+
erosion: number; // vertical erosion channels
136+
weathering: number; // surface breakdown
137+
hardness: number; // resistant layer influence
138+
tilting: number; // geological tilting angle
139+
faulting: number; // fault line disruption
140+
drainageComplexity: number; // gully system complexity
141+
operation: 'add' | 'mix' | 'max' | 'min' | 'replace';
142+
amount: number;
143+
};
144+
}
145+
91146
export type TerrainNode =
92147
| TerrainInputNode
93148
| TerrainOutputNode
94149
| TerrainPerlinNode
95150
| TerrainVoronoiNode
96151
| TerrainMountainNode
97152
| TerrainCraterNode
153+
| TerrainCanyonNode
154+
| TerrainDunesNode
155+
| TerrainBadlandsNode
98156
| TerrainNodeBase;
99157

100158
export interface TerrainEdge {

src/utils/terrain/generate.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import type { TerrainGraph, TerrainNode } from '@/types/terrain';
2-
import { evaluatePerlin, evaluateVoronoi, evaluateMountain, evaluateCrater } from './terrain-nodes';
2+
import { evaluatePerlin, evaluateVoronoi, evaluateMountain, evaluateCrater, evaluateCanyon, evaluateDunes, evaluateBadlands } from './terrain-nodes';
33

44

55
const evaluators: Record<string, (node: TerrainNode, u: number, v: number, worldW: number, worldH: number, currentH: number) => number> = {
66
perlin: evaluatePerlin,
77
voronoi: evaluateVoronoi,
88
mountain: evaluateMountain,
99
crater: evaluateCrater,
10+
canyon: evaluateCanyon,
11+
dunes: evaluateDunes,
12+
badlands: evaluateBadlands,
1013
};
1114

1215
// Create a stable signature for a terrain graph that ignores node positions and transient ids

0 commit comments

Comments
 (0)