npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@interverse/three-scatter

v1.1.1

Published

High-performance instanced scatter systems for Three.js with chunk-based LOD

Downloads

368

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-scatter

Peer 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