threejs-scene

A lightweight imperative three.js app shell, rebuilt small.

createApp + a small module contract. Deterministic clock, seeded rng, unidirectional state flow, strict dispose chain. The successor core of threejs-scenes v3.

What it is

State flows down — store → module.update → scene, once per simulation tick. Input flows back only through setState/dispatch, never straight into scene objects. Same seed + same tick sequence reproduce the exact same world, headless included.

threejs-scene demo scene rendered by the playground

Core ideas

Deterministic clock

A fixed-step clock decouples simulation ticks from frame rate, so the same seed and tick sequence always reproduce the same world — headless included.

Seeded rng

One seeded RNG threads through app state, so anything that reads randomness stays reproducible run to run.

Unidirectional state flow

State flows down through module.update once per tick; input flows back only through setState/dispatch.

Strict dispose chain

Every module tears down cleanly. app.dispose() walks the chain so nothing leaks between scenes.

Post-processing

Post-processing is a pluggable module, same contract as the lighting and orbit modules: drop postProcessing() into use: [] and it owns the frame draw through a module render hook — no composer wiring at the call site. The WebGL effect catalogue (bloom, god rays, colour grade, DOF, CRT, glitch, chromatic aberration, and more) is ported from threejs-scenes.

import { postProcessing } from 'threejs-scene/modules/post'
import { createChromaticAberration } from 'threejs-scene/modules/post/webgl/ca'

const app = createApp(canvas, {
  use: [
    standardLighting(),
    postProcessing({
      bloom:   { strength: 0.8 },
      effects: () => [ createChromaticAberration({ strength: 1.2 }) ],
    }),
  ],
})

Every effect's parameters are adjustable live in the interactive demo, which runs this exact scene on the built package.

Layout

lib/time
Clock, loop — the fixed/variable-step simulation clock and the shared frame loop.
lib/state
Store, rng — the state container and the seeded random source threaded through it.
lib/render
Renderer, resize — three.js renderer setup and viewport-resize handling.
lib/lifecycle
Dispose — the strict teardown chain every module and the app itself follow.
lib/input
Pointer-gesture — input capture that only ever reaches the scene through dispatch.
lib/app
Composition root — createApp, wiring clock, store, renderer and modules together.
modules/
Behavior modules (lighting, orbit, postprocessing) built entirely on the public surface.

Quickstart

import { createApp, defineModule } from 'threejs-scene'
import { standardLighting } from 'threejs-scene/modules/lighting'
import { orbitControls } from 'threejs-scene/modules/orbit'

interface State { speed: number }

const turbine = defineModule<State>({
  name: 'turbine',
  build (ctx)                { /* create objects once, add to ctx.scene */ },
  update (state, frame, ctx) { /* project state onto them, every sim tick */ },
})

const app = createApp<State>(canvas, {
  state: { speed: 1 },
  seed:  7,
  clock: { mode: 'fixed', step: 1 / 120 },
  use:   [ standardLighting(), orbitControls() ],
})

app.use(turbine)          // runtime add; returned handle.remove() tears down
app.start()               // or app.tick() for deterministic stepping
app.setState({ speed: 2 })
app.dispose()

Install

Peer-depends on three >=0.160.0.

npm i threejs-scene three