@interverse/three-scatter
v1.1.1
Published
High-performance instanced scatter systems for Three.js with chunk-based LOD
Downloads
368
Maintainers
Readme
@interverse/three-scatter
High-performance instanced scatter systems for Three.js with chunk-based LOD and multiple distribution modes.
📦 Installation
npm install @interverse/three-scatter
# or
yarn add @interverse/three-scatterPeer Dependencies:
three>= 0.182.0
Features
- 8 Scatter Systems - Different distribution patterns for every need
- Chunk-based LOD - Only renders instances near the camera
- Multi-Level LOD - Progressive density reduction with distance
- Frustum Culling - Skip chunks outside camera view
- Density Maps - Texture-based density modulation
- Event Callbacks - Lifecycle hooks for monitoring
- Serialization - Save/load configurations as JSON
- Biome Blending - Multi-layer management with masks
- Runtime Editing - Paint/erase instances with brush tool
- Deterministic placement - Same seed = same results
- Noise-based distribution - Natural-looking patterns
- Instance pooling - Efficient memory management
Scatter Systems Overview
| System | Use Case |
|--------|----------|
| MeshScatterSystem | Surface sampling on any mesh |
| HeightmapScatterSystem | Terrain vegetation with height/mask textures |
| CurveScatterSystem | Distribution along a THREE.Curve |
| SplineScatterSystem | Advanced spline with banking and Frenet frames |
| VolumeScatterSystem | 3D volume filling (box, sphere, cylinder) |
| RadialScatterSystem | Ring/circular patterns |
| GridScatterSystem | Regular grids with skip patterns |
| PhysicsScatterSystem | Physics-simulated natural placement |
Quick Start
import * as THREE from 'three';
import { MeshScatterSystem } from '@interverse/three-scatter';
const scatter = new MeshScatterSystem({
source: treeMesh,
surfaceMesh: terrainMesh,
density: 0.01,
visibilityRange: 200,
scaleRange: [0.8, 1.2],
alignToNormal: true
});
scene.add(scatter); // Scatter systems are THREE.Groups
function animate() {
scatter.update(camera); // Update chunk visibility
renderer.render(scene, camera);
}Base Configuration
All systems share these options:
interface BaseScatterConfig {
source: THREE.Mesh | THREE.Group; // Object to instance
density: number; // Instances per unit area
visibilityRange: number; // LOD distance
maxInstances?: number; // Default: 10000
chunkSize?: number; // Default: 64
scaleRange?: [number, number]; // Default: [0.8, 1.2]
rotationRange?: [number, number]; // Default: [0, 2π]
heightOffset?: number; // Default: 0
alignToNormal?: boolean; // Default: true
randomSeed?: number; // For deterministic placement
showChunksDebug?: boolean; // Visualize chunks
noiseDistribution?: NoiseDistributionConfig;
lod?: LODConfig; // Multi-level LOD
densityMap?: DensityMapConfig; // Texture-based density
events?: ScatterEvents; // Lifecycle callbacks
}🎯 Multi-Level LOD
Progressive density reduction based on camera distance:
const scatter = new MeshScatterSystem({
source: treeMesh,
surfaceMesh: terrain,
density: 0.1,
visibilityRange: 500,
lod: {
levels: [
{ distance: 0, densityMultiplier: 1.0 }, // Full density up close
{ distance: 100, densityMultiplier: 0.5 }, // Half at 100 units
{ distance: 200, densityMultiplier: 0.2 }, // 20% at 200 units
{ distance: 300, densityMultiplier: 0.05 } // Sparse beyond
],
blendDistance: 20 // Smooth transition between levels
}
});
// Toggle frustum culling
scatter.setFrustumCulling(true);🗺️ Density Maps
Use textures to control where instances spawn:
const scatter = new HeightmapScatterSystem({
source: treeMesh,
// ... other config
densityMap: {
textureUrl: '/textures/forest-density.png',
channel: 'r', // 'r' | 'g' | 'b' | 'a'
worldBounds: new THREE.Box2(
new THREE.Vector2(-500, -500),
new THREE.Vector2(500, 500)
),
multiplier: 1.0 // Scale the sampled value
}
});White areas = full density, black = no instances.
📡 Event Callbacks
Monitor scatter system lifecycle:
const scatter = new MeshScatterSystem({
// ... config
events: {
onChunkActivated: (chunkKey, instanceCount) => {
console.log(`Chunk ${chunkKey}: ${instanceCount} instances`);
},
onChunkDeactivated: (chunkKey) => {
console.log(`Deactivated: ${chunkKey}`);
},
onStatsChanged: (stats) => {
updateUI(stats.instances.active);
}
}
});💾 Serialization
Save and load scatter configurations:
import { ScatterSerializer } from '@interverse/three-scatter';
// Save
const json = ScatterSerializer.toJSON(scatterConfig, 'HeightmapScatterSystem');
localStorage.setItem('scatter-config', json);
// Load
const saved = localStorage.getItem('scatter-config');
const { type, config } = ScatterSerializer.fromJSON(saved, sourceMesh);
// Recreate system based on type
if (type === 'HeightmapScatterSystem') {
new HeightmapScatterSystem({ ...config, ...heightmapSpecificConfig });
}🌍 Biome Blending (ScatterBlender)
Manage multiple scatter layers with blend masks:
import { ScatterBlender, HeightmapScatterSystem } from '@interverse/three-scatter';
const blender = new ScatterBlender({
worldBounds: new THREE.Box2(
new THREE.Vector2(-500, -500),
new THREE.Vector2(500, 500)
),
updateInterval: 2 // Update every 2 frames
});
// Add layers
blender.addLayer('grass', new HeightmapScatterSystem(grassConfig), 1.0);
blender.addLayer('flowers', new HeightmapScatterSystem(flowerConfig), 0.5);
blender.addLayer('rocks', new HeightmapScatterSystem(rockConfig), 0.8);
// Initialize with blend mask (R=grass, G=flowers, B=rocks)
await blender.init('/textures/biome-mask.png');
scene.add(blender);
// Update each frame
blender.update(camera);
// Sample mask at position
const weights = blender.sampleBlendMask(x, z);
// { r: 0.8, g: 0.2, b: 0.5, a: 1.0 }
// Modify weights at runtime
blender.setLayerWeight('flowers', 0.8);🖌️ Runtime Editing (ScatterBrush)
Paint and erase instances interactively:
import { ScatterBrush } from '@interverse/three-scatter';
const brush = new ScatterBrush(scatter, {
radius: 5,
strength: 1.0,
density: 10, // Instances per stroke
falloff: 'smooth' // 'constant' | 'linear' | 'smooth'
});
// On mouse click
const hitPoint = ScatterBrush.getIntersection(raycaster, [terrain]);
if (hitPoint) {
if (isPainting) {
// Paint with optional height provider
const painted = brush.paint(hitPoint, (x, z) => terrain.getHeight(x, z));
console.log(`Painted ${painted.length} instances`);
} else {
// Erase
const erased = brush.erase(hitPoint);
console.log(`Erased ${erased} instances`);
}
}
// Brush controls
brush.setRadius(10);
brush.setStrength(0.5);
brush.setDensity(20);
// Get painted positions (for saving)
const positions = brush.getPaintedPositions();
// Clear all painted
brush.clearAll();🌲 HeightmapScatterSystem
Distributes instances on terrain using heightmap and mask textures.
import { HeightmapScatterSystem } from '@interverse/three-scatter';
const vegetation = new HeightmapScatterSystem({
source: treeMesh,
density: 0.02,
visibilityRange: 300,
// Heightmap config
worldSize: 1000,
heightMapUrl: '/textures/terrain_height.png',
heightMapScale: 50, // Height multiplier
maskMapUrl: '/textures/vegetation_mask.png',
slopeLimit: 35 // Max slope in degrees
});
scene.add(vegetation);| Option | Type | Description |
|--------|------|-------------|
| worldSize | number | Total terrain size in units |
| heightMapUrl | string | URL to grayscale height texture |
| heightMapScale | number | Height multiplier (default: 0.2) |
| maskMapUrl | string | URL to mask (white = place, black = skip) |
| slopeLimit | number | Maximum slope in degrees (default: 45) |
🔄 CurveScatterSystem
Distributes instances along a THREE.Curve (fences, paths, power lines).
import { CurveScatterSystem } from '@interverse/three-scatter';
const path = new THREE.CatmullRomCurve3([
new THREE.Vector3(-10, 0, 0),
new THREE.Vector3(0, 5, 10),
new THREE.Vector3(10, 0, 0)
]);
const fence = new CurveScatterSystem({
source: fencePostMesh,
density: 1,
visibilityRange: 200,
// Curve config
curve: path,
spacing: 2, // Fixed spacing (overrides segmentCount)
width: 3, // Width perpendicular to curve
offsetRange: [-0.5, 0.5] // Random perpendicular offset
});
scene.add(fence);
// Update curve dynamically
fence.updateCurve(newCurve, segmentCount);| Option | Type | Description |
|--------|------|-------------|
| curve | THREE.Curve<Vector3> | Any Three.js curve |
| segmentCount | number | Points along curve (default: 50) |
| spacing | number | Alternative: fixed distance between instances |
| width | number | Distribution width perpendicular to curve |
| offsetRange | [number, number] | Random perpendicular offset |
| handles | THREE.Object3D[] | Rotation/scale interpolation handles |
🛤️ SplineScatterSystem
Advanced spline distribution with Frenet frames, banking, and distribution modes.
import { SplineScatterSystem } from '@interverse/three-scatter';
const track = new THREE.CatmullRomCurve3(controlPoints, true);
const rails = new SplineScatterSystem({
source: railMesh,
density: 1,
visibilityRange: 500,
// Spline config
spline: track,
distributionMode: 'adaptive', // More points on curves
tension: 0.5,
width: 1.4,
distributionsPerSegment: 2, // Rails on both sides
bankAngle: 0.3, // Tilt on curves
followTangent: true,
upVector: new THREE.Vector3(0, 1, 0)
});
scene.add(rails);
rails.setBankAngle(0.5); // Update banking| Option | Type | Description |
|--------|------|-------------|
| spline | CatmullRomCurve3 \| BezierCurve | Spline to follow |
| distributionMode | 'uniform' \| 'adaptive' \| 'density-based' | Point distribution |
| tension | number | CatmullRom tension (default: 0.5) |
| width | number | Width perpendicular to spline |
| distributionsPerSegment | number | Instances per segment (for lanes) |
| bankAngle | number | Tilt angle in radians |
| followTangent | boolean | Orient along spline direction |
| upVector | Vector3 | Custom up vector for Frenet frame |
| offsetCurve | (t: number) => number | Custom offset function |
📦 VolumeScatterSystem
Fills a 3D volume with instances (particles, debris, asteroid fields).
import { VolumeScatterSystem } from '@interverse/three-scatter';
const debris = new VolumeScatterSystem({
source: rockMesh,
density: 0.001,
visibilityRange: 100,
// Volume config
bounds: new THREE.Box3(
new THREE.Vector3(-50, 0, -50),
new THREE.Vector3(50, 30, 50)
),
volumeType: 'sphere', // 'box' | 'sphere' | 'cylinder'
hollowness: 0.5, // 0 = solid, 0.5 = 50% center hollow
falloffDistance: 5 // Density fades at edges
});
scene.add(debris);
debris.updateBounds(newBounds);| Option | Type | Description |
|--------|------|-------------|
| bounds | THREE.Box3 | Bounding volume |
| volumeType | 'box' \| 'sphere' \| 'cylinder' | Shape type |
| hollowness | number | 0-1, creates hollow center |
| falloffDistance | number | Density falloff at edges |
🎯 RadialScatterSystem
Ring/circular distribution (forest clearings, explosion debris).
import { RadialScatterSystem } from '@interverse/three-scatter';
const clearing = new RadialScatterSystem({
source: treeMesh,
density: 0.05,
visibilityRange: 200,
// Radial config
center: new THREE.Vector3(0, 0, 0),
innerRadius: 20, // Creates hole in center
outerRadius: 100,
angleStart: 0, // Partial ring
angleEnd: Math.PI * 1.5, // 270 degrees
radialDensityFalloff: 1 // Denser toward outside
});
scene.add(clearing);
clearing.updateRadialBounds(30, 120);| Option | Type | Description |
|--------|------|-------------|
| center | Vector3 | Center point |
| innerRadius | number | Inner radius (0 = filled circle) |
| outerRadius | number | Outer radius |
| angleStart | number | Start angle in radians |
| angleEnd | number | End angle in radians |
| heightRange | [number, number] | Y position range |
| radialDensityFalloff | number | 0 = uniform, >0 = denser outside |
🔳 GridScatterSystem
Regular grid distribution with skip patterns (orchards, street lights).
import { GridScatterSystem } from '@interverse/three-scatter';
const orchard = new GridScatterSystem({
source: appleMesh,
density: 1,
visibilityRange: 200,
// Grid config
gridSize: new THREE.Vector2(20, 20),
cellSize: 5,
center: new THREE.Vector3(0, 0, 0),
randomOffset: 0.2, // Jitter within cell
// Skip pattern for paths
skipPattern: (x, z) => {
return x === 10 || z === 10; // Cross-shaped path
}
});
scene.add(orchard);
orchard.updateGrid(new THREE.Vector2(30, 30), 4);| Option | Type | Description |
|--------|------|-------------|
| gridSize | Vector2 | Number of cells (X, Z) |
| cellSize | number | Size of each cell |
| center | Vector3 | Grid center |
| randomOffset | number | 0-1, random jitter within cells |
| skipPattern | (x, z) => boolean | Function to skip cells |
🎲 PhysicsScatterSystem
Physics-simulated placement for natural-looking debris and rocks.
import { PhysicsScatterSystem } from '@interverse/three-scatter';
const rocks = new PhysicsScatterSystem({
source: rockMesh,
density: 0.01,
visibilityRange: 200,
// Physics config
dropHeight: 50,
dropBounds: new THREE.Box3(
new THREE.Vector3(-50, 0, -50),
new THREE.Vector3(50, 0, 50)
),
groundMesh: terrainMesh,
simulationSteps: 300,
gravity: new THREE.Vector3(0, -9.8, 0),
enableCollisions: true,
// Callbacks
beforeSimulation: (body, index) => {
body.restitution = 0.3; // Bounciness
body.friction = 0.8;
},
afterSimulation: (position, rotation, index) => {
// Post-process final position
}
});
scene.add(rocks);
rocks.resimulate(); // Re-run physics| Option | Type | Description |
|--------|------|-------------|
| dropHeight | number | Height to drop from |
| dropBounds | THREE.Box3 | Area to drop instances |
| groundMesh | THREE.Mesh | Collision ground |
| simulationSteps | number | Physics iterations |
| gravity | Vector3 | Gravity vector |
| enableCollisions | boolean | Inter-object collisions |
| beforeSimulation | callback | Modify body before sim |
| afterSimulation | callback | Process results |
Noise Distribution
Add natural variation with Perlin noise:
noiseDistribution: {
enabled: true,
scale: 0.05, // Noise frequency
octaves: 4, // Detail levels
persistence: 0.5, // Amplitude falloff
lacunarity: 2.0, // Frequency increase
threshold: 0.4, // Only place where noise > threshold
power: 1.0, // Contrast adjustment
offset: 0.0, // Shift noise values
scaleVariation: 0.2 // Apply to instance scale
}Common API
All systems share these methods:
// Update each frame
scatter.update(camera);
// Change parameters
scatter.setDensity(0.02);
scatter.setVisibilityRange(300);
// Debug
scatter.toggleDebug(true); // Visualize chunk bounds
// Regenerate
scatter.regenerateAll();
// Statistics
const stats = scatter.getStats();
// { instances: { active, total, max }, chunks: { total, active }, meshes }
// Cleanup
scatter.dispose();License
MIT © Interverse Engine
