Terrain Painter
Paint Mode
Paint the primary terrain texture
Textures
Brush Settings
50m
50%
100%
[ / ] Brush size
Left-click to paint | Right-click to rotate
Terrain Painting
Interactive texture and heightmap painting with real-time brush preview
Overview
This example demonstrates an interactive terrain painting system with real-time brush preview. The brush preview is rendered directly in the terrain shader using a composable TSL node that wraps the standard terrain color output.
Features
Brush Preview System
The painting system uses a composable shader node (createPaintableTerrainColorNode) that:
- Takes the base terrain color node as input
- Calculates distance from each fragment to the brush center
- Applies gaussian falloff based on brush softness
- Blends the preview texture over the terrain within the brush radius
- Adds a subtle ring outline at the brush edge
Paint Modes
- Base Texture - Paint the primary terrain texture layer
- Overlay Texture - Paint the secondary overlay layer
- Blend - Adjust the blend factor between base and overlay
- Raise/Lower Terrain - Modify the heightmap elevation
Brush Settings
- Radius - Size of the brush in world units (5-200m)
- Softness - Edge falloff (0 = hard edge, 1 = soft gaussian)
- Strength - Paint opacity/intensity (0-100%)
Controls
| Control | Action |
|---|---|
| Left-click + Drag | Paint on terrain |
| Right-click + Drag | Rotate camera |
| Scroll | Zoom camera |
| Middle-click + Drag | Zoom camera |
[ / ] | Decrease / Increase brush size |
Technical Implementation
Composable Color Node
The brush preview is implemented as a wrapper around the terrain color node:
const colorNode = createPaintableTerrainColorNode({
baseColorNode: createTerrainColorNodeTriplanarNoTile({...}),
textureArray,
brushUniforms,
textureScale: 50,
});Brush Uniforms
Brush state is managed via GPU uniforms for real-time updates:
const brushUniforms = createBrushPreviewUniforms();
// Update each frame
brushUniforms.brushPosition.value.set(worldX, worldZ);
brushUniforms.brushRadius.value = 50;
brushUniforms.brushSoftness.value = 0.5;
brushUniforms.brushActive.value = 1;Falloff Calculation
The brush uses a blend between hard and soft edges:
// In TSL shader
const hardEdge = float(1).sub(smoothstep(0.9, 1.0, normalizedDist));
const softEdge = exp(normalizedDist * normalizedDist * -3);
const falloff = mix(hardEdge, softEdge, brushSoftness);Reusable Components
The painting system includes several reusable UI components:
PaintingToolbar- Main container panelBrushSettings- Radius, softness, strength slidersTexturePalette- Texture selection gridPaintModeSelector- Mode toggle buttonsusePaintingState- State management hook
Import them from @/components/Painting:
import {
PaintingToolbar,
usePaintingState,
createBrushPreviewUniforms,
createPaintableTerrainColorNode,
} from "@/components/Painting";