threedgs Interactive demo
Loading splats 0%
Drag to orbit · Scroll to zoom

Three.js rendering library

3D Gaussian Splatting for Three.js.

Render 3D Gaussian Splatting scenes with a focused library built for Three.js.

Getting started

Browser

Save this as index.html, place scene.sog beside it, and serve the directory with a local web server.

index.html
<canvas id="canvas"></canvas>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.175.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.175.0/examples/jsm/",
    "threedgs": "https://cdn.jsdelivr.net/npm/threedgs@latest/threedgs.js"
  }
}
</script>

<script type="module">
import { PerspectiveCamera, Scene, WebGLRenderer } from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { Sog, SplatMesh } from 'threedgs'

const canvas = document.querySelector('#canvas')
const renderer = new WebGLRenderer({ canvas, antialias: false })
renderer.setSize(innerWidth, innerHeight)

const scene = new Scene()
const camera = new PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 1000)
camera.position.z = 3

const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
controls.autoRotate = true

const sog = await Sog.load('/scene.sog')
const splats = SplatMesh.create(sog)
scene.add(splats)

renderer.setAnimationLoop(() => {
  controls.update()
  renderer.render(scene, camera)
})
</script>

npm

For a bundled application, install threedgs and Three.js from npm.

$ npm install threedgs three

Render data

SplatRenderData is the seam between parsed source data and SplatMesh: it supplies geometry, uniforms, GLSL decoding, sortable positions, draw-count updates, and resource disposal.

Interface

Implement this interface when a source format needs its own rendering adapter:

import type { BufferGeometry } from 'three'

export interface SplatRenderData {
  // Total number of splats represented by this adapter.
  readonly vertexCount: number

  // Instanced geometry with a uint `drawOffset` attribute.
  // Within each instance, every four consecutive vertex ids represent one splat.
  readonly geometry: BufferGeometry

  // Values exposed to the shader under `renderData`.
  readonly uniforms: Record<string, unknown>

  // GLSL inserted after the shared `RenderState` and `Splat` declarations.
  // Must define `Splat decodeSplat(int dataIndex)`. `Splat.index` is stable
  // for modifiers; position is local-space; rotation is a normalized
  // scalar-first `(w, x, y, z)` quaternion; scale is positive and linear;
  // color contains RGB with opacity in alpha.
  readonly shaderParts: string

  // `vertexCount * 3` flat local-space XYZ values used for sorting.
  getPositions(): Float32Array

  // Draw the first `count` splats.
  setDrawCount(count: number): void

  // Release owned resources; safe to call more than once.
  dispose(): void
}

Each adapter instance is owned by one SplatMesh. Register its factory against the source data's exact constructor:

SplatMesh.registerRenderData(CustomData, (data, options) =>
  new CustomRenderData(data, options)
)

Registered render data

The package registers support for these formats by default.

Format Description
PLY Industry-standard uncompressed format for source, editing, and interchange.
Compressed PLY PlayCanvas's compressed PLY format, detected and decompressed automatically when read.
SOG PlayCanvas's bundled super-compressed format for web delivery.

Render modifiers

SplatRenderModifier changes a splat effect or appearance in the shader without rewriting or copying the source model data.

  • Add custom uniforms for animation and runtime controls.
  • Inject GLSL through modifySplat, modifySplatProjection, or modifyFragmentColor.
  • Control sub-pixel splat culling with smallSplatCulling.

Each example includes its complete implementation and usage. Choose one to restart it in the interactive preview.

import { SplatRenderModifier } from 'threedgs'

const modifier = new SplatRenderModifier({
  uniforms: {
    effect: {
      value: {
        center: splats.worldToLocal(controls.target.clone()),
        progress: 0,
      },
    },
  },
  shaderParts: {
    vertex: {
      declarations: `
        struct EffectUniforms {
            vec3 center;
            float progress;
        };
        uniform EffectUniforms effect;

        float radialProgress(vec3 position, float phase) {
            float positionProgress =
                clamp(length((position - effect.center).xz) / 10.0, 0.0, 1.0);
            float start = min(positionProgress, 0.92);
            return clamp((phase - start) / 0.08, 0.0, 1.0);
        }
      `,
      modifySplat: `
        bool modifySplat(inout Splat splat) {
            float pointReveal = radialProgress(
                splat.position,
                pow(clamp(effect.progress / 0.55, 0.0, 1.0), 2.0)
            );
            vec2 offset = (splat.position - effect.center).xz;
            vec2 direction = length(offset) > 0.0001
                ? normalize(offset)
                : vec2(0.0);

            splat.position.xz += direction
                * sin(pointReveal * 3.14159265)
                * 0.18;
            splat.color.a *= pointReveal;
            return pointReveal > 0.0;
        }
      `,
      modifySplatProjection: `
        void modifySplatProjection(
            const in Splat splat,
            inout vec2 splatOffset
        ) {
            float gaussianReveal = radialProgress(
                splat.position,
                clamp((effect.progress - 0.55) / 0.45, 0.0, 1.0)
            );
            vec4 center = renderState.projectionMatrix
                * renderState.modelViewMatrix
                * vec4(splat.position, 1.0);
            vec2 screenOffset = splatOffset
                * renderState.viewport
                / center.w;
            vec2 pointOffset = length(screenOffset) > 0.0001
                ? normalize(screenOffset) * 6.0
                    / renderState.viewport * center.w
                : vec2(0.0);

            splatOffset = mix(pointOffset, splatOffset, gaussianReveal);
        }
      `,
    },
  },
})

splats.setRenderModifier(modifier)
const startTime = performance.now()

renderer.setAnimationLoop((time) => {
  const elapsed = Math.min((time - startTime) / 9000, 1)
  const progress = elapsed * elapsed * (3 - 2 * elapsed)
  modifier.uniforms.effect.value.progress = progress
  modifier.invalidateUniforms()
  controls.update()
  renderer.render(scene, camera)
})

After changing uniform values in place, call invalidateUniforms(). If shader snippets change, call invalidateDefinitions() to rebuild the material.

The mesh does not own the modifier. Detach it with setRenderModifier(null), then reuse it with another mesh if needed.

modifySplat receives local-space data. Sorting and coarse culling still use the source position; use renderState.modelMatrix and renderState.inverseModelMatrix for world-space effects.

After attaching a modifier, inspect the composed shader through splats.material.vertexShader and splats.material.fragmentShader.