Skip to content

Procedural Splats

Forge makes it easy to create Gaussian splat collections procedurally, and includes some splat constructors that are useful for common tasks like creating a grid or text made of splats. The example "Procedural Splats" puts some of these to use in a scene.

Adding splats to a collection

To create a PackedSplats with custom, procedurally-derived splats use the methods pushSplat or setSplat:

const splats = new PackedSplats();
const center = new THREE.Vector3(0, 0, 0);
const scales = new THREE.Vector3(0.1, 0.1, 0.1);
const quaternion = new THREE.Quaternion();
const opacity = 1.0;
const color = new THREE.Color()
splats.pushSplat(center, scales, quaternion, opacity, color);
...

Note that calling .pushSplat and .setSplat will automatically resize the PackedSplats array to fit the splat. Additionally, the center, scales, etc parameters are encoded into 16 bytes, and the original objects passed in (such as center: THREE.Vector3) are no longer used. Re-using these objects for all the splats is a good idea.

Alternatively, you can use the construct initializer callback:

const splats = new PackedSplats({
  construct: (splats) => {
    ...
    for (let i = 0; i < NUM_SPLATS; ++i) {
      // Compute splat #i
      ...
      splats.pushSplat(center, scales, quaternion, opacity, color);
    }
  },
});

Once you've created your PackedSplats, render it to the scene via a SplatMesh:

const mesh = new SplatMesh({ packedSplats: splats });
scene.add(mesh);

Alternatively, you can create the SplatMesh and its splats in the initializer, which internally passes your constructor to its contained PackedSplats:

const mesh = new SplatMesh({
  constructSplats: (splats) => {
    for (let i = 0; i < NUM_SPLATS; ++i) {
      // Compute splat #i
      ...
      splats.pushSplat(center, scales, quaternion, opacity, color);
    }
  },
});
scene.add(mesh);

Grid

import { constructGrid } from "@forge-gfx/forge";

const grid = new SplatMesh({
  constructSplats: (splats) => constructGrid({
    splats,
    extents: new THREE.Box3(
      new THREE.Vector3(-10, -10, -10),
      new THREE.Vector3(10, 10, 10),
    ),
  }),
});
scene.add(grid);

Required parameters

Parameter Description
splats PackedSplats object to add splats to
extents min and max box extents of the grid

Optional parameters

Parameter Default Description
stepSize 1 step size along each grid axis
pointRadius 0.01 spherical radius of each splat
pointShadowScale 2.0 relative size of the "shadow copy" of each splat placed behind it
opacity 1.0 splat opacity
color RGB-modulated grid splat color (THREE.Color) or function to set color for position: ((THREE.Color, THREE.Vector3) => void)

XYZ axis

import { constructAxes } from "@forge-gfx/forge";

const axes = new SplatMesh({
  constructSplats: (splats) => constructAxes({ splats }),
});
axes.position.set(0, 0, -1);
scene.add(axes);

Required parameters

Parameter Description
splats PackedSplats object to add splats to

Optional parameters

Parameter Default Description
scale 0.25 scale (splat scale along axis)
axisRadius 0.0075 radius of the axes (splat scale orthogonal to axis)
axisShadowScale 2.0 relative size of the "shadow copy" of each splat placed behind it
origins [new THREE.Vector3()] origins of the axes (default single axis at origin)

Splat sphere

import { constructSpherePoints } from "@forge-gfx/forge";

const sphere = new SplatMesh({
  constructSplats: (splats) => constructSpherePoints({
    splats,
    maxDepth: 4,
  }),
});
scene.add(sphere);

Required parameters

Parameter Description
splats PackedSplats object to add splats to

Optional parameters

Parameter Default Description
origin new THREE.Vector3() center of the sphere (default: origin)
radius 1.0 radius of the sphere
maxDepth 3 maximum depth of recursion for subdividing the sphere. Warning: splat count grows exponentially with depth
filter null filter function to apply to each point, for example to select points in a certain direction or other function ((THREE.Vector3) => boolean)
pointRadius 0.02 radius of each oriented splat
pointThickness 0.001 flatness of each oriented splat
color new THREE.Color(1, 1, 1) color of each splat (THREE.Color) or function to set color for point: ((THREE.Color, THREE.Vector3) => void)

Rasterizing Text

const splats = textSplats({
  text: string;
  font?: string;
  fontSize?: number;
  color?: THREE.Color;
  rgb?: THREE.Color;
  dotRadius?: number;
  textAlign?: "left" | "center" | "right" | "start" | "end";
  lineHeight?: number;
});
scene.add(splats);

Required parameters

Parameter Description
text text string to display

Optional parameters

Parameter Default Description
font "Arial" browser font to render text with
fontSize 32 font size in pixels/splats
color new THREE.Color(1, 1, 1) SplatMesh.recolor tint assuming white splats
rgb new THREE.Color(1, 1, 1) Individual splat color
dotRadius 0.8 splat radius (0.8 covers 1-unit spacing well)
textAlign "start" text alignment: "left", "center", "right", "start", "end"
lineHeight 1.0 line spacing multiplier, lines delimited by "\n"

Turning images into splats

const image = imageSplats({
  url: string;
  dotRadius?: number;
  subXY?: number;
  forEachSplat?: (
    width: number,
    height: number,
    index: number,
    center: THREE.Vector3,
    scales: THREE.Vector3,
    quaternion: THREE.Quaternion,
    opacity: number,
    color: THREE.Color,
  ) => number | null;
});
scene.add(image);

Required parameters

Parameter Description
url URL of the image to convert to splats (example: url: "./image.png")

Optional parameters

Parameter Default Description
dotRadius 0.8 Radius of each splat, default covers 1-unit spacing well
subXY 1 Subsampling factor for the image. Higher values reduce resolution, for example 2 will halve the width and height by averaging
forEachSplat undefined Optional callback function to modify each splat before it's added. Return null to skip adding the splat, or a number to set the opacity and add the splat with parameter values in the objects center, rgba etc. were passed into the forEachSplat callback. Ending the callback in return opacity; will retain the original opacity.

Example

// Load RGBA image from image.png, subsample it 2x
// horizontally and vertically, and create splats for
// the resulting pixels that have at least 10% opacity.
const image = imageSplats({
  url: "./image.png",
  subXY: 2,
  forEachSplat: (width, height, index, center, scales, quaternion, opacity, color) => {
    // Only keep splats with opacity 10% or higher
    return (opacity >= 0.1) ? opacity : null;
  },
});
scene.add(image);

Particle Effects

Some of the building blocks are meant to serve as "code inspiration", showing how a particle effect animation can be achieved using a "stateless" dyno computation graph that uses only the splat index input to produce pseudo-random numbers that drive various randomized particle effects.

Note that splat sorting takes a little bit of time and can "lag behind" the splat updates each frame, so it's important that there is a reasonably stable correspondence between each frame's splat output for the same index.

generators.staticBox(...options)

In staticBox you provide a 3D box to render random "static" splats within. The AABB is sliced up into a 3D grid of cells, and random X/Y/Z points within those cells are sampled each frame, retaining consistency in splat position between successive frames.

generators.snowBox(...options)

Similarly, snowBox produces splat trajectories that move in a deterministic fashion over time, with high similarity between adjacent frames. See "VFX - Particle Simulation" for an example that creates a snowBox.

A snowBox instance has a collection of properties that can be tuned to achieve different particle effects. DEFAULT_SNOW and DEFAULT_RAIN are example parameter sets that look a lot like snow and rain, and can be used as a starting point for further tweaking: const mySnow = { ...DEFAULT_SNOW, density: 500 };

const snowControls = generators.snowBox({
  box,
  minY,
  numSplats,
  density,
  anisoScale,
  minScale,
  maxScale,
  fallDirection,
  fallVelocity,
  wanderScale,
  wanderVariance,
  color1,
  color2,
  opacity,
  onFrame,
});
scene.add(snowControls.snow);
Parameter Default Value Description
box new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1)) min and max box extents of the snowBox
minY Number.NEGATIVE_INFINITY minimum y-coordinate to clamp particle position, which can be used to fake hitting a ground plane and lingering there for a bit
numSplats calculated from box and density number of splats to generate
density 100 density of splats per unit volume
anisoScale new THREE.Vector3(1, 1, 1) The xyz anisotropic scale of the splat, which can be used for example to elongate rain particles
minScale 0.001 Minimum splat particle scale
maxScale 0.005 Maximum splat particle scale
fallDirection new THREE.Vector3(0, -1, 0) The average direction of fall
fallVelocity 0.02 The average speed of the fall (multiplied with fallDirection)
wanderScale 0.01 The world scale of wandering overlay motion
wanderVariance 2 Controls how uniformly the particles wander in sync, more variance means more randomness in the motion
color1 new THREE.Color(1, 1, 1) Color 1 of the two colors interpolated between
color2 new THREE.Color(0.5, 0.5, 1) Color 2 of the two colors interpolated between
opacity 1 The base opacity of the splats
onFrame undefined Optional callback function to call each frame