@threenative/core API
Every public function and class in @threenative/core. The small vanilla Three.js game runtime foundation.
On this page
Generated from the engine's capability manifest: 121 functions and 47 classes. Each entry is the exported signature, what it is for, and a working example. Start with the package overview.
addInSlices
Attach hundreds of built objects to the scene in slices, presenting a frame between each.
async function addInSlices<T>( objects: Iterable<T>, add: (object: T, index: number) => void, options: IAddInSlicesOptions =Use it to
- add hundreds of built objects to the scene without one multi-second frame
- stream a detail tier in behind a loading curtain without the page looking hung
Example
const report = await addInSlices(objects, (object) => ctx.add(object), {
onProgress: ({ added, total }) => setProgress(added / total),
while: () => generation.live,
});Good to know
- the objects, and where each one goes, stay the game's; this decides only when each joins the graph
- input order is the attach order and cannot be changed
- a false
whilestops the run and is reported asstopped, never thrown
Options
- sliceSize defaults to 256;
marker: falsesilences the TN_ADD_SLICES line, not the report
addSpan
Attribute the render phase to 100% with a nested span tree, off unless TN_FRAME_SPANS asks. Every non-leaf reports its own time minus the spans inside it, so an unmeasured part shows up as a residual instead of being filed under "other"; a child that outlives its parent reports a negative residual rather than a clamped zero.
function addSpan(id: SpanId, ms: number): voidUse it to
- find out what inside the render phase is actually costing the frame
- tell a shadow pass's traversal from the main pass's, with the residual computed
- price an optimisation against a measured part of the phase rather than the whole of it
Example
if (spansRequested()) setSpanRecorder(new SpanRecorder());Good to know
- off by default and installed by
TN_FRAME_SPANS=1; unset, every call site is one guarded return - the tree is closed against the frame budget's own render phase, so
TN_FRAME_SPANSandTN_FRAME_BUDGETdescribe the same frames - measurement only: no span changes what is drawn, in what order, or with which renderer
aerodynamicCoefficients
Fly a fixed-wing aircraft with a real force balance instead of a steered velocity.
function aerodynamicCoefficients( alpha: number, flaps = 0, gear = 0, brakes = 0, ):Use it to
- fly an airplane with lift, drag, stall and control authority
- launch an aircraft off a moving carrier deck
- apply component damage or a loadout to an aircraft's performance
Example
const model = new FlightModel({ airframe: sbd, state: aircraft, wind: seaWind });
model.step(1 / 60, { turn: -1, pitch: 0.4 });Good to know
- every mass, area, power and inertia value comes from the game's airframe
- damage, stores and configuration arrive as the game's own modifier sample
afterPhysics
Register work that reads a body or camera after physics has moved it and before this frame draws. The engine owns the phase ordering; a callback cannot be misplaced by plugin-array order.
function afterPhysics( context: IAfterPhysicsContext, callback: AfterPhysicsCallback, ): () => voidUse it to
- read a body after physics has moved it
- place a camera or aim from the solved character transform
Example
afterPhysics(ctx, (dt) => camera.position.copy(player.mesh.position));Good to know
- register from a scene context; callbacks are cleared when that scene exits
aircraftMass
Fly a fixed-wing aircraft with a real force balance instead of a steered velocity.
function aircraftMass(state: IFlightState, airframe: IAircraftAirframe): numberUse it to
- fly an airplane with lift, drag, stall and control authority
- launch an aircraft off a moving carrier deck
- apply component damage or a loadout to an aircraft's performance
Example
const model = new FlightModel({ airframe: sbd, state: aircraft, wind: seaWind });
model.step(1 / 60, { turn: -1, pitch: 0.4 });Good to know
- every mass, area, power and inertia value comes from the game's airframe
- damage, stores and configuration arrive as the game's own modifier sample
airDensity
Fly a fixed-wing aircraft with a real force balance instead of a steered velocity.
function airDensity(y: number): numberUse it to
- fly an airplane with lift, drag, stall and control authority
- launch an aircraft off a moving carrier deck
- apply component damage or a loadout to an aircraft's performance
Example
const model = new FlightModel({ airframe: sbd, state: aircraft, wind: seaWind });
model.step(1 / 60, { turn: -1, pitch: 0.4 });Good to know
- every mass, area, power and inertia value comes from the game's airframe
- damage, stores and configuration arrive as the game's own modifier sample
alwaysRender
Keep an object drawn even when the render camera cannot resolve it. The engine's projected-size gate is on by default: an object whose world bounding sphere projects to fewer than 0.5 raster pixels in the camera about to render it is not submitted, per camera. Mark the player's own cockpit, a nameplate, a quest marker, or anything a game never wants to pop out of the frame. alwaysRender(object, false) removes the marker. Camera-attached objects and shadow casters are already kept, and the number of marked objects is reported beside the cull in the TN_PROJECTION window rather than hidden. The threshold itself is renderer.minimumProjectedPixels — a larger number cuts more aggressively, false leaves every object drawn while still measuring.
function alwaysRender(object: Object3D, enabled = true): voidUse it to
- keep a small object drawn when the engine would skip it as too far to resolve
- stop my cockpit, marker or player model popping out at distance
- a tiny object disappeared at range and I need it always visible
- widen or narrow the projected-size cull with a named threshold
Example
import { alwaysRender } from "@threenative/core";
alwaysRender(ctx.camera.children[0]); // a camera-attached cockpit stays drawnGood to know
- the marker is per object and is reported as
exemptMarkedin the projection window renderer.minimumProjectedPixels: falseleaves the scene drawn and keeps the measurement on- the marker is per object and survives scene rebuilds only as long as the object does
- disabling the gate (
renderer.minimumProjectedPixels: false) keeps its measurement on
Options
- renderer.minimumProjectedPixels sets the projected-pixel threshold, default 0.5
AnimationPlayer
Play a skinned or sprite animation from game code. A locomotion clip's playback rate is matched to the ground the body actually covers, so feet do not skate or spin — on by default, strideSync: false to keep the authored rate, and player.stride reports the measurement either way. Name the body a game moves as strideRoot when the rig is a child of it. Clips authored in place — every ActorX and Unreal export, every Mixamo "in place" clip, every stock animal pack — are matched too: their stride is read off the ground a planted foot sweeps, and stride.inPlace says so.
class AnimationPlayerUse it to
- play an animation on a character
- switch a character between idle and attack clips
- stop a walking character's feet from sliding or spinning
- match a walk or run cycle to how fast a character is moving
- match an in-place walk cycle with no root motion to the body's speed
- find out what speed an animation clip was authored for
Example
const player = new AnimationPlayer({ clips, root: rig, strideRoot: body });Good to know
- name the body a game moves as strideRoot when the animated rig is a child of it
- a one-shot clip always plays at its authored rate; the matched rate re-times loops only
- the matched rate is held inside 0.15x-3x; a speed outside what the clip's own stride supports is clamped, and
stride.ratesays so
Options
- strideSync controls whether the matched rate is applied while stride is still measured
Atmosphere
Own the compute lifetime and expose only parameter-driven atmosphere nodes. The class deliberately creates no mesh, material, or scene light. A template chooses all of those, and the same object remains useful when a game supplies a completely different look.
class Atmosphere extends Group implements IComputeDrivenUse it to
- render a sunrise that changes as time and place change
- add distance haze from the depth of a scene pass
Example
const atmosphere = new Atmosphere({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });
ctx.add(atmosphere);Good to know
- supply rayleigh, mie, ozone, planetRadius, and atmosphereRadius; there is no Earth fallback
- the game creates the sky object, surface, and sun from the returned nodes
Also found by
- bright sky saturated green platforms
AtmosphereLuts
Own the transmittance, multi-scattering, and sky-view compute lookup textures.
class AtmosphereLutsUse it to
- bake the three atmosphere LUTs once before a game shows its world
Example
const luts = new AtmosphereLuts({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });Good to know
- supply all physical parameters; this class creates no scene appearance
attachToBone
Attach a game-owned object to a named skeleton bone.
function attachToBone(root: Object3D, boneName: string, child: Object3D): Object3DUse it to
- put a weapon in a character's hand
- hold a rifle in a character's right hand
- add an enemy that patrols the level, chases the player when it sees them, and holds a rifle in its right hand
- attach an accessory to a skinned model
- equip crew with swords and pistols
- put a sword or pistol in a crew member's hand
Example
import { attachToBone } from "@threenative/core";
attachToBone(character, "RightHand", rifle);Good to know
- when a request is to hold or attach a weapon to a hand, import and call
attachToBonefrom@threenative/core; do not manually parent, position, or rotate the rifle - if a stock visual has no skeleton, add a portable Three.js Bone named
RightHandunder the character, then callattachToBone; do not replace the helper with manual parenting
attitudeAxes
Fly a fixed-wing aircraft with a real force balance instead of a steered velocity.
function attitudeAxes(state: IFlightState): IFlightAxesUse it to
- fly an airplane with lift, drag, stall and control authority
- launch an aircraft off a moving carrier deck
- apply component damage or a loadout to an aircraft's performance
Example
const model = new FlightModel({ airframe: sbd, state: aircraft, wind: seaWind });
model.step(1 / 60, { turn: -1, pitch: 0.4 });Good to know
- every mass, area, power and inertia value comes from the game's airframe
- damage, stores and configuration arrive as the game's own modifier sample
AudioBus
Route effects through a named audio bus.
class AudioBusUse it to
- play a sound effect with a volume bus
- mute or adjust a category of game audio
- keep a gunshot audible at 20 metres by tuning positional falloff
- hold a loud mix below clipping with a compressor on the bus
- play cannon, wave, and ship sound effects
Example
const effects = new AudioBus({ camera });Good to know
- create buses before playing clips and dispose them with the game
- refDistance and rolloffFactor tune positional falloff and apply to playAt only
- compressor takes threshold, knee, ratio, attack and release from the game and takes effect on the bus sum
Replaces new Audio(.
baseGeometryOf
The full-detail geometry of a mesh the loader gave an automatic LOD chain. Selection swaps mesh.geometry, so a ray test or a collision body built from the current geometry would change with the camera. Framework picking and gameplay collide against this instead: the authored LOD0, which never changes as the camera moves.
function baseGeometryOf(mesh: Mesh): BufferGeometryUse it to
- collide or ray-test the authored geometry of a mesh whose render detail changes with distance
Example
const geometry = baseGeometryOf(mesh);beginSpan
Attribute the render phase to 100% with a nested span tree, off unless TN_FRAME_SPANS asks. Every non-leaf reports its own time minus the spans inside it, so an unmeasured part shows up as a residual instead of being filed under "other"; a child that outlives its parent reports a negative residual rather than a clamped zero.
function beginSpan(id: SpanId): voidUse it to
- find out what inside the render phase is actually costing the frame
- tell a shadow pass's traversal from the main pass's, with the residual computed
- price an optimisation against a measured part of the phase rather than the whole of it
Example
if (spansRequested()) setSpanRecorder(new SpanRecorder());Good to know
- off by default and installed by
TN_FRAME_SPANS=1; unset, every call site is one guarded return - the tree is closed against the frame budget's own render phase, so
TN_FRAME_SPANSandTN_FRAME_BUDGETdescribe the same frames - measurement only: no span changes what is drawn, in what order, or with which renderer
Billboard3D
Face a game-owned object toward a perspective or orthographic camera.
class Billboard3DUse it to
- keep a world-space marker or nameplate facing the camera
- billboard a tree, label, or effect under a rotated parent
Example
const billboard = new Billboard3D(label, { camera });
billboard.update();Good to know
- call update from the owning scene; no global scene scan is installed
- orthographic cameras use their forward direction, and lockAxis restricts world rotation
boneContact
Measure whether a named bone reaches the object it is supposed to be touching, in metres.
function boneContact( root: Object3D, boneName: string, target: Object3D, ): IBoneContactReportUse it to
- check that a seated character's hands reach the keyboard
- check that a character's hips meet the chair it is sitting on
- turn "the character is not touching the prop" into a number a scenario can assert
Example
import { boneContact } from "@threenative/core";
const contact = boneContact(worker, "hand_r", keyboard);Good to know
- this walks the target's vertices; call it on a check or a debug sample, not every frame
boneLengthDeviations
Compare a rig's bone distances now against a captured snapshot and name every bone that moved.
function boneLengthDeviations( root: Object3D, bind: IBoneLengthSnapshot, options: IBoneLengthDeviationsOptions =Use it to
- find out why a skinned character renders deformed
- check that an animated pose keeps the skeleton rigid
- name the bone that breaks a posed skeleton without taking a screenshot
Example
import { boneLengths, boneLengthDeviations } from "@threenative/core";
const report = boneLengthDeviations(character, boneLengths(character));
if (!report.rigid && report.worst !== null) console.log(report.worst.bone, report.worst.ratio);Good to know
- a rigid skeleton preserves every parent→child distance under any pose; a named bone is a defect with an address
- this is a diagnostic — it reports numbers and names; it moves nothing and decides no appearance
boneLengths
Measure a rig's parent→child bone distances, in world space, as it stands right now.
function boneLengths(root: Object3D): IBoneLengthSnapshotUse it to
- capture a rig's bind-pose bone lengths before any clip plays
- measure a skeleton for the bone-length invariance check
Example
import { boneLengths } from "@threenative/core";
const baseline = boneLengths(character);Good to know
- the baseline and the later comparison must come from the same rig under the same ancestor transform, so a uniform scale cancels
bvhIntersectFirstHit
Pack a selected static scene into TSL storage nodes for an upstream BVH ray query.
bvhIntersectFirstHit = upstream.bvhIntersectFirstHitUse it to
- trace thousands of scene rays inside a TSL kernel
- build a contact-occlusion or visibility query over loaded meshes
Example
const bvh = ctx.add(new GPUSceneBVH(ctx.scene, { include: (object) => object.userData.traceable === true }));Good to know
- call rebuild() after a scene transform or geometry change; the snapshot is static by default
- rebuild() is an explicit CPU SAH build proportional to selected triangles; process() is a no-op, and the game pays upstream traversal per shader ray
CameraShake
Produce a game-authored camera shake offset for a template-owned camera rig.
class CameraShakeUse it to
- add a hit, explosion, or landing shake to a camera
- compose a transient camera offset after camera damping
Example
const shake = new CameraShake({ amplitude, rotationAmplitude, frequency, decay, curve });Good to know
- amplitude, rotationAmplitude, frequency, decay, and curve are required game choices
- update returns an offset and never writes to a camera
CanvasLayer
Manage a camera or screen-facing canvas layer.
class CanvasLayerUse it to
- place a HUD layer above the Three.js scene
- attach a canvas layer to a camera
Example
const hud = new CanvasLayer(ctx.viewport);captureMouse
Lock the pointer to the game's surface: the capture every first-person mouse look needs. A browser grants capture only from a user gesture, so call it from a click or a pointerdown handler. A relative binding such as look: { pointerRelative: true } already requests capture on the first canvas click; this is the same request for a game that starts capture from another named gesture, and ctx.input.captureMouse() is the map's own way to ask.
function captureMouse(target: EventTarget): Promise<void> | undefinedUse it to
- lock the mouse pointer so first-person mouse look keeps the cursor out of the way
- stop the cursor leaving the window in the middle of a turn
Example
canvas.addEventListener("click", () => captureMouse(canvas));Good to know
- the browser grants capture only from a user gesture and a refusal is reported, never swallowed
- a relative binding requests capture on canvas click unless
captureOnClick: falseopts out
clipBoneCoverage
Report which bones of a character a clip does not drive.
function clipBoneCoverage(root: Object3D, clip: AnimationClip): IClipCoverageReportUse it to
- find out why a character keeps the previous animation's hand shape
- check how much of a rig a clip covers before shipping it
Example
import { clipBoneCoverage } from "@threenative/core";
const coverage = clipBoneCoverage(character, clip);Good to know
- a track that binds nothing counts as driving nothing
clipPoseError
Score a retargeted clip against the source it came from, per bone, in degrees.
function clipPoseError( subject: IClipPoseSubject, reference: IClipPoseSubject, options: IClipPoseErrorOptions =Use it to
- find out why a retargeted animation looks wrong on a character
- tell a fixed retarget from one that merely moved
- catch a retarget that rolled every limb about its own axis
Example
import { clipPoseError } from "@threenative/core";
const report = clipPoseError({ root: rig, clip: retargeted }, { root: source, clip: original });Good to know
- each bone is compared as a whole quaternion relative to its own rig's bind pose, so the two rigs never have to share a bind convention; a bone-direction check reports zero on the roll this catches
- both rigs must face the same way in world space, and both are driven and then restored to the transforms they arrived with
Options
- bones maps one rig's bone names onto the other's when they differ; samples sets how many poses are compared
clipTrackBindings
Report which of a clip's tracks bind to nothing on a character.
function clipTrackBindings(root: Object3D, clip: AnimationClip): IClipBindingReportUse it to
- find out why a character plays its bind pose instead of the animation
- check that a loaded clip actually drives the model it was written for
Example
import { clipTrackBindings } from "@threenative/core";
const bindings = clipTrackBindings(character, clip);Good to know
- the reason is Three.js's own, captured off its console hook so a scenario's console assertions stay clean
ClusteredBatch
Draw many copies of one over-detailed body, each at the detail its own distance earns. InstancedBatch collapses repeated props into one draw and gives every copy the same triangles. This gives a copy two hundred metres away a coarser cut than one twelve metres away, and still submits one draw per distance group rather than one per copy — which is what makes four hundred scanned boulders affordable. The shape, the surface and every transform stay the game's.
class ClusteredBatchUse it to
- draw hundreds of copies of a scanned or sculpted body without hundreds of draw calls
- stop distant copies of a dense prop from costing their full triangle count
Example
const boulders = new ClusteredBatch({ geometry, material, table });
boulders.place({ position: [x, y, z], rotation: [0, angle, 0] });
boulders.build({ name: "boulders", parent: ctx.scene });
// in the scene's update, before the render:
boulders.update(ctx.camera, ctx.renderer.domElement.height);Good to know
- the body's geometry must carry a cluster table from
assets.models.virtual - every copy is placed before build(); place() after build() throws
Options
- distanceRatio sets how wide one distance group is, default 1.25
- errorPixels sets the screen-space error budget, default 1 pixel
ClusteredMesh
Draw a model too detailed for the screen to resolve, without submitting the part it cannot. This is on, and a game does not call it. Any primitive of 65,536 triangles or more bakes to a cluster DAG in the asset pipeline, the loader returns a ClusteredMesh for it, and the engine takes the cut every frame before it renders. Each frame the mesh submits one draw holding only the clusters whose error projects to fewer than errorPixels screen pixels; a mesh nothing has cut yet draws in full, so the worst case is an ordinary Mesh. Geometry, surface and every appearance parameter stay the game's.
class ClusteredMesh extends MeshUse it to
- draw a model too detailed for the screen to resolve
- import a scanned or sculpted mesh of millions of triangles and still hold the frame
- stop a dense rock face or terrain body from costing its full triangle count up close
Example
// Nothing to call: the loader returns one of these and the engine cuts it every frame.
const face = await ctx.assets.model("quarry-face.glb");
ctx.scene.add(face.scene);Good to know
- the bake happens in the asset pipeline, never at run time — there is no runtime flag
- the payload costs about 3-4x a baked primitive's bytes;
assets.models.virtual: "none"opts out andminSourceTrianglesmoves the 65,536 line - needs a perspective camera; a screen-space error has no meaning without one
- one shadow cut is chosen at load and does not follow a shadow camera
Options
- errorPixels sets the screen-space error budget, default 1 pixel
- recutDistance sets how far the camera moves before the cut is retaken, default a thousandth of the mesh's radius
ComputeDrivenRegistry
Register game-owned IComputeDriven objects with the shared compute lifetime.
class ComputeDrivenRegistryUse it to
- use IComputeDriven for a cloth, fluid, boid, or other GPU simulation
- run a cloth or fluid simulation with ordered GPU passes
- keep IComputeDriven kernels warm before the first visible frame
Example
const registry = new ComputeDrivenRegistry(); registry.add(cloth, ctx.renderer.raw);Good to know
- add the object through ctx.add so it attaches, dispatches, and releases with its scene
counterDeviceOf
Count the frame's host-boundary crossings and the bytes it writes into GPU buffers, off unless TN_FRAME_SPANS asks for them. On the frame budget's own window as counters, so a crossing count and a millisecond split describe the same frames.
function counterDeviceOf(raw: unknown): unknownUse it to
- decide whether a CPU-bound frame is paying for the V8-to-host boundary
- measure how many bytes a frame writes into GPU buffers, and how many commands it issues
Example
const counters = FrameCounters.install(counterDeviceOf(renderer.raw));Good to know
- counts command-encoder and queue methods only;
mapAsyncand the presentation path are named, not folded in gpuBytesisqueue.writeBufferexactly, so it reconciles against a driver; texture uploads are not includedjsAllocBytesneedsperformance.memoryand stays absent where the platform lacks it
createAssetLoader
Create the portable asset loader a scene also receives as ctx.assets.
function createAssetLoader(options: IAssetLoaderOptions =Use it to
- preload models, textures, or audio before a scene enters
- load assets from a nonstandard base path or a compiled asset manifest
Example
const assets = createAssetLoader({ basePath: "/assets" });
const rock = await assets.texture("rock.png");Good to know
- reuse the loader handed to scenes as
ctx.assetsinstead of building parallel caches
Also found by
- different props in each area
- first playable screen external assets
createPipelineCensus
Read the renderer's bounded, versioned pipeline capture in a diagnostic or playtest tool.
function createPipelineCensus(options: IPipelineCensusOptions): PipelineCensusUse it to
- inspect shader and pipeline creation work during a real launch
- correlate pipeline creation with material, object, pass, and shader identities
Example
const capture = game.runtime.pipelineCensus?.();
// The game normally reaches this through `runtime.pipelineCensus`; direct construction exists
// for renderer adapters and contract tests, not for gameplay.Good to know
- the capture is bounded and incomplete when the backend cannot expose an observation
createRandom
Create a deterministic random source for portable gameplay.
function createRandom(seed?: number): IRandomUse it to
- get a seeded deterministic random number generator — the same mulberry32 a game would hand-roll
- seed enemy patrol choices
- reproduce the same procedural level in a playtest
Example
const random = createRandom(42);Good to know
- use the returned source instead of Math.random for replayable behavior
Replaces Math.random(.
createReplayDriver
Record or replay deterministic game input and state.
function createReplayDriver( recording: Recording, target: EventTarget, pointerTarget = target, )Use it to
- reproduce a gameplay bug from recorded input
- run a deterministic replay in a playtest
Example
const driver = createReplayDriver(recording, ctx.renderer.domElement);Good to know
- use a seeded random source and fixed-step simulation for meaningful replays
Also found by
- fixed seed fixed-step simulation
Daylight
An outdoor daylight rig: physical sky, one sun with open-world shadows that follow the eye, hemisphere fill, sky-coloured haze and the AgX tone curve. Every value is the game's.
class Daylight extends Group implements IComputeDrivenUse it to
- daytime sky, sun and shadows for a large outdoor map
- distant terrain should fade into the sky instead of a coloured wall
- match a Blender look-dev scene's sun, sky and exposure in the game
Example
const daylight = new Daylight({ follow: ctx.camera, sunDirection, sunColor, sunIntensity: 4, shadowExtents: [24, 96, 320], sky: { turbidity: 3, rayleigh: 1.4, mieCoefficient: 0.004, mieDirectionalG: 0.8 }, fill: { sky, ground, intensity: 1.1 }, haze: { color: horizon, density: 0.0011 }, exposure: 2 ** -0.6, skySize: 1600 });
ctx.add(daylight);Good to know
- every value is required; there is no default sun, sky, haze or exposure
skySizemust keep the sky box's corners inside the camera's far plane- shadowExtents follow
VirtualShadowNode: half-widths, finest first, strictly increasing
Options
- sky uniforms stay live on
daylight.sky; the light and fill aredaylight.sunanddaylight.fill
debugFlag
Read a debug switch from the URL, or from TN_DEBUG_* in the environment on a native launch.
function debugFlag(name: string): booleanUse it to
- read a debug toggle from the URL or an environment variable
Example
import { debugFlag } from "@threenative/core";
if (debugFlag("freeCam")) camera.flyMode = true;Good to know
- a name in camelCase becomes UPPER_SNAKE:
debugFlag("freeCam")reads?freeCamorTN_DEBUG_FREE_CAM 0andfalseare off, so a saved URL cannot turn a switch back on
defineGame
Define the portable game entry shared by web and native.
function defineGame<TState extends Record<string, unknown>, TPhysics = undefined>( config: IGameConfig<TState, TPhysics>, ): IGame<TState, TPhysics>Use it to
- start a ThreeNative game from src/game.ts
- register physics and gameplay plugins
- let the player zoom the camera with a wheel, pinch, or gamepad axis
- frame a camera behind the player
Example
const game = defineGame({ input: { zoom: { scroll: true, pinch: true } }, scenes: { Play }, start: "Play" });Good to know
- keep DOM and React mounting in src/main.ts
- bind scroll or pinch and read the intent with ctx.input.axis(name); do not add a window wheel listener
- scroll: true uses the DOM wheel sign on browser and native: negative deltaY toward the user is positive intent
Also found by
- firing line nearest target crosshair
- third-person camera
- restart the run without a page reload
- field of view while aiming
describeSceneShape
Warn the agent that built the scene before a human plays it: a frame whose GPU is idle while its JS render phase is longer than the display's own period is a scene-shape problem, and the engine already has the shape. On by default, printed at most once per reported window as TN_SCENE_WARNING, and silent on a scene that is honestly GPU-bound.
function describeSceneShape( window: IFrameBudgetWindow, cull: IRenderCameraCullReport | undefined, ): ISceneShape | undefinedUse it to
- find out whether a slow frame is the scene's shape or the device
- tell an authoring agent what to reduce before it promises a merge
Example
defineGame({ display: { maxFps: 60 }, scenes: { Play } });Good to know
- the rule is derived from the frame's own numbers; the display period comes from the host's presentation cap or the game's declared target, never a constant
- no verdict without a measured GPU reading and a pass census — an absent measurement is not evidence
- the record carries objects considered, draws per pass, triangles per draw and shadow-exempt casters, so the reader is not dependent on the ranking
describeSceneWarning
Warn the agent that built the scene before a human plays it: a frame whose GPU is idle while its JS render phase is longer than the display's own period is a scene-shape problem, and the engine already has the shape. On by default, printed at most once per reported window as TN_SCENE_WARNING, and silent on a scene that is honestly GPU-bound.
function describeSceneWarning(warning: ISceneWarning): stringUse it to
- find out whether a slow frame is the scene's shape or the device
- tell an authoring agent what to reduce before it promises a merge
Example
defineGame({ display: { maxFps: 60 }, scenes: { Play } });Good to know
- the rule is derived from the frame's own numbers; the display period comes from the host's presentation cap or the game's declared target, never a constant
- no verdict without a measured GPU reading and a pass census — an absent measurement is not evidence
- the record carries objects considered, draws per pass, triangles per draw and shadow-exempt casters, so the reader is not dependent on the ranking
directionalTransmittance
Approximate direct transmittance for a ray leaving the game surface.
function directionalTransmittance( parameters: IAtmosphereParameters | IResolvedAtmosphereParameters, direction: Vector3, ): Vector3Use it to
- colour a game-owned sun from atmosphere extinction
Example
const transmittance = directionalTransmittance(parameters, sunDirection);Good to know
- pass a non-zero direction; coefficients and radii come from the game
directionFromSolarPosition
Convert solar elevation and azimuth degrees into a normalized Three.js direction.
function directionFromSolarPosition(elevation: number, azimuth: number): Vector3Use it to
- aim a template's sun from solarPosition output
Example
const direction = directionFromSolarPosition(sun.elevation, sun.azimuth);Good to know
- elevation and azimuth must be finite degrees
displayPeriodMs
Warn the agent that built the scene before a human plays it: a frame whose GPU is idle while its JS render phase is longer than the display's own period is a scene-shape problem, and the engine already has the shape. On by default, printed at most once per reported window as TN_SCENE_WARNING, and silent on a scene that is honestly GPU-bound.
function displayPeriodMs( declaredTargetFps: number | undefined, ):Use it to
- find out whether a slow frame is the scene's shape or the device
- tell an authoring agent what to reduce before it promises a merge
Example
defineGame({ display: { maxFps: 60 }, scenes: { Play } });Good to know
- the rule is derived from the frame's own numbers; the display period comes from the host's presentation cap or the game's declared target, never a constant
- no verdict without a measured GPU reading and a pass census — an absent measurement is not evidence
- the record carries objects considered, draws per pass, triangles per draw and shadow-exempt casters, so the reader is not dependent on the ranking
endSpan
Attribute the render phase to 100% with a nested span tree, off unless TN_FRAME_SPANS asks. Every non-leaf reports its own time minus the spans inside it, so an unmeasured part shows up as a residual instead of being filed under "other"; a child that outlives its parent reports a negative residual rather than a clamped zero.
function endSpan(id: SpanId): voidUse it to
- find out what inside the render phase is actually costing the frame
- tell a shadow pass's traversal from the main pass's, with the residual computed
- price an optimisation against a measured part of the phase rather than the whole of it
Example
if (spansRequested()) setSpanRecorder(new SpanRecorder());Good to know
- off by default and installed by
TN_FRAME_SPANS=1; unset, every call site is one guarded return - the tree is closed against the frame budget's own render phase, so
TN_FRAME_SPANSandTN_FRAME_BUDGETdescribe the same frames - measurement only: no span changes what is drawn, in what order, or with which renderer
ensureVelocityOutput
Provision screen-space motion data for temporal nodes and keep per-instance history at the frame boundary.
function ensureVelocityOutput(pass: IVelocityRenderPass): MRTNodeUse it to
- keep a skinned character or instanced crowd stable in a temporal stage
- add the velocity output to a Three.js scene pass
- add a velocity target before a temporal stage consumes a scene pass
Example
const scenePass = pass(scene, camera);
ensureVelocityOutput(scenePass);
renderer.setOutputNode(scenePass);
const tracker = new VelocityTracker();
tracker.update(scene);
renderer.render(scene, camera);
tracker.commit(scene);Good to know
- call
VelocityTracker.update()before the render andcommit()after it - a pass is only given a velocity target when a temporal stage consumes it
- call only when a temporal stage is active
exposeDebug
Publish one game object under __THREENATIVE__.debug for a capture script or the console.
function exposeDebug(name: string, value: unknown): voidUse it to
- expose a game object to a capture script or the console in dev builds
Example
import { exposeDebug } from "@threenative/core";
exposeDebug("player", player);
// then from the console: __THREENATIVE__.debug.playerGood to know
- development builds only; a production build publishes nothing
FlightModel
Fly a fixed-wing aircraft with a real force balance instead of a steered velocity.
class FlightModel<TState extends IFlightState = IFlightState>Use it to
- fly an airplane with lift, drag, stall and control authority
- launch an aircraft off a moving carrier deck
- apply component damage or a loadout to an aircraft's performance
Example
const model = new FlightModel({ airframe: sbd, state: aircraft, wind: seaWind });
model.step(1 / 60, { turn: -1, pitch: 0.4 });Good to know
- every mass, area, power and inertia value comes from the game's airframe
- damage, stores and configuration arrive as the game's own modifier sample
FluidField2D
Simulate a deterministic 2D velocity-and-dye field on the GPU while exposing its data to game-owned rendering.
class FluidField2D extends GroupUse it to
- simulate smoke, fire, fog, wind, or fluid response on a grid
- inject a touch, pointer, or gameplay impulse into a fluid field
- sample fluid dye or velocity in a game-owned render node
- simulate ocean currents and wind affecting a sailing ship
- simulate ocean fluid dynamics and currents that affect a ship
Example
const field = new FluidField2D({ resolution: 256, pressureIterations: 20 });
ctx.add(field);
field.splat({ x: 0.5, y: 0.5 }, { x: 0.2, y: 0 }, 1);Good to know
- add the field through
ctx.addso renderer attachment, fixed-step dispatch, and release are automatic dyeandvelocityare numeric samplers; appearance stays in the game'ssrc/render/code- the conformance sample measures mean absolute velocity divergence at 0.001732 after four 32² steps with pressureIterations 2, below the 0.0025 threshold
Options
- pressureIterations, viscosity, vorticity, and splatRadius tune the solver without changing its pass order
FluidParticles3D
Simulate liquid as GPU particles (pour, splash, dam break, waterfall) and expose positions, a density volume and a surface height while the game owns every look.
class FluidParticles3D extends GroupUse it to
- pour, splash, or dam-break water that fills a container and flows around obstacles
- simulate water or another liquid as particles in a fluid simulation
- drop a ball or box into liquid and let it displace and float on the water
- emit a stream or waterfall of particle fluid and drain it somewhere else
- sample particle-fluid density or surface height in a game-owned render node
Example
const water = new FluidParticles3D({ capacity: 6000 });
ctx.add(water);
water.fill([-2.8, 0.1, -1.5], [-0.6, 3, 1.5]);Good to know
- add the fluid through
ctx.addso renderer attachment, fixed-step dispatch, and release are automatic - a renderer without WebGPU compute throws a named error at attach; it never draws nothing
emitrecycles the oldest slot oncecapacityslots have been used;fillstops at capacitysampleandstatsread a throttled GPU copy and reportstaleFrames; they are never live
Options
- iterations, viscosity, cohesion, vorticity, gravity and maxSpeed tune the solver without changing its pass order
formatSceneWarning
Warn the agent that built the scene before a human plays it: a frame whose GPU is idle while its JS render phase is longer than the display's own period is a scene-shape problem, and the engine already has the shape. On by default, printed at most once per reported window as TN_SCENE_WARNING, and silent on a scene that is honestly GPU-bound.
function formatSceneWarning(warning: ISceneWarning): stringUse it to
- find out whether a slow frame is the scene's shape or the device
- tell an authoring agent what to reduce before it promises a merge
Example
defineGame({ display: { maxFps: 60 }, scenes: { Play } });Good to know
- the rule is derived from the frame's own numbers; the display period comes from the host's presentation cap or the game's declared target, never a constant
- no verdict without a measured GPU reading and a pass census — an absent measurement is not evidence
- the record carries objects considered, draws per pass, triangles per draw and shadow-exempt casters, so the reader is not dependent on the ranking
formatSpansWindow
Attribute the render phase to 100% with a nested span tree, off unless TN_FRAME_SPANS asks. Every non-leaf reports its own time minus the spans inside it, so an unmeasured part shows up as a residual instead of being filed under "other"; a child that outlives its parent reports a negative residual rather than a clamped zero.
function formatSpansWindow(window: ISpanWindow): stringUse it to
- find out what inside the render phase is actually costing the frame
- tell a shadow pass's traversal from the main pass's, with the residual computed
- price an optimisation against a measured part of the phase rather than the whole of it
Example
if (spansRequested()) setSpanRecorder(new SpanRecorder());Good to know
- off by default and installed by
TN_FRAME_SPANS=1; unset, every call site is one guarded return - the tree is closed against the frame budget's own render phase, so
TN_FRAME_SPANSandTN_FRAME_BUDGETdescribe the same frames - measurement only: no span changes what is drawn, in what order, or with which renderer
formatValidationReport
Prove that nothing a cache skipped changed the picture: TN_RENDERLIST_VALIDATE=1 recomputes every world matrix the long way, every frame, and throws on the first element that disagrees with what the frame is about to draw.
function formatValidationReport(report: IValidationReport): stringUse it to
- prove a static freeze did not leave a stale transform on screen
- gate a scene in CI against silent transform divergence
Example
if (renderListValidationRequested()) console.log(formatValidationReport(report));Good to know
- off by default and expensive by construction — it does the work it is checking, twice
- it throws on the first divergence rather than logging; a validation mode that continues is one nobody reads
- it proves the frames it ran on and nothing else
FrameBudget
Read where the frame's milliseconds went, per presented frame, on any platform; each TN_FRAME_BUDGET window also carries the GPU time per resolved frame and the draw calls and triangles each render pass submitted.
class FrameBudgetUse it to
- show an on-screen frame time meter with p50, p95 and p99 percentiles
- find out why a game runs slowly on a phone
- attribute a frame to present wait, simulation, three.js render, or overlay
- tell whether the GPU is the frame's constraint from a per-frame series, not one lagged timestamp
- split a frame's draw calls and triangles per render pass (main, shadow, reflection)
- tell a shadow or reflection pass's cost from the main colour pass
Example
defineGame({ frameBudget: { reportEvery: 120 }, scenes: { Play } });Good to know
- on by default and printed as TN_FRAME_BUDGET; defineGame({ frameBudget: false }) silences the marker, not the measurement
- per-pass numbers are attributed to the innermost active render call, so nested shadow and reflection passes do not read as main
- GPU is a mean/p50/p95/max series over resolved frames (
gpu) withgpuStalecounting frames that had no fresh reading; absent means no timestamps, never zero
FrameCounters
Count the frame's host-boundary crossings and the bytes it writes into GPU buffers, off unless TN_FRAME_SPANS asks for them. On the frame budget's own window as counters, so a crossing count and a millisecond split describe the same frames.
class FrameCountersUse it to
- decide whether a CPU-bound frame is paying for the V8-to-host boundary
- measure how many bytes a frame writes into GPU buffers, and how many commands it issues
Example
const counters = FrameCounters.install(counterDeviceOf(renderer.raw));Good to know
- counts command-encoder and queue methods only;
mapAsyncand the presentation path are named, not folded in gpuBytesisqueue.writeBufferexactly, so it reconciles against a driver; texture uploads are not includedjsAllocBytesneedsperformance.memoryand stays absent where the platform lacks it
gearClearance
Fly a fixed-wing aircraft with a real force balance instead of a steered velocity.
function gearClearance(state: IFlightState): numberUse it to
- fly an airplane with lift, drag, stall and control authority
- launch an aircraft off a moving carrier deck
- apply component damage or a loadout to an aircraft's performance
Example
const model = new FlightModel({ airframe: sbd, state: aircraft, wind: seaWind });
model.step(1 / 60, { turn: -1, pitch: 0.4 });Good to know
- every mass, area, power and inertia value comes from the game's airframe
- damage, stores and configuration arrive as the game's own modifier sample
getPlatform
Read the host platform without reaching for browser globals.
function getPlatform(): Readonly<IPlatformInfo>Use it to
- branch a portable game on web or native
- choose touch controls for a mobile device
- can I raytrace on native
Example
if (isMobile()) showTouchControls();Good to know
- only use the returned platform facts; native globals are host-owned
- ray tracing is unavailable on native until buffer-to-texture copy-out interop exists;
mystralRT.traceRaysrefuses instead of resolving success without a readable result
GPUParticles3D
Dispatch a game-owned particle surface and process function through a pooled system.
class GPUParticles3D extends Sprite implements IComputeDrivenUse it to
- emit sparks, smoke, or other transient effects
- update many small visual particles
- trail dust, exhaust, or spray behind a moving object
- emit cannon smoke and muzzle flash particles
- fire a cannonball projectile with cannon smoke particles
- splash spray droplets with lifetime and gravity
- spawn water droplets, mist, or foam bubbles above a fluid surface
- falling snowflakes, rain or ash around the player that thicken into a windy storm or blizzard
- kick up a spray of powder snow or dust where a foot or a ball lands
Example
const particles = new GPUParticles3D(particleOptions);Good to know
- geometry, color, and timing remain supplied by the game
GPUReadback
Copy a GPU buffer back to the CPU on a throttle, and report how old each sample is.
class GPUReadbackUse it to
- read a GPU simulation on the CPU without stalling the frame
- float a body on a wave field whose height only exists on the GPU
- count GPU-side survivors for a diagnostic without blocking drawing
- keep a ship floating on simulated ocean waves
Example
const heights = new GPUReadback({ attribute: field.value, everyFrames: 4 });Good to know
- the copy is asynchronous, so every sample carries staleFrames and is never this frame
- WebGPU only; the seam throws on a WebGL2 renderer rather than returning nothing
- one copy is in flight at a time and requests made during one are dropped, not queued
GPUSceneBVH
Pack a selected static scene into TSL storage nodes for an upstream BVH ray query.
class GPUSceneBVH extends Group implements IComputeDrivenUse it to
- trace thousands of scene rays inside a TSL kernel
- build a contact-occlusion or visibility query over loaded meshes
Example
const bvh = ctx.add(new GPUSceneBVH(ctx.scene, { include: (object) => object.userData.traceable === true }));Good to know
- call rebuild() after a scene transform or geometry change; the snapshot is static by default
- rebuild() is an explicit CPU SAH build proportional to selected triangles; process() is a no-op, and the game pays upstream traversal per shader ray
GroundSnap
Keep a rendered model's feet on a surface while preserving an auditable override.
class GroundSnapUse it to
- keep a character's feet on the floor
- correct visual grounding after an animation update
Example
import { GroundSnap } from "@threenative/core";
const snap = new GroundSnap(character, { enabled: true });Good to know
- import from
@threenative/core; this moves the rendered model, not its physics collider
Options
- enabled controls whether correction is applied while clearance is still measured
InputMap
The map a game reads input through: named actions, 2D vectors and scalar axes resolved from keyboard, gamepad, mouse, wheel, pinch and touch. defineGame({ input }) builds one and hands it to the running game as ctx.input, so the usual route is a binding in the config and ctx.input.axis("move") in the update. Construct one directly to drive a menu, a replay or a test outside a running game.
class InputMapUse it to
- map WASD keys to a movement axis instead of reading the held key set in the update loop
- read a jump, a fire or a reload as one named action bound to key, gamepad button and mouse button together
- read mouse look, wheel zoom or a two-finger pinch as an axis the frame loop already ticks
Example
const game = defineGame({ input: { move: { up: ["KeyW"], down: ["KeyS"], left: ["KeyA"], right: ["KeyD"] } }, scenes: { Play } });
// inside the scene, per frame: ctx.input.axis("move") is 0 at rest and 1 at full tiltGood to know
buttonsis the gamepad andmouseButtonsthe mouse;up/down/left/rightare the directions ofvector(name), not the keys that press it- scroll, pinch and pointer-relative sources are declared on the binding and read through
axis(name); a game adds no window listener of its own
installSpanProbes
Attach the span probes to a renderer: three's render, _projectObject, the render list's sort, the per-draw submission, and the scene-graph walk. Installed for you when TN_FRAME_SPANS asks; exported so a harness can wrap a renderer it owns.
function installSpanProbes(target: ISpanProbeTarget, root: Object3D): () => voidUse it to
- measure which part of three's render path costs the frame
- attach the span tree to a renderer a test or a tool constructed itself
Example
const uninstall = installSpanProbes(renderer.raw, scene);Good to know
- returns an uninstall that restores every wrapper, asserted by test
- a renderer whose internals have moved loses that span rather than throwing, and it is absent from the report rather than zero
- only the outermost
_projectObjectopens a span, because three's recurses per child
InstancedBatch
Collapse many copies of one game-authored shape into a single draw, without counting them first. new InstancedMesh(geometry, material, count) needs the count before anything is placed, so a procedural builder ends up walking its layout twice or over-allocating. Place as you go and build() once; the shape, the surface and every transform stay the game's, and the built mesh is returned so instances can still be animated by the index place and span hand back.
class InstancedBatchUse it to
- draw hundreds of repeated props without hundreds of draw calls
- draw thousands of identical instanced blocks or obstacles in one mesh
- automatically select instanced prop detail from projected screen error using baked AutoLOD chains
- place repeated props when the count is not known until the layout has been walked
- build a chain, railing, cable, or tie rod out of point-to-point segments
Example
const curbs = new InstancedBatch({ geometry: new BoxGeometry(1, 1, 1), material });
curbs.place({ position: [x, 0.08, z], rotation: [0, angle, 0], scale: [length, 0.18, 0.42] });
curbs.build({ castShadow: true, name: "curbs", parent: ctx.scene });Good to know
- geometry and material are required and come from the game; the batch chooses neither
- span stretches along +Y, so its geometry must be unit-height and centred on the origin
- placing after build() throws, and build() returns undefined when nothing was placed
- baked AutoLOD chains survive geometry clone/transform preparation and partition instances automatically into spatially bounded draws in the engine frame loop at 4 px projected error; public instance slots remain stable
- unavailable authored levels report TN_INSTANCED_LOD_FAILED once naming the batch; a million-triangle batch with no chain reports TN_INSTANCED_LOD_UNAVAILABLE
Options
- autoLod: false leaves selection to the game; autoLod.maxPixelError and hysteresis override the measured-camera budget; lods supplies authored distance/geometry levels and wins over the baked chain
- castShadow and receiveShadow pass through to the built mesh and default to Three.js's own false
Also found by
- landmarks points of interest
- obstacles collectibles increasing pace
invalidateStatic
Stop recomposing the transforms of a subtree nobody moves. markStatic(root) composes the subtree once and freezes it; the engine re-arms a root whose own transform the game changes, and invalidateStatic(object) announces a write deeper inside one.
function invalidateStatic(object: Object3D): number | undefinedUse it to
- cut the per-frame matrix work of terrain, buildings, props and other scenery that never moves
- keep a frozen subtree correct when the game does move it after all
Example
invalidateStatic(drawbridge);Good to know
- staticness is authored, never guessed; no heuristic watches gameplay and decides for you
- it deletes the local and world matrix composes, not the walk itself — three 0.185 recurses into every child regardless
- a write deeper inside a frozen subtree must call
invalidateStatic;TN_RENDERLIST_VALIDATE=1is what proves it happened
isMobile
Read the host platform without reaching for browser globals.
function isMobile(): booleanUse it to
- branch a portable game on web or native
- choose touch controls for a mobile device
- can I raytrace on native
Example
if (isMobile()) showTouchControls();Good to know
- only use the returned platform facts; native globals are host-owned
- ray tracing is unavailable on native until buffer-to-texture copy-out interop exists;
mystralRT.traceRaysrefuses instead of resolving success without a readable result
isNative
Read the host platform without reaching for browser globals.
function isNative(): booleanUse it to
- branch a portable game on web or native
- choose touch controls for a mobile device
- can I raytrace on native
Example
if (isMobile()) showTouchControls();Good to know
- only use the returned platform facts; native globals are host-owned
- ray tracing is unavailable on native until buffer-to-texture copy-out interop exists;
mystralRT.traceRaysrefuses instead of resolving success without a readable result
isStatic
Stop recomposing the transforms of a subtree nobody moves. markStatic(root) composes the subtree once and freezes it; the engine re-arms a root whose own transform the game changes, and invalidateStatic(object) announces a write deeper inside one.
function isStatic(root: Object3D): booleanUse it to
- cut the per-frame matrix work of terrain, buildings, props and other scenery that never moves
- keep a frozen subtree correct when the game does move it after all
Example
invalidateStatic(drawbridge);Good to know
- staticness is authored, never guessed; no heuristic watches gameplay and decides for you
- it deletes the local and world matrix composes, not the walk itself — three 0.185 recurses into every child regardless
- a write deeper inside a frozen subtree must call
invalidateStatic;TN_RENDERLIST_VALIDATE=1is what proves it happened
isTouchscreenAvailable
Read the host platform without reaching for browser globals.
function isTouchscreenAvailable(): booleanUse it to
- branch a portable game on web or native
- choose touch controls for a mobile device
- can I raytrace on native
Example
if (isMobile()) showTouchControls();Good to know
- only use the returned platform facts; native globals are host-owned
- ray tracing is unavailable on native until buffer-to-texture copy-out interop exists;
mystralRT.traceRaysrefuses instead of resolving success without a readable result
isWeb
Read the host platform without reaching for browser globals.
function isWeb(): booleanUse it to
- branch a portable game on web or native
- choose touch controls for a mobile device
- can I raytrace on native
Example
if (isMobile()) showTouchControls();Good to know
- only use the returned platform facts; native globals are host-owned
- ray tracing is unavailable on native until buffer-to-texture copy-out interop exists;
mystralRT.traceRaysrefuses instead of resolving success without a readable result
loadAll
Load a list with bounded concurrency, returning results in the input's order.
async function loadAll<TIn, TOut>( items: readonly TIn[], load: (item: TIn, index: number) => Promise<TOut>, options: ILoadAllOptions =Use it to
- load many models or textures in parallel instead of one at a time
- keep a loading screen moving while a list of assets downloads
Example
const species = await loadAll(names, (name) => ctx.assets.model(`flora/${name}.glb`));Good to know
- results are written to each item's own index and never appended, so a positional lookup finds the same asset on every load
- the first rejection rejects the call and no lane starts a load it had not begun
Options
- concurrency defaults to 6;
marker: falsesilences the TN_LOAD_ALL line, not onProgress
lodPixelScale
The screen pixels one world unit covers at depth for this camera and viewport. Perspective divides the projected scale by the depth; orthographic has no depth term and uses the frustum height instead. This is the number a level of detail is chosen against: multiply a level's world-space error by it and you have the on-screen error a player can see, which is the comparison updateModelLods makes from the baked chain.
function lodPixelScale(camera: Camera, viewportHeight: number, depth: number): numberUse it to
- pick a level of detail from an object's projected size in pixels on screen
- know how many screen pixels a world-space error covers at a given distance
Example
const pixels = lodPixelScale(camera, canvas.clientHeight, mesh.position.distanceTo(camera.position));Good to know
- a non-positive viewport height, a non-positive frustum height or an unprojectable camera throws
- a non-positive depth has no projected scale and returns Infinity
markStatic
Freeze a subtree nobody moves: markStatic(root) composes its transforms once and stops the per-frame recompose. It is the call a game makes on scenery, terrain, buildings and props. The engine re-arms a root whose own transform the game changes; a write deeper inside a frozen subtree is announced with invalidateStatic(object).
function markStatic(root: Object3D): numberUse it to
- freeze a static mesh or subtree so its matrices are not recomputed every frame
- stop the engine recomposing the transforms of props, terrain and buildings each frame
Example
markStatic(island); invalidateStatic(drawbridge);Good to know
- staticness is authored, never guessed; no heuristic watches gameplay and decides for you
- a write deeper inside a frozen subtree must call
invalidateStatic;TN_RENDERLIST_VALIDATE=1is what proves it happened
MatrixWorldPass
Walk the scene graph's world matrices each frame without recursing into a hidden subtree. On by default as renderer.matrixWorld: "visible". three's updateMatrixWorld recurses into every child whatever its visible flag, so a hidden LOD body, a merged stand-in and a parked model cost a world-matrix multiply each while nothing under them can draw. This pass mirrors three exactly for every visible node and defers a hidden node's subtree until the frame it shows again. A class that overrides updateMatrixWorld (SkinnedMesh, Camera) runs its own, and a hidden node that holds bones is walked, so no skeleton or view matrix goes stale.
class MatrixWorldPassUse it to
- update the world matrices of the visible objects each frame instead of the whole scene
- the per-frame world matrix walk is hot in a profile
- stop multiplying matrices for hidden models, LOD levels and merged stand-ins
- a game needs every node walked, exactly as three's own updateMatrixWorld does
Example
import { MatrixWorldPass } from "@threenative/core";
const pass = new MatrixWorldPass(); // renderer.matrixWorld defaults to "visible"Good to know
- a game that reads a hidden object's matrixWorld directly must use getWorldPosition or updateWorldMatrix(true, false) first
renderer.matrixWorld: "all"visits every node;TN_PROJECTIONreports the visited count either way
Options
- renderer.matrixWorld: "all" runs three's full walk instead of the visible-only default
measureThreePose
Measure a Three.js pose for grounded or attachment-aware checks.
function measureThreePose( object: Object3D, options: IMeasureThreePoseOptions =Use it to
- inspect a skinned model's posed bounds
- verify a character's visual pose
Example
const measurement = measureThreePose(model);Good to know
- precise per-vertex measurement is opt-in and not for frame loops
mergeByMaterial
Bake a hierarchy's static meshes into one mesh per material, with their transforms baked in. already made; nothing here decides appearance tree: add them to root and remove the sources yourself, or both draw vertices are not its own to bake texture mapping; a missing normal is recomputed
function mergeByMaterial(root: Object3D, options: IMergeByMaterialOptions): Mesh[]Use it to
- collapse a building or ship of dozens of boxes into one draw call per material
- consolidate the static parts of a group before adding it to the scene
Example
const [hull, deck] = mergeByMaterial(ship, { label: "ship" });
// a piece that must keep moving at run time:
const [steady] = mergeByMaterial(ship, { label: "ship", skip: (mesh) => mesh.name === "radar" });Good to know
- the material is the game's own instance and the split follows the materials the game
- the meshes come back in root's local space and unparented, with the originals still in the
- a skinned or instanced mesh, and a mesh with several materials, is left out — its
- a group where only some meshes carry uv throws naming the label rather than losing the
Options
- skip leaves one mesh out of its group and out of the result
mergeParts
Merge pieces a game authored out of primitives into one buffer, keeping each piece's own look. InstancedBatch collapses many copies of one shape; this collapses many different shapes that never move relative to each other — a building, a ship, a character built from boxes, or the static parts of an imported glTF model. Two things go wrong every time and neither is about how any of it looks. mergeGeometries hands back null on mismatched inputs instead of throwing, and the usual mismatch is invisible: one ExtrudeGeometry is non-indexed while every other primitive is indexed, so the merge fails at the first piece and the scene never loads. And a merged buffer draws with one surface, so per-piece colour is gone unless every piece carries a flat color attribute written before the merge. Both are mechanical. By default every part is stripped to position and the normals are recomputed from the merged buffer; pass preserve to keep authored normals and texture UVs while baking each part's object transform. Geometry, placement, colour and the surface it draws with all stay the game's.
function mergeParts( parts: Iterable<IMergePart>, options: IMergePartsOptions, ): BufferGeometryUse it to
- bake a building, ship or character authored out of primitives into one draw call
- merge multiple static Three.js meshes into one mesh per material
- consolidate the static parts of an imported glTF model into one buffer
- preserve texture UV coordinates and authored normals while baking object transforms
- merge many small geometries and keep each piece's own colour
- stop mergeGeometries from silently returning null on an extruded shape
Example
const wall = new Mesh(mergeParts(pieces, { label: "gatehouse" }), stone);
// pieces are meshes, or { geometry, matrix, color } when the colour is per piece:
const banner = mergeParts([{ color: 0x8b2f1a, geometry: cloth, matrix: placement }], { label: "banner" });
// keep a model's texture UVs and authored normals while baking its transforms:
const hull = mergeParts(hullParts, { label: "hull", preserve: ["uv", "normal"] });Good to know
- every part is de-indexed; without preserve it is stripped to position and normals are recomputed from the merged buffer
- preserve keeps the listed channels, transforming position and normal by the part's placement matrix while UV values are retained unchanged
- a part that does not carry a listed preserve channel throws naming the label, the part and the channel
- either every part names a color or none does, and a mix throws
- an empty part list throws, and a merge three.js refuses throws naming the label
Options
- color is per part and optional; without it no colour attribute is written and the surface alone decides
- preserve is optional and empty by default: position-only merge with recomputed normals, exactly as before
normaliseToMetres
Scale an asset to a real-world measurement and return the applied factor.
function normaliseToMetres(object: Object3D, options: INormaliseToMetresOptions): numberUse it to
- make a character exactly 1.8 metres tall
- normalize a prop or weapon to a known longest axis
Example
normaliseToMetres(character, { metres: 1.8, axis: "height" });Good to know
- skinned height uses a crown bone; game-specific asset expectations stay in render code
Replaces new Box3().setFromObject(.
onLaunchFailure
Called for every launch failure the engine notices, with the message to show the player.
function onLaunchFailure(listener: (failure: ILaunchFailure) => void): () => voidUse it to
- show the player why the game stopped loading instead of leaving the loading screen up
- report a stalled launch or a lost GPU device in the game's own UI
Example
const off = onLaunchFailure((failure) => shell.loading({ failure: failure.message }));parseReplayRecording
Validate and parse a replay recording file.
function parseReplayRecording(value: unknown): IReplayRecordingUse it to
- validate a recording before replaying it in another host
Example
const recording = parseReplayRecording(rawRecording);Good to know
- recordings are version 1; the parser fails closed with TN_REPLAY_* codes
PathFollow3D
Move an object along a Three.js curve with Godot-style path following.
class PathFollow3DUse it to
- move an enemy or prop along a patrol path
- sample a racing line from a curve
Example
const follower = new PathFollow3D({ points: patrolPoints, loop: true, speed: 3 });PipelineCensus
Read the renderer's bounded, versioned pipeline capture in a diagnostic or playtest tool.
class PipelineCensusUse it to
- inspect shader and pipeline creation work during a real launch
- correlate pipeline creation with material, object, pass, and shader identities
Example
const capture = game.runtime.pipelineCensus?.();
// The game normally reaches this through `runtime.pipelineCensus`; direct construction exists
// for renderer adapters and contract tests, not for gameplay.Good to know
- the capture is bounded and incomplete when the backend cannot expose an observation
PointerEvents3D
Dispatch portable pointer events from the game surface to registered Three.js objects.
class PointerEvents3D implements IPointerEvents3DUse it to
- let the player click on a thing in the world
- show a 3D object while a pointer hovers over it
- handle touch and mouse taps on a loaded model without naming its child meshes
- drag a crate or prop with the mouse or a finger
Example
ctx.pointer.on(tile, "tapped", (event) => place(event.point));Good to know
- listeners are side-table registrations; Three.js prototypes are never patched
- one raycast serves each active pointer and no raycast runs when nothing is registered
posedBounds
Measure a Three.js pose for grounded or attachment-aware checks.
function posedBounds(root: Object3D, meshes?: readonly Object3D[]): IThreePoseBoundsUse it to
- inspect a skinned model's posed bounds
- verify a character's visual pose
Example
const measurement = measureThreePose(model);Good to know
- precise per-vertex measurement is opt-in and not for frame loops
prewarm
Keep transient render surfaces in the renderer's pipeline cache before first use.
function prewarm(object: Object3D | readonly Object3D[]): voidUse it to
- prewarm a projectile, tracer, particle, or other transient effect
- avoid a long first-use frame for a newly visible effect
Example
prewarm(tracerPool);Good to know
- keep the surface visible with zero opacity; do not hide it with
visible = false
Replaces .visible = false.
ProbeVolume
Bake static diffuse irradiance that reaches surfaces from outside the camera view.
class ProbeVolume extends Object3D implements IComputeDrivenUse it to
- light bouncing from a room I cannot see
- light a wall with an off-screen emitter
Example
const probes = new ProbeVolume({ bounds, density: 0.5 }); ctx.add(probes); void probes.requestBake(scene);Good to know
- request a bake after static geometry and lights are authored; this is static-lighting-first, not fully dynamic relighting
- add the volume with ctx.add() so its incremental work is measured in the render phase
- call sample() or sampleNode() from a game-owned material before screen-space GI; the volume owns no light, material, or colour
Options
- density, bounds, bakeBudgetMs, and bounces are game-owned choices
Also found by
- readable world lighting
readProbeVolumeObservation
Read the most recent observation from a probe volume.
function readProbeVolumeObservation(value: unknown): IProbeVolumeObservation | undefinedUse it to
- inspect the latest probe bake observation
Example
const observation = readProbeVolumeObservation(probes);Good to know
- the returned observation is a measurement; it does not own lighting or materials
readRenderChainObservation
Compose game-provided render nodes in a measured, fail-closed chain. Authored stages use an opaque id and anchor before or after a built-in or another supplied stage; the engine does not need to know the effect's visual vocabulary.
function readRenderChainObservation( renderer: unknown, ): IRenderChainMarker["applied"] | undefinedUse it to
- compose screen-space effects in a canonical order
- insert a game-authored post stage into the measured render chain
- report which render tier and velocity route actually ran
Example
const chain = new RenderChain(renderer, { input: colour, stages, request: { stages: ["bloom"], tier: "auto" } });Good to know
- stage factories own colour, strength, and all other appearance choices
- authored stages declare exactly one before or after anchor
readRenderChainReport
Compose game-provided render nodes in a measured, fail-closed chain. Authored stages use an opaque id and anchor before or after a built-in or another supplied stage; the engine does not need to know the effect's visual vocabulary.
function readRenderChainReport(renderer: unknown): IRenderChainMarker | undefinedUse it to
- compose screen-space effects in a canonical order
- insert a game-authored post stage into the measured render chain
- report which render tier and velocity route actually ran
- expose the render tier and dropped-stage reasons to a playtest
- inspect whether a temporal pass received velocity
Example
const chain = new RenderChain(renderer, { input: colour, stages, request: { stages: ["bloom"], tier: "auto" } });Good to know
- stage factories own colour, strength, and all other appearance choices
- authored stages declare exactly one before or after anchor
- an absent value means no chain was installed and must fail a chain assertion
readVelocityPreviousBoneMatrices
Provision screen-space motion data for temporal nodes and keep per-instance history at the frame boundary.
function readVelocityPreviousBoneMatrices(object: Object3D): Float32Array | undefinedUse it to
- keep a skinned character or instanced crowd stable in a temporal stage
- add the velocity output to a Three.js scene pass
- inspect the previous skinned pose at a renderer adapter boundary
Example
const scenePass = pass(scene, camera);
ensureVelocityOutput(scenePass);
renderer.setOutputNode(scenePass);
const tracker = new VelocityTracker();
tracker.update(scene);
renderer.render(scene, camera);
tracker.commit(scene);Good to know
- call
VelocityTracker.update()before the render andcommit()after it - a pass is only given a velocity target when a temporal stage consumes it
readVelocityPreviousMatrices
Provision screen-space motion data for temporal nodes and keep per-instance history at the frame boundary.
function readVelocityPreviousMatrices(object: Object3D): Float32Array | undefinedUse it to
- keep a skinned character or instanced crowd stable in a temporal stage
- add the velocity output to a Three.js scene pass
- inspect the previous instance frame at a renderer adapter boundary
Example
const scenePass = pass(scene, camera);
ensureVelocityOutput(scenePass);
renderer.setOutputNode(scenePass);
const tracker = new VelocityTracker();
tracker.update(scene);
renderer.render(scene, camera);
tracker.commit(scene);Good to know
- call
VelocityTracker.update()before the render andcommit()after it - a pass is only given a velocity target when a temporal stage consumes it
readVelocityPreviousWorldMatrix
Provision screen-space motion data for temporal nodes and keep per-instance history at the frame boundary.
function readVelocityPreviousWorldMatrix(object: Object3D): Matrix4 | undefinedUse it to
- keep a skinned character or instanced crowd stable in a temporal stage
- add the velocity output to a Three.js scene pass
- inspect the previous rigid transform at a renderer adapter boundary
Example
const scenePass = pass(scene, camera);
ensureVelocityOutput(scenePass);
renderer.setOutputNode(scenePass);
const tracker = new VelocityTracker();
tracker.update(scene);
renderer.render(scene, camera);
tracker.commit(scene);Good to know
- call
VelocityTracker.update()before the render andcommit()after it - a pass is only given a velocity target when a temporal stage consumes it
readVirtualShadowMarker
Parse a TN_VIRTUAL_SHADOW console line back into its complete stats, or undefined.
function readVirtualShadowMarker(line: string): IVirtualShadowStats | undefinedUse it to
- inspect virtual shadow cache and mover counters from a renderer log
Example
const stats = readVirtualShadowMarker(line);
if (stats !== undefined) console.log(stats.reuseRatio);Good to know
- non-marker lines and markers with incomplete or non-numeric stats return
undefined
reconcileMirroredClips
Repair an exported rig whose animation clips are z-mirrored against its own bind pose.
function reconcileMirroredClips(root: Object3D, clips: readonly AnimationClip[]): booleanUse it to
- find out why a skinned character renders deformed
- repair an imported character that walks backwards with its spine folded
- load a rigged GLB through a custom loader and keep the framework's repair
Example
import { reconcileMirroredClips } from "@threenative/core";
if (reconcileMirroredClips(gltf.scene, gltf.animations)) console.info("clips were z-mirrored; repaired");Good to know
- the model loader already applies this automatically; only a game loading GLBs around it needs the call
- detection votes per tracked bone and converts only on an overwhelming signature; a file that does not carry it is left byte-identical
refreshStaticTransforms
Stop recomposing the transforms of a subtree nobody moves. markStatic(root) composes the subtree once and freezes it; the engine re-arms a root whose own transform the game changes, and invalidateStatic(object) announces a write deeper inside one.
function refreshStaticTransforms(): voidUse it to
- cut the per-frame matrix work of terrain, buildings, props and other scenery that never moves
- keep a frozen subtree correct when the game does move it after all
Example
invalidateStatic(drawbridge);Good to know
- staticness is authored, never guessed; no heuristic watches gameplay and decides for you
- it deletes the local and world matrix composes, not the walk itself — three 0.185 recurses into every child regardless
- a write deeper inside a frozen subtree must call
invalidateStatic;TN_RENDERLIST_VALIDATE=1is what proves it happened
RenderChain
Compose game-provided render nodes in a measured, fail-closed chain. Authored stages use an opaque id and anchor before or after a built-in or another supplied stage; the engine does not need to know the effect's visual vocabulary.
class RenderChainUse it to
- compose screen-space effects in a canonical order
- insert a game-authored post stage into the measured render chain
- report which render tier and velocity route actually ran
Example
const chain = new RenderChain(renderer, { input: colour, stages, request: { stages: ["bloom"], tier: "auto" } });Good to know
- stage factories own colour, strength, and all other appearance choices
- authored stages declare exactly one before or after anchor
renderListValidationRequested
Prove that nothing a cache skipped changed the picture: TN_RENDERLIST_VALIDATE=1 recomputes every world matrix the long way, every frame, and throws on the first element that disagrees with what the frame is about to draw.
function renderListValidationRequested(): booleanUse it to
- prove a static freeze did not leave a stale transform on screen
- gate a scene in CI against silent transform divergence
Example
if (renderListValidationRequested()) console.log(formatValidationReport(report));Good to know
- off by default and expensive by construction — it does the work it is checking, twice
- it throws on the first divergence rather than logging; a validation mode that continues is one nobody reads
- it proves the frames it ran on and nothing else
RenderListValidator
Prove that nothing a cache skipped changed the picture: TN_RENDERLIST_VALIDATE=1 recomputes every world matrix the long way, every frame, and throws on the first element that disagrees with what the frame is about to draw.
class RenderListValidatorUse it to
- prove a static freeze did not leave a stale transform on screen
- gate a scene in CI against silent transform divergence
Example
if (renderListValidationRequested()) console.log(formatValidationReport(report));Good to know
- off by default and expensive by construction — it does the work it is checking, twice
- it throws on the first divergence rather than logging; a validation mode that continues is one nobody reads
- it proves the frames it ran on and nothing else
replay
Record or replay deterministic game input and state.
function replay< TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined, >(options: IReplayOptions =Use it to
- reproduce a gameplay bug from recorded input
- run a deterministic replay in a playtest
Example
const driver = createReplayDriver(recording, ctx.renderer.domElement);Good to know
- use a seeded random source and fixed-step simulation for meaningful replays
Also found by
- fixed seed fixed-step simulation
resetAudioCueLedger
Forgets every recorded cue.
function resetAudioCueLedger(): voidUse it to
- clear the recorded audio cue counts between tests so one test cannot read another's plays
Example
resetAudioCueLedger();resetStaticTransforms
Stop recomposing the transforms of a subtree nobody moves. markStatic(root) composes the subtree once and freezes it; the engine re-arms a root whose own transform the game changes, and invalidateStatic(object) announces a write deeper inside one.
function resetStaticTransforms(): voidUse it to
- cut the per-frame matrix work of terrain, buildings, props and other scenery that never moves
- keep a frozen subtree correct when the game does move it after all
Example
invalidateStatic(drawbridge);Good to know
- staticness is authored, never guessed; no heuristic watches gameplay and decides for you
- it deletes the local and world matrix composes, not the walk itself — three 0.185 recurses into every child regardless
- a write deeper inside a frozen subtree must call
invalidateStatic;TN_RENDERLIST_VALIDATE=1is what proves it happened
resolveAtmosphereLutResolutions
Resolve the three LUT dimensions, allowing a game to trade startup cost for resolution.
function resolveAtmosphereLutResolutions( resolutions: Partial<IAtmosphereLutResolutions> | undefined, ): IAtmosphereLutResolutionsUse it to
- choose atmosphere LUT dimensions for a measured startup budget
Example
const resolutions = resolveAtmosphereLutResolutions({ skyView: { width: 128, height: 72 } });Good to know
- every width and height must be a positive integer; the dimensions are not a named fidelity tier
resolveAtmosphereParameters
Validate and clone game-owned atmosphere coefficients.
function resolveAtmosphereParameters( options: IAtmosphereParameters, ): IResolvedAtmosphereParametersUse it to
- validate atmosphere coefficients before a game creates its sky
Example
const parameters = resolveAtmosphereParameters({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });Good to know
- provide all three coefficient vectors and both radii; omitted fields are errors
resolveTargetFps
Read what frame rate a game gets when its config does not say, and why. display.maxFps follows the display — capped at 120 on desktop and web, 60 on mobile — rather than the 60 every template used to ship, and an explicit number still wins with 0 still uncapping. The engine calls this itself; a game calls it when it needs the same number for its own frame-rate-dependent work, and TN_FRAME_BUDGET reports the resolved target and its source on every window.
function resolveTargetFps( config: ITargetFpsConfig | undefined, platform: ITargetFpsPlatform | undefined, measuredRefreshHz?: number, ): ITargetFpsUse it to
- my game does frame-rate-dependent work and must not hardcode 60
Example
resolveTargetFps(config, getPlatform()).targetFps;Good to know
- pass the measured display rate when you have one; without it the answer is the 60 fallback and says so
RippleField
Propagate a disturbance across a patch of water surface and let it fade. step is 1/60s or the CFL stability limit for the given resolution and speed, whichever is smaller
class RippleFieldUse it to
- make a splash or explosion ripple outward across water
- show the sea reacting to a bomb, shell, or torpedo hitting it
- leave a foam trail behind something moving through water
- disturb a water surface the player can see respond
- spread and drift foam on a water surface over time
Example
const ripples = new RippleField({ resolution: 128, size: 400 });
ripples.impulse(hit.x, hit.z, 6, -40, 0.5);
ripples.advance(dt);
const lift = ripples.heightAt(boat.x, boat.z);Good to know
- it draws nothing; the game supplies the mesh, the material and every colour
- add its height to an analytic swell, never in place of one
- the patch is finite and its rim absorbs; call recenter to keep it over the action
- there is no obstacle mask, because a mask is only correct for a body that never moves
Options
- speed, damping, foamHalfLife, current, step and maxSteps tune the solve; the default
Scene
Implement a portable Godot-shaped game scene lifecycle.
abstract class Scene< TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined, >Use it to
- add a playable level or menu scene
- move scene setup and per-frame gameplay out of the entry point
- run scene work once per actual world draw, after the frame's last fixed update and before the projection packs
Example
class Play extends Scene { update(ctx, dt) {} }
ctx.beforeRender(() => packBatches()); // cleared on scene change and stop, like ctx.afterPhysicsGood to know
- scene code must stay portable across web and native
ScenePicker
Raycast the game scene using the framework's picker.
class ScenePickerUse it to
- select an object under the pointer
- interact with the first collider or mesh hit
Example
const picker = new ScenePicker({ camera: ctx.camera, scene: ctx.scene, pointer: () => ctx.input.raw.pointer, viewport: ctx.viewport });Replaces new Raycaster(.
sceneWarning
Warn the agent that built the scene before a human plays it: a frame whose GPU is idle while its JS render phase is longer than the display's own period is a scene-shape problem, and the engine already has the shape. On by default, printed at most once per reported window as TN_SCENE_WARNING, and silent on a scene that is honestly GPU-bound.
function sceneWarning( window: IFrameBudgetWindow, shape: ISceneShape | undefined, declaredTargetFps: number | undefined, ): ISceneWarning | undefinedUse it to
- find out whether a slow frame is the scene's shape or the device
- tell an authoring agent what to reduce before it promises a merge
Example
defineGame({ display: { maxFps: 60 }, scenes: { Play } });Good to know
- the rule is derived from the frame's own numbers; the display period comes from the host's presentation cap or the game's declared target, never a constant
- no verdict without a measured GPU reading and a pass census — an absent measurement is not evidence
- the record carries objects considered, draws per pass, triangles per draw and shadow-exempt casters, so the reader is not dependent on the ranking
Scheduler
Schedule delayed and repeating callbacks or tween numeric properties with game-owned cleanup.
class SchedulerUse it to
- delay an enemy patrol transition
- run a callback every simulation tick
- tween a numeric property with a game-owned curve
- a countdown timer: end the level when its time limit runs out
Example
const door = { y: 0 };
await ctx.tween(door, { y: 2.4 }, 0.5, { ease: (t) => 1 - (1 - t) ** 3 });Good to know
- dispose returned handles when the owning scene exits
- ease receives progress in the range 0 to 1 and its return value is the interpolation factor
Also found by
- tower defense game
- spawn waves
setAttitude
Fly a fixed-wing aircraft with a real force balance instead of a steered velocity.
function setAttitude( state: IFlightState, heading = 0, pitch = 0, roll = 0, ): IFlightQuaternionUse it to
- fly an airplane with lift, drag, stall and control authority
- launch an aircraft off a moving carrier deck
- apply component damage or a loadout to an aircraft's performance
Example
const model = new FlightModel({ airframe: sbd, state: aircraft, wind: seaWind });
model.step(1 / 60, { turn: -1, pitch: 0.4 });Good to know
- every mass, area, power and inertia value comes from the game's airframe
- damage, stores and configuration arrive as the game's own modifier sample
setSpanRecorder
Attribute the render phase to 100% with a nested span tree, off unless TN_FRAME_SPANS asks. Every non-leaf reports its own time minus the spans inside it, so an unmeasured part shows up as a residual instead of being filed under "other"; a child that outlives its parent reports a negative residual rather than a clamped zero.
function setSpanRecorder(next: SpanRecorder | undefined): voidUse it to
- find out what inside the render phase is actually costing the frame
- tell a shadow pass's traversal from the main pass's, with the residual computed
- price an optimisation against a measured part of the phase rather than the whole of it
Example
if (spansRequested()) setSpanRecorder(new SpanRecorder());Good to know
- off by default and installed by
TN_FRAME_SPANS=1; unset, every call site is one guarded return - the tree is closed against the frame budget's own render phase, so
TN_FRAME_SPANSandTN_FRAME_BUDGETdescribe the same frames - measurement only: no span changes what is drawn, in what order, or with which renderer
SkeletalMesh3D
Shared preparation for an imported rigged character. Instances the rig with a skeleton-safe clone, normalises size with skin-aware measurement, validates requested clips against the file and rig at load time, and sets up AnimationPlayer with honest stride-root accounting.
class SkeletalMesh3D extends AnimationPlayerUse it to
- put an animated character in the scene
- my imported character renders deformed
- instance an imported rigged character
- validate animation clips on a character rig at load time
- prepare a skinned character with safe skeleton cloning and stride sync
Example
import { SkeletalMesh3D } from "@threenative/core";
const character = new SkeletalMesh3D({
source: gltf.scene, clips: gltf.animations, requiredClips: ["idle", "walk"],
size: { metres: 1.8, axis: "height" }, strideRoot: body,
});
body.add(character.root); character.play("idle");
function update(dt: number): void { character.update(dt); }Good to know
- use strideRoot to name the body moved by game code when the rig is parented under it
- requiredClips fails closed at load time if any requested clip is missing or binds 0 tracks
Options
- strideSync controls whether locomotion playback rate matches ground covered
- size normalises the instance to real-world metres with skin-aware measurement
skeletonBones
List the names of every bone in a character hierarchy.
function skeletonBones(root: Object3D): readonly string[]Use it to
- inspect the available bones before attaching a game-owned object
- debug a missing skeleton bone name
Example
import { skeletonBones } from "@threenative/core";
const bones = skeletonBones(character);snapRefreshRate
Read what frame rate a game gets when its config does not say, and why. display.maxFps follows the display — capped at 120 on desktop and web, 60 on mobile — rather than the 60 every template used to ship, and an explicit number still wins with 0 still uncapping. The engine calls this itself; a game calls it when it needs the same number for its own frame-rate-dependent work, and TN_FRAME_BUDGET reports the resolved target and its source on every window.
function snapRefreshRate(refreshHz: number): numberUse it to
- my game does frame-rate-dependent work and must not hardcode 60
Example
resolveTargetFps(config, getPlatform()).targetFps;Good to know
- pass the measured display rate when you have one; without it the answer is the 60 fallback and says so
SoftBody3D
Simulate an ordinary game-authored triangle mesh as cloth on the existing fixed-step GPU lane. The mesh supplies every visible choice. This class welds exporter duplicates, owns spring and position buffers, and replaces only the cloned material's position node. It adds no material, colour, texture, wind, stiffness, damping, or pinning default.
class SoftBody3D extends Mesh<BufferGeometry, NodeMaterial> implements IComputeDrivenUse it to
- make a flag, cape, or curtain move as cloth
- simulate a deforming surface while keeping one edge pinned
- simulate cloth sails blowing in the wind
- make cloth sails billow in wind on a ship
Example
const flag = new SoftBody3D(flagMesh, { pinned: topEdge, stiffness: 35, damping: 1.8, gravity: [0, -9.81, 0], wind: [1.5, 0, 0.4] });Good to know
- the mesh must use one Three.js node material and contain complete triangles
- pinned, stiffness, damping, gravity, and wind are required game-owned inputs
- Pixel 8 steady upper bound for the shipped 45-vertex pennant with readback every two frames: whole-starter update p95 4.66 ms, render p95 3.56 ms, and GPU timer 0.05 ms across three 300-frame final-rung windows at 552x248 with 4x MSAA; these whole-scene numbers are not isolated solver cost
Options
- timeStep follows the engine 1/60-second convention unless the game overrides it
- readbackEveryFrames enables an explicitly stale CPU position sample; zero disables it
softCircleDataTexture
Build a soft round sprite as pixel data instead of painting a canvas.
function softCircleDataTexture(size = 64, hardness = 0.25): DataTextureUse it to
- give smoke, flash, or glow sprites a radial alpha falloff
- generate sprite images that render identically under every backend
Example
const puff = softCircleDataTexture(64, 0.25);Good to know
- canvas-painted images sample black under WebGPURenderer; write sprites as pixel data there
solarPosition
Calculate solar elevation and azimuth from time, latitude, and longitude.
function solarPosition(input: ISolarPositionInput, target?: ISolarPosition): ISolarPosition;Use it to
- move a sun across a real day at a game's latitude and longitude
- run a day and night cycle over the game's sky
Example
const sun = solarPosition({ date, latitude: 49.28, longitude: -123.12, utcOffset: -8 });Good to know
- dates are interpreted as UTC unless utcOffset is supplied; no fixed sun direction is assumed
- pass a mutable { azimuth, elevation } target to reuse the result object in a steady frame loop
solarPositionAt
Calculate solar elevation and azimuth for one UTC date.
function solarPositionAt( date: Date | string, latitude: number, longitude: number, ): ISolarPositionUse it to
- calculate a sun direction from a timestamp and a game location
Example
const sun = solarPositionAt(new Date(), 49.28, -123.12);Good to know
- dates are interpreted as UTC; use solarPosition for an explicit local offset
spanNow
Attribute the render phase to 100% with a nested span tree, off unless TN_FRAME_SPANS asks. Every non-leaf reports its own time minus the spans inside it, so an unmeasured part shows up as a residual instead of being filed under "other"; a child that outlives its parent reports a negative residual rather than a clamped zero.
function spanNow(): numberUse it to
- find out what inside the render phase is actually costing the frame
- tell a shadow pass's traversal from the main pass's, with the residual computed
- price an optimisation against a measured part of the phase rather than the whole of it
Example
if (spansRequested()) setSpanRecorder(new SpanRecorder());Good to know
- off by default and installed by
TN_FRAME_SPANS=1; unset, every call site is one guarded return - the tree is closed against the frame budget's own render phase, so
TN_FRAME_SPANSandTN_FRAME_BUDGETdescribe the same frames - measurement only: no span changes what is drawn, in what order, or with which renderer
spanRecorder
Attribute the render phase to 100% with a nested span tree, off unless TN_FRAME_SPANS asks. Every non-leaf reports its own time minus the spans inside it, so an unmeasured part shows up as a residual instead of being filed under "other"; a child that outlives its parent reports a negative residual rather than a clamped zero.
function spanRecorder(): SpanRecorder | undefinedUse it to
- find out what inside the render phase is actually costing the frame
- tell a shadow pass's traversal from the main pass's, with the residual computed
- price an optimisation against a measured part of the phase rather than the whole of it
Example
if (spansRequested()) setSpanRecorder(new SpanRecorder());Good to know
- off by default and installed by
TN_FRAME_SPANS=1; unset, every call site is one guarded return - the tree is closed against the frame budget's own render phase, so
TN_FRAME_SPANSandTN_FRAME_BUDGETdescribe the same frames - measurement only: no span changes what is drawn, in what order, or with which renderer
SpanRecorder
Attribute the render phase to 100% with a nested span tree, off unless TN_FRAME_SPANS asks. Every non-leaf reports its own time minus the spans inside it, so an unmeasured part shows up as a residual instead of being filed under "other"; a child that outlives its parent reports a negative residual rather than a clamped zero.
class SpanRecorderUse it to
- find out what inside the render phase is actually costing the frame
- tell a shadow pass's traversal from the main pass's, with the residual computed
- price an optimisation against a measured part of the phase rather than the whole of it
Example
if (spansRequested()) setSpanRecorder(new SpanRecorder());Good to know
- off by default and installed by
TN_FRAME_SPANS=1; unset, every call site is one guarded return - the tree is closed against the frame budget's own render phase, so
TN_FRAME_SPANSandTN_FRAME_BUDGETdescribe the same frames - measurement only: no span changes what is drawn, in what order, or with which renderer
spansRequested
Attribute the render phase to 100% with a nested span tree, off unless TN_FRAME_SPANS asks. Every non-leaf reports its own time minus the spans inside it, so an unmeasured part shows up as a residual instead of being filed under "other"; a child that outlives its parent reports a negative residual rather than a clamped zero.
function spansRequested(): booleanUse it to
- find out what inside the render phase is actually costing the frame
- tell a shadow pass's traversal from the main pass's, with the residual computed
- price an optimisation against a measured part of the phase rather than the whole of it
Example
if (spansRequested()) setSpanRecorder(new SpanRecorder());Good to know
- off by default and installed by
TN_FRAME_SPANS=1; unset, every call site is one guarded return - the tree is closed against the frame budget's own render phase, so
TN_FRAME_SPANSandTN_FRAME_BUDGETdescribe the same frames - measurement only: no span changes what is drawn, in what order, or with which renderer
SpectralOcean
Simulate a spectral ocean — cascaded wave spectra inverse-transformed on the GPU each frame.
class SpectralOcean extends Object3D implements IComputeDrivenUse it to
- make an ocean whose surface is the view rather than a background
- float a boat on waves the GPU is drawing
- drive a water material from a wave simulation without writing an FFT
- sail a ship on simulated ocean waves with buoyancy
Example
const sea = ctx.add(new SpectralOcean(oceanOptions));Good to know
- it draws nothing; the game supplies the mesh, the material and every colour
- CPU height is a throttled copy carrying staleFrames, never this frame and never exact
- an exact free CPU height needs an analytic wave field instead; that is a different contract
- cascades are ordered largest patchSize first and every tuning number is required
SpriteAnimator3D
Advance a game-owned non-uniform sprite atlas on the fixed step.
class SpriteAnimator3DUse it to
- play an animated pickup or sprite-sheet effect
- sequence atlas frames with different authored durations
Example
const animator = new SpriteAnimator3D({ texture: atlas, frames, mode: "pingPong" });Good to know
- the game supplies the atlas texture, surface, filtering, layout, and every frame duration
- update is driven by the scene fixed step; no wall clock or default frame rate is used
staticTransformCensus
Stop recomposing the transforms of a subtree nobody moves. markStatic(root) composes the subtree once and freezes it; the engine re-arms a root whose own transform the game changes, and invalidateStatic(object) announces a write deeper inside one.
function staticTransformCensus(): IStaticTransformCensusUse it to
- cut the per-frame matrix work of terrain, buildings, props and other scenery that never moves
- keep a frozen subtree correct when the game does move it after all
Example
invalidateStatic(drawbridge);Good to know
- staticness is authored, never guessed; no heuristic watches gameplay and decides for you
- it deletes the local and world matrix composes, not the walk itself — three 0.185 recurses into every child regardless
- a write deeper inside a frozen subtree must call
invalidateStatic;TN_RENDERLIST_VALIDATE=1is what proves it happened
TracerPool3D
Pool travelling bullet-streak meshes for hitscan shots.
class TracerPool3DUse it to
- show where a hitscan round went
- show each round a weapon fires, one tracer per trigger press
- draw incoming fire without spawning projectiles
Example
const tracers = new TracerPool3D(ctx.scene, tracerOptions);
tracers.spawn(muzzle, shotDirection, hit.distance);Good to know
- the surface comes from the game; pooling, travel, and fading belong to the engine
- update once per frame and dispose with the owning scene
unmarkStatic
Stop recomposing the transforms of a subtree nobody moves. markStatic(root) composes the subtree once and freezes it; the engine re-arms a root whose own transform the game changes, and invalidateStatic(object) announces a write deeper inside one.
function unmarkStatic(root: Object3D): voidUse it to
- cut the per-frame matrix work of terrain, buildings, props and other scenery that never moves
- keep a frozen subtree correct when the game does move it after all
Example
invalidateStatic(drawbridge);Good to know
- staticness is authored, never guessed; no heuristic watches gameplay and decides for you
- it deletes the local and world matrix composes, not the walk itself — three 0.185 recurses into every child regardless
- a write deeper inside a frozen subtree must call
invalidateStatic;TN_RENDERLIST_VALIDATE=1is what proves it happened
updateAtmosphereParameters
Apply a partial game-owned atmosphere change while preserving validation.
function updateAtmosphereParameters( current: IResolvedAtmosphereParameters, patch: IAtmosphereParameterPatch, ): IResolvedAtmosphereParametersUse it to
- change scattering coefficients and rebake an atmosphere
Example
atmosphere.setAtmosphere({ rayleigh: [0.008, 0.016, 0.04] });Good to know
- patches cannot introduce omitted, negative, or non-finite physical values
updateClusteredMeshes
Take every clustered mesh under a root through this frame's cut, before the render. The engine already does this for the scene it renders; reach for it only to cut a subtree the engine does not render, such as one staged for a camera of your own.
function updateClusteredMeshes( root:Use it to
- cut a virtual-geometry subtree the engine does not render itself
Example
updateClusteredMeshes(stagedRoot, myCamera, ctx.renderer.domElement.height);updateModelLods
Draw a model at the detail its projected geometric error earns, from a chain the asset cook baked. This is engine-owned, and a game does not call it. assets.lod: {} opts in — the default-on front door opens after qualification — the model pass bakes TN_discrete_lod into eligible models, the loader registers the reader, and the engine runs the selection every frame before it renders. Each frame the mesh picks the cheapest baked level whose measured error projects to fewer than the resolved pixel budget, taking the camera's own projection, zoom, viewport and a conservative nearest depth into account. Refinement is immediate; coarsening waits for the resolved hysteresis. A mesh with no baked chain draws its full geometry.
function updateModelLods( root:Use it to
- draw a vehicle or hull built from many small meshes at distance without its full triangles
- stop a distant model from costing its authored mesh count and triangle count
- one source model, right detail by default, no hand-authored LOD files
Example
// Nothing to call: the loader returns a mesh with the chain and the engine selects every frame.
const hull = await ctx.assets.model("hull.glb");
ctx.scene.add(hull.scene);Good to know
- the chain is baked by the asset cook; there is no runtime generation and no runtime flag
assets.lod: falseopts out globally andassets.lod.overridesper asset, with no runtime controller installed- only static indexed triangles are eligible; skinned, morphed, alpha-blended or authored-LOD meshes keep full detail
- the real gate is measured benefit: a level must save at least
minSaving(20% default) of its predecessor, and a mesh that cannot is skipped, not forced
Options
assets.lod.generation.maxLevels,.minTriangles,.minTrianglesScope,.minSavingand.errorTargetsmove the bake's ceiling, pre-filter, its scope, its saving rule and its error ladder;assets.lod.runtime.maxPixelErrorand.hysteresismove the runtime budget and coarsen band; all by project, preset or asset
validateWorldMatrices
Prove that nothing a cache skipped changed the picture: TN_RENDERLIST_VALIDATE=1 recomputes every world matrix the long way, every frame, and throws on the first element that disagrees with what the frame is about to draw.
function validateWorldMatrices(root: Object3D):Use it to
- prove a static freeze did not leave a stale transform on screen
- gate a scene in CI against silent transform divergence
Example
if (renderListValidationRequested()) console.log(formatValidationReport(report));Good to know
- off by default and expensive by construction — it does the work it is checking, twice
- it throws on the first divergence rather than logging; a validation mode that continues is one nobody reads
- it proves the frames it ran on and nothing else
velocityTexture
Provision screen-space motion data for temporal nodes and keep per-instance history at the frame boundary.
function velocityTexture(pass: IVelocityRenderPass): NodeUse it to
- keep a skinned character or instanced crowd stable in a temporal stage
- add the velocity output to a Three.js scene pass
- read screen-space motion for a temporal stage
Example
const scenePass = pass(scene, camera);
ensureVelocityOutput(scenePass);
renderer.setOutputNode(scenePass);
const tracker = new VelocityTracker();
tracker.update(scene);
renderer.render(scene, camera);
tracker.commit(scene);Good to know
- call
VelocityTracker.update()before the render andcommit()after it - a pass is only given a velocity target when a temporal stage consumes it
VelocityTracker
Provision screen-space motion data for temporal nodes and keep per-instance history at the frame boundary.
class VelocityTrackerUse it to
- keep a skinned character or instanced crowd stable in a temporal stage
- add the velocity output to a Three.js scene pass
- retain previous transforms for animated and instanced renderables
Example
const scenePass = pass(scene, camera);
ensureVelocityOutput(scenePass);
renderer.setOutputNode(scenePass);
const tracker = new VelocityTracker();
tracker.update(scene);
renderer.render(scene, camera);
tracker.commit(scene);Good to know
- call
VelocityTracker.update()before the render andcommit()after it - a pass is only given a velocity target when a temporal stage consumes it
- call
update()after gameplay writes andcommit()after the render
VirtualShadowNode
One directional shadow for a whole open world: camera-centred clip levels, each snapped to its own texel grid and re-rendered only when its window moves. Tracked casters draw into a per-level mover map every frame, so animated casters do not invalidate the cached levels after the one refresh caused by tracking or untracking them. Plugs into three's own light.shadow.shadowNode slot, so every material receives it.
class VirtualShadowNode extends ShadowBaseNodeUse it to
- crisp shadows close to the player across a large outdoor level
- shadow map too coarse over a big terrain
- one directional light shadow for a whole open world
- shadows shimmer when the camera moves
Example
const sun = new DirectionalLight(0xffffff, 3);
sun.castShadow = true;
sun.shadow.shadowNode = new VirtualShadowNode(sun, { clipExtents: [12, 40, 120] });Good to know
- the light must be a DirectionalLight with
castShadowand a target in the scene - clipExtents are half-widths in world units, finest first, strictly increasing
- call
trackCaster(object)for movers; it enables layerVIRTUAL_SHADOW_MOVER_LAYERon the object and its descendants, tracking or untracking refreshes cached levels once, and subsequent mover movement refreshes only when a window moves - call
object.layers.set(VIRTUAL_SHADOW_CASTER_LAYER)for a mesh that exists only to cast; the level cameras already render that layer and the main camera never does
Options
- bias, biasNode, normalBias, intensity, radius, blurSamples, mapType and filterNode stay on
light.shadow; mapSize and the other options here have defaults, andmarker: falsesilences the TN_VIRTUAL_SHADOW line, not the measurement - bias, biasNode, normalBias, intensity, radius, blurSamples, mapType and filterNode stay on
light.shadow; mapSize and the other options here have defaults - followViewFocus: false keeps eye follow; receiverPlaneBias: false uses only authored bias
warmUpScene
Warms up scene for camera, in slices, presenting a frame between each. Fail closed on a nonsensical slice size rather than quietly choosing one: a zero or negative slice would loop forever, and a caller that passed it has a bug worth seeing now.
async function warmUpScene( renderer: IWarmUpRenderer, scene: Object3D, camera: Camera, options: IWarmUpOptions =Use it to
- compile a scene's shaders during the loading screen instead of on the first frame
- stop a native launch freezing for seconds inside its first rendered frame
Example
await warmUpScene(renderer, scene, camera, { onProgress: (p) => setLoading(p) });WaterSurface3D
Give a horizontal water surface the world mirrored in it, the world beneath it, and the metres of water between them.
class WaterSurface3DUse it to
- reflect the sky and the shoreline in a lake, pond or river
- see the bed through the water and have the shallows fade at the shore
- know how deep the water is under a pixel without a second render pass
- stop a water surface repeating in visible bands or stripes
- keep a crowd of small actors out of the water's reflection so the frame can afford it
- stop the water reflection redrawing the whole world every frame
- automatically reflect terrain and large casters without redrawing instanced small props
Example
const surface = new WaterSurface3D({ level: 0, maxThickness: 3, reflection: { resolutionScale: 0.5 } });
material.colorNode = mix(surface.refractionAt(offset), surface.reflectionAt(offset), fresnel);Good to know
- it draws nothing; the game supplies the mesh, the material and every colour
- the material must be transparent so the frame beneath it is already drawn
- thickness is metres, saturating at maxThickness; sky behind the surface reads deep
- one reflection is a second draw of the world; resolutionScale is its pixels only
- omitted reflection.layers automatically reflects terrain-sized surfaces and large non-instanced casters, excluding instanced/skinned props; TN_WATER_REFLECTION_DEFAULT reports the set once
- reflection.refreshInterval is in presented frames; 1 is every frame, and the default
- the mirror plane is level, from level alone; do not parent target to a scaled mesh
Options
- reflection.layers is an explicit Three Layers mask and wins over automatic filtering, including layer-0 mask 1; reflection.minSize overrides the default 10 metre minimum extent
WaveField
Evaluate analytic waves on CPU and displace game-owned vertices with the matching TSL graph. wanted, and the warp jacobian, every slope and both allocations are skipped
class WaveFieldUse it to
- float a boat on waves
- make water move
- find the water surface height at a point
Example
const field = new WaveField({ waves });
const { height, normal } = field.sample(x, z, elapsed);
const lift = field.heightAt(x, z, elapsed);Good to know
- supply every wave amplitude, wavelength, direction, speed and warp value
- call setTime for the default graph clock when the game advances its own time
- sample allocates a result and a normal vector; ask heightAt when only the height is
withVelocityContext
Provision screen-space motion data for temporal nodes and keep per-instance history at the frame boundary.
function withVelocityContext<T>(node: T, source: Node): TUse it to
- keep a skinned character or instanced crowd stable in a temporal stage
- add the velocity output to a Three.js scene pass
- hand the provisioned velocity source through a composed temporal graph
Example
const scenePass = pass(scene, camera);
ensureVelocityOutput(scenePass);
renderer.setOutputNode(scenePass);
const tracker = new VelocityTracker();
tracker.update(scene);
renderer.render(scene, camera);
tracker.commit(scene);Good to know
- call
VelocityTracker.update()before the render andcommit()after it - a pass is only given a velocity target when a temporal stage consumes it
zenithTransmittance
Return direct vertical transmittance for the supplied atmosphere.
function zenithTransmittance( parameters: IAtmosphereParameters | IResolvedAtmosphereParameters, ): AtmosphereRgbUse it to
- check a supplied atmosphere's direct vertical transmittance
Example
const zenith = zenithTransmittance({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });Good to know
- use the returned value as a validation oracle; the rendered path samples the LUT
acceptHotUpdate
Register hot-reload state preservation for a game.
function acceptHotUpdate<TState extends Record<string, unknown>, TPhysics>( game: IGame<TState, TPhysics>, hot: IImportMeta["hot"], ): voidUse it to
- keep game state while editing source in development
- diagnose state shape changes during hot reload
Example
acceptHotUpdate(game, import.meta.hot);Good to know
- use only in the web development entry
assertPortableState
Validate that hot-reload state can cross the Vite boundary.
function assertPortableState(state: unknown): voidUse it to
- preserve JSON-shaped state during hot reload
- reject a non-portable game state before reload
Example
assertPortableState(game.state.getState());Good to know
- state must contain finite numbers and plain objects only
connect
Open a bounded, authenticated WebTransport message channel shared by browser and native games.
function connect(url: string, options: INetworkOptions): Promise<INetworkConnection>Use it to
- connect two game clients over the portable WebTransport seam
- send ordered actions and bounded unreliable state messages between game clients
- exchange multiplayer messages without putting replication or gameplay in the engine
Example
const connection = await connect("https://game.example/game", { applicationProtocol: "my-game/1", credential, channels: [{ id: 1, delivery: "unreliable" }] });Good to know
- the URL must use HTTPS and the credential is supplied by the game's identity flow; this API never issues credentials
- channels, message sizes, and queues are validated before WebTransport opens, and reliable overflow returns false
- native qualification depends on the installed host WebTransport bridge; iOS remains unverified
Options
- connectTimeoutMs, maxReliableMessageBytes, maxQueuedReliableBytes, and maxQueuedDatagrams are named per-connection limits
playtest
Install the playtest bridge into a portable game.
function playtest< TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined, >(options: IPlaytestOptions =Use it to
- expose movement and world observations to a playtest
- connect a game to the ThreeNative scenario runner
Example
const game = defineGame({ plugins: [playtest()] });Good to know
- install once in the game's plugin list
createReactOverlay
@threenative/core/react — React that renders on a phone, with no DOM and no WebView. A subpath on purpose. react and react-reconciler are optional peers, and nothing in @threenative/core's main entry imports either of them, so a game that never mounts a React overlay pays nothing and core stays consumable from React Three Fiber. Importing this module is the opt-in. The element vocabulary is two components, borrowed from React Native rather than invented: View is a rectangle, Text is a run of glyphs. They are components and not lowercase intrinsics because view and text are already SVG tags in @types/react, and a HUD element that silently means <svg:text> on one platform is exactly the kind of quiet divergence this path exists to end.
function createReactOverlay(options: IReactOverlayOptions): IReactOverlayUse it to
- render a React HUD on Android or iOS without a WebView
- show the same React component on web and on a phone
- show a React HUD on Android, iOS or desktop native
- render the same React component on web and on a phone without a WebView
Example
const overlay = createReactOverlay({ canvasLayer: ctx.canvasLayer });Good to know
- styling is the
styleprop; Tailwind class names are CSS and cannot cross - import
react, neverreact-dom, from the portable native entry - import
react, neverreact-dom, from a native entry
measureText
Width in pixels of a glyph run at a given cell height.
function measureText(text: string, fontSize: number, letterSpacing = 0): numberUse it to
- measure native React HUD text before laying it out
Example
const scoreWidth = measureText("SCORE 10", 24)supportedGlyphs
Every character this glyph set can draw, for error messages and for the templates' AGENTS.md.
function supportedGlyphs(): stringUse it to
- discover which characters a native React HUD can draw
Example
supportedGlyphs().includes("A")supportedStyleKeys
Every style key the overlay implements, for the templates' AGENTS.md and for error messages.
function supportedStyleKeys(): readonly string[]Use it to
- discover which React HUD style properties work on native
Example
supportedStyleKeys().includes("centerX")Text
A run of bitmap glyphs, drawn as one instanced quad per lit pixel.
function Text(props: ITextProps): ReactNodeUse it to
- show text in a native React HUD without a DOM
Example
<Text style={{ color: "#ffffff", fontSize: 24 }}>SCORE 10</Text>Also found by
- objective panel journal
View
A rectangle. Paints when its style has a background; otherwise it only positions children.
function View(props: IViewProps): ReactNodeUse it to
- group and position native React HUD elements
Example
<View style={{ centerX: true, top: 24 }}><Text>READY</Text></View>connectUiBridge
Open the message channel between a game and its UI, whatever host is underneath.
function connectUiBridge(options: IConnectOptions): IUiBridgeUse it to
- write a UI that talks to the game on web and on a phone alike
- send a message from a HUD rendered over the game surface
- connect game code to a platform-owned UI realm
- share one UI bridge implementation across web, Android, iOS, and desktop
Example
const bridge = connectUiBridge({ end: "ui" });Good to know
- the transport is discovered, never configured; no game names the web view
onUiIntent
Handle the actions a UI sends back to the game.
function onUiIntent( bridge: IUiBridge, listener: (intent: string, payload: unknown) => void, ): () => voidUse it to
- restart or pause a game from a button in its HUD
- handle a HUD button action in game code
- route native and web UI commands through one listener
Example
onUiIntent(bridge, (intent) => { if (intent === "restart") game.goto("Play"); });Good to know
- prefer game.ui.onIntent, which connects the bridge for you
publishHitRegions
Tell the native input host where a UI's touchable controls are.
function publishHitRegions(options: IRegistryOptions): IHitRegionRegistryUse it to
- let a touch on empty HUD space reach the game instead of the UI
- make a HUD button receive taps on a phone
- give native input hosts the rectangles claimed by UI controls
- keep touch hit testing aligned with a moving web or native HUD
Example
publishHitRegions({ bridge });Good to know
- mark controls with data-tn-interactive; pointer-events is not the mechanism
publishUiState
Publish the game's state so a UI in another process can mirror it.
function publishUiState<T>( bridge: IUiBridge, store: IPublishableStore<T>, options: IPublishOptions =Use it to
- show score or health in a UI rendered over the game surface
- keep a HUD in step with the game without re-rendering on the loop
- publish game state to a HUD in another realm
- keep a web and native UI mirror on the same coalesced state stream
Example
publishUiState(bridge, game.state);Good to know
- publishes once per rendered frame unless stateFlushMs selects a slower interval, and not at all with no UI listening
Also found by
- journal objective panel
- readable HUD
sendUiIntent
Send a player action from the UI back to the game.
function sendUiIntent(bridge: IUiBridge, intent: string, payload?: unknown): voidUse it to
- wire a Restart button in a HUD to the running game
- pause a game from a menu drawn over its surface
- send a button or menu action from a UI HUD to game code
- keep UI input portable across web and native hosts
Example
sendUiIntent(bridge, "restart");Good to know
- one-way; the game decides what each name means and may ignore one
subscribeUiState
Mirror the game's published state on the UI side.
function subscribeUiState<T>(bridge: IUiBridge): IUiStateMirror<T>Use it to
- read game state from a HUD that runs in the platform's web view
- read published game state from a UI HUD
- subscribe a web or native UI to the game's mirrored state
Example
const mirror = subscribeUiState(bridge);Good to know
- returns undefined until the game publishes its first state
cellPlacements
Borrow the run's placement records as a live view over the placement buffer.
function cellPlacements(placements: ArrayBuffer, run: IWorldRun): Float32ArrayUse it to
- feed one cell's instance transforms into a batch without copying
Example
const records = cellPlacements(buffer, { asset: "tree", offset: 0, count: 120 });Good to know
- the returned view aliases the caller's buffer; writing to it mutates the source
getWorldCapabilities
Resolve the active world-generation path from host capability facts. The function accepts the adapter facts instead of reaching through a renderer-specific global, so browser and native hosts can report the same object. Missing limits are not treated as infinite: a host must either provide a valid GPU limit report or explicitly choose CPU fallback. GPU generation remains unavailable until a GPU readback can own the canonical field; a host adapter report therefore never upgrades a CPU fallback into a GPU generation claim.
function getWorldCapabilities(options: IWorldCapabilitiesOptions =Use it to
- decide whether generated terrain can use GPU compute
- report why terrain generation is using a reduced CPU fallback
Example
const capabilities = getWorldCapabilities({ limits: adapter.limits, cpuFallbackIterations: 8 });Good to know
- unsupported is returned when compute limits are unknown or below the requirement; callers must not silently continue
Options
- minimumWorkgroupsPerDimension, minimumStorageBufferBindingSize, and cpuFallbackIterations
Heightfield
One height buffer shared by world queries, rendered geometry, and a physics heightfield. The game supplies every value, so changing the terrain's shape never requires a package edit. fromSampler evaluates that game function exactly once at each vertex and retains only the resulting numbers. Queries interpolate those same numbers instead of evaluating the function again.
class Heightfield extends Group implements IComputeDrivenUse it to
- build terrain geometry and collision from one game-authored height function
- generate a terrain a player can walk across
- query the same ground height or normal that a player sees and collides with
- ask how high the ground is here
- build islands and coastlines from terrain
- keep a collider or other copy of a deforming terrain in step without rescanning the whole field (
trackChanges)
Example
const field = Heightfield.fromSampler({ rows: 65, columns: 65, width: 64, depth: 64, origin: { x: 0, z: 0 }, sampleHeight: terrainHeight });Good to know
- sampleHeight owns the terrain shape and stays in game source; the framework stores and interpolates its output
- rows and columns are vertex counts; geometry is row-major z-then-x and collider export transposes once into Rapier's column-major matrix order
Options
- rows, columns, width, depth, origin, and sampleHeight are explicit on every field
heightSamplerFromHeightmap
Build a game-usable sampleHeight from a raw v1 heightmap. The returned function interpolates bilinearly in world units and clamps to the map edges, so it plugs straight into Heightfield.fromSampler and TerrainTiles. Height is heightMin + v / 65535 * (heightMax - heightMin) at vertex (column, row).
function heightSamplerFromHeightmap( terrain: IWorldTerrain, extent: IWorldExtent, data: Uint16Array, ): (x: number, z: number) => numberUse it to
- turn an exported raw heightmap into terrain collision and rendering
- query ground height from a Blender-authored world package
Example
const sampleHeight = heightSamplerFromHeightmap(terrain, extent, await loadWorldHeightmap(url));Good to know
- the sampler reads the game's data; the framework never selects a terrain shape
loadTerrainSplat
The splat terrain surface a world package describes, for WorldCells.load({ surface }). Layers blend over a base by mask channels read as linear data (the masks ship raw, beside the heightmap, so no cook moves a blend threshold), with noise-broken edges and macro brightness variation. Texture sets tile in world metres on the package's ground plane (x, -z: a Z-up authoring tool's x and y), cliffs can be triplanar, and the base plus any layer that asks carries a normal map. Nothing here is a look choice: textures, tiles, tints, thresholds and noise scales all come from the package's table, which the game authors once and its DCC shares.
async function loadTerrainSplat(options: ILoadTerrainSplatOptions): Promise<Material>Use it to
- terrain textured by splat masks exported from Blender with the world package
- the game's terrain should match the DCC's terrain material without a second copy
Example
const surface = await loadTerrainSplat({ assets: ctx.assets, url: "world/world.json" });
const world = await WorldCells.load({ assets: ctx.assets, url: "world/world.json", surface, follow, ring: 2 });Good to know
- the package's world.json must carry
terrain.layers.tableandterrain.layers.splat, written by theexport_terrain_layers.pyrecipe - WebGPU allows 16 sampled textures per stage: planes + diffuse maps + normal maps must fit
Options
- every value comes from the package's table; the returned material is the game's to adjust
loadWorldHeightmap
Fetch a raw little-endian uint16 heightmap and expose it as samples.
async function loadWorldHeightmap(url: string): Promise<Uint16Array>Use it to
- load a world package's heightmap once before building terrain
Example
const data = await loadWorldHeightmap("/world/terrain/heightmap.u16");Good to know
- a non-OK response throws; bytes are byte-swapped only on a big-endian host
snowDiscFootprint
A circular contact footprint, sized from a radius the caller measures. The shape a sphere, a ball, a wheel or a probe presses into snow, and the default the physics binding derives from a sphere's contact geometry. Flat inside the radius, eased over the softness band, with a raised rim just outside it.
function snowDiscFootprint( radius: number, options: ISnowDiscFootprintOptions =Use it to
- press a ball, wheel or probe into snow
- give a sphere a physically sized snow contact instead of a boot shape
Example
const footprint = snowDiscFootprint(0.25);Good to know
- radius is metres; the footprint never grows with load, only deeper
Options
- softness widens the eased edge without changing the contact radius
SnowField
Persistent snow deformation over one canonical heightfield. Snow keeps four channels per cell — indentation, displaced bank, compaction and disturbance — and writes the combined surface (terrain + depth + bank - indent) back into the heightfield it was given. Queries, rendered geometry and collider export therefore all read one surface, and no second terrain representation exists. The field is numeric only: it never chooses a boot, a tread, a particle, a material or a camera. Games supply terrain heights, the snow depth, the contact profile and the response coefficients; this class owns the storage, the load-dependent penetration and the bounded recovery. It is a heightfield approximation — not granular snow, displaced-volume conservation, avalanches, melting or a calibrated material law.
class SnowFieldNeeds
- @threenative/core/world Heightfield as the canonical terrain and surface
Use it to
- leave footprints, tracks and tyre ruts in snow that persist and fill in over time
- let a pushed sphere carve a connected track and a dropped one settle into a crater
- store snow deformation that rendered geometry and collision both read
- reset a snowfield between rounds or change its depth at runtime
- show how packed the snow is where people have walked or objects have rested
Example
import { Heightfield } from "@threenative/core/world";
const terrain = Heightfield.fromSampler({ rows: 129, columns: 129, width: 64, depth: 64, origin: { x: 0, z: 0 }, sampleHeight: (x, z) => Math.sin(x * 0.1) * 0.5 });
const snow = new SnowField({ field: terrain, depth: 0.28, hardness: 0.3 });
snow.stamp({ x: 0, z: 0, area: 0.074, load: 784, duration: 0.1, footprint: snowDiscFootprint(0.12) });
snow.recover(1 / 60, 0.0012, 0.4);Good to know
- the field composes onto a Heightfield; construct the terrain first and let this own the surface
- zero depth is bare ground: contacts register no indentation at all
- out-of-region heightAt and normalAt follow Heightfield's error contract; sample returns zeros
Options
- depth, hardness, yieldFraction, maxBank and responseTime name the response coefficients
TerrainTiles
Stream a bounded square of game-authored heightfields and keep their render and physics units together. The class composes ordinary THREE.LOD objects and leaves frustum/projection culling to the renderer's existing scene path.
class TerrainTiles extends Object3D implements IComputeDrivenUse it to
- stream terrain without cracks
- keep generated terrain resident around a moving player
- put a generated terrain tile into a game-owned physics world
Example
const tiles = new TerrainTiles({ sampleHeight, surface: gameSurface(), tileSize: 256, tileResolution: 129, residentTileBudget: 25, residentByteBudget: 32_000_000 });Good to know
- sampleHeight and surface are required game choices; no landform or surface preset is installed
- residentTileBudget and residentByteBudget are hard caps; a tile that cannot fit throws
- seam gap, LOD pop and the rendered-vertex finiteness scan are measurements that are off by default; TN_TERRAIN_VALIDATE=1, ?tnTerrainValidate=1 or validate: true runs them, and maxSeamGap, maxVisualSeamGap and maxLodPop report undefined while they are off
Options
- tileSize, tileResolution, lodFactors, lodDistances, skirtDepth, streamRadius, colliderRadius, mergeTiles, validate, and budgets
Also found by
- stream terrain across chunks
terrainValidationRequested
Whether TN_TERRAIN_VALIDATE asks for terrain validation on this launch.
function terrainValidationRequested(): booleanUse it to
- turn terrain's per-frame seam, LOD pop and vertex checks on for one run
- assert the terrain geometry a game streams before it ships
Example
// Reads its own the way `renderListValidationRequested` does: a native launch sets the
// environment variable, a browser asks with the query string, a test or harness sets the
// global. `0` and `false` are off, so a saved URL that enabled it still says "off".
const tiles = new TerrainTiles({ ...options, validate: terrainValidationRequested() });Good to know
- off by default: it is the work it checks, every frame
validateWorldPackage
Validate a world.json manifest against the v1 contract. Never throws on garbage input: a non-object manifest is WORLD_MALFORMED. Every problem is collected, so an exporter sees the complete list at once.
function validateWorldPackage( manifest: unknown, options: IWorldPackageValidationOptions, ):Use it to
- check a Blender-exported world package before the runtime attaches anything
- report why a world package cannot be streamed
Example
const { ok, errors } = validateWorldPackage(json, { placementsByteLength: buffer.byteLength });Good to know
- validation only checks structure and ranges; it never fetches the heightmap or GLBs
WorldCells
Stream a Blender-authored world package by cell and keep it resident around a followed point. The class composes TerrainTiles for the package's heightmap, builds one InstancedBatch per resident cell asset run, distance level and mesh part, and loads hand-placed chunk GLBs through loadAll + addInSlices. Ring residency, per-asset maxDistance filtering, the per-asset lods levels, hard budgets and generation-tokened cancellation all live here; every geometry, material and surface still comes from the package's GLBs and the game. An asset is drawn per part, not per model: a GLB with several primitives is one InstancedBatch each, and a scattered part whose own material is transparent draws as an alpha cutout unless the game asks for blending, because an InstancedMesh cannot sort its instances. An asset whose package entry names no lods is drawn at the levels its own model carries a baked AutoLOD chain for: the levels an instanced draw cannot reach by itself, switched at the distance their error projects over the autoLod viewport. Authored lods win; a chain is only a fallback.
class WorldCells extends Group implements IComputeDrivenUse it to
- stream a large Blender-authored world by cell instead of one huge GLB
- keep scattered props and hand-placed chunks resident around a moving player
- honour per-asset draw distances and hard streaming budgets without a mid-frame throw
Example
const world = await WorldCells.load({ url: "/world/world.json", surface, follow, ring: 1, budgets: { residentCells: 25, instances: 20000, bytes: 8000000 } });
scene.add(world);
world.update();Good to know
- surface is the game's; this class creates no material, colour or geometry
- budgets are hard caps that report pressure instead of over-committing
- model loads are bounded by
concurrency(default 12) across every resident cell, not per cell - refilters are bounded by
rebuildsPerUpdate(default 16) per update, nearest cell first - admission is bounded by
admissionBudgetMs(default 2) per update across every path, plus at most one unit each for terrain and props; while props are queued terrain takes at most half, so neither starves the other, and a deferred cell keeps drawing what it has - SkinnedMesh parts are skipped; an instanced copy would draw one rest pose
- a baked chain's switch distances are measured against
autoLod(default 4 px of error over 60° and 1080 raster rows), because an instanced draw cannot select a level per instance; an asset with authoredlodsnever consults it prewarmedresolves once every prewarmed shared batch has been drawn; a game with a loading screen waits on it, andstats().pendingPrewarmis the same gate as a number- every
asset:level:partis one InstancedMesh for the main pass, plus one caster InstancedMesh per world-grid square ofclusterSizeon the shadow caster layer, so the main pass draws one mesh per key and a shadow level submits only the squares it covers - two definitions the asset loader resolves to one model — the same cooked
glb, the samelodsat the same distances, the samemaxDistanceand bounds — are one asset under the lexicographically smallest id: one model load, one set ofasset:level:partkeys, one prewarm and one refcount, released when the last cell holding any member of the group leaves the ring;TN_WORLD_ASSET_ALIASreports how many of the package's assets are really distinct - the main pass mesh draws only the squares the render camera's frustum covers — on by default, narrowed once per frame for every main batch by the engine's render-cadence dispatch, never for an orthographic camera — a batch with nothing to draw is hidden rather than submitted at
count 0, andTN_WORLD_MAIN_CULLreports both every five seconds - a loaded chunk is merged by material before it is added, so it submits one draw per material rather than one per node; a skinned, multi-material or morph-target mesh, one carrying a baked AutoLOD chain, and an instanced mesh past
chunkMergeMaxTriangles(default 43,690 triangles) or with a shape over 2,048 triangles, all keep their own geometry; a material group crossing 131,072 vertices (4 MiB of position + normal + uv) is split into several meshes in traversal order instead of one giant upload, indexed parts keep their index, andTN_WORLD_CHUNK_MERGEreports what the merge did and the bytes it left shadows.castDistanceis accepted and ignored (clusters replaced it);shadows.invalidateis called at most once a second after streamed records changed
Options
- ring, budgets, terrain tile size/resolution, terrain stream and collider radius,
transparentScatter,clusterSizeandshadows.invalidate, loadconcurrency,rebuildsPerUpdate,admissionBudgetMsand the package's per-asset maxDistance
renderer.matrixWorld
The per-frame world-matrix walk, on by default as "visible": a hidden subtree — a full-detail body behind a merged stand-in, a hidden LOD level, a parked or hangared model — is not recursed into, so nothing that cannot draw pays a world-matrix multiply. Every visible node is composed exactly as three does, a class that overrides updateMatrixWorld (SkinnedMesh, Camera) runs its own, and a hidden node that holds bones is still walked. "all" restores three's every-node walk.
renderer.matrixWorld?: "visible" | "all"Use it to
- updateMatrixWorld and multiplyMatrices are hot in a profile
- the per-frame matrix walk is slow with many hidden LOD bodies or paired full/hull models
- stop paying for matrices of models nothing can draw
- a skinned mesh or camera goes stale because its world matrix was not refreshed
- restore three's own full updateMatrixWorld walk
- compare how many scene nodes the engine walks per frame
Example
renderer: { matrixWorld: "all" } // in threenative.config.ts; omit for the visible-only defaultGood to know
- Unset is the shipping behaviour:
"visible", which does not recurse into a hidden subtree."all"visits every node exactly as three's ownupdateMatrixWorlddoes. - A game that reads a hidden object's
matrixWorlddirectly must not rely on the walk reaching it: usegetWorldPosition/getWorldQuaternion/getWorldScaleor callobject.updateWorldMatrix(true, false)first. - The walk is the engine's either way, so three's renderer never walks the scene a second time; the count of nodes visited is reported as
matrixWorldin everyTN_PROJECTIONwindow. - Bones are never skipped: a hidden node that holds a
Boneis walked, because a visibleSkinnedMeshdraws with its skeleton's matrices wherever the armature sits.
Options
- renderer.matrixWorld: "all" restores three's every-node walk; the visited count still reports
renderer.minimumProjectedPixels
Do not submit what the render camera cannot resolve. On by default at a conservative 0.5 projected pixel; an object below it is skipped per render camera. Raise the number to cull more, set false to leave every object drawn — the count of what was skipped still reports in TN_PROJECTION.
renderer.minimumProjectedPixels?: number | falseUse it to
- my frame is slow with many distant objects
- draw count is high but the screen is mostly empty
- far away models, aircraft, boats or props cost draw calls but are specks
- a large roster or fleet drops the frame rate while barely visible
- stop submitting objects smaller than a pixel to the camera
- cull by how big something looks to the camera rather than how far it is from the player
- tune how aggressively distant objects are skipped
- a small object I need disappeared at range
Example
renderer: { minimumProjectedPixels: 2 } // in threenative.config.tsGood to know
- Unset is the shipping behaviour: the gate runs at 0.5 px. A game that wants the cut a shipped title tuned names 2;
falseleaves every object drawn. - The decision reads the render camera's projection and viewport, not the player's distance — a camera far from the player still culls its own specks.
- It writes only
object.visible, which the projection's batch key ignores;castShadow,layersandfrustumCulledare never flipped, because that churns batch grouping. - Shadow casters and objects attached to the render camera are never dropped on the main view alone. Exempt any other object with
alwaysRender. - Turning the gate off with
falsedoes not turn its measurement off:TN_PROJECTIONstill reports considered and skipped counts ascull.
Options
- alwaysRender(object) keeps one object drawn whatever the render camera resolves
- renderer.minimumProjectedPixels: false leaves the scene drawn and keeps the measurement on
renderer.projection
The engine's scene-render projection — an internal mirror that collapses repeated draws, including animated skinned rigs that share a geometry and material into one palette draw per pass — on by default. Set renderer.projection: false to decline it, or projection: { materialChecks: 'everyFrame' } to keep it and pay for a per-material check on every frame.
renderer.projection?: boolean | { materialChecks?: 'spread' | 'everyFrame' }Use it to
- a crowd of animated characters draws slowly
- many SkinnedMesh copies of one rig, each its own draw call
- the game got slower after the projection engaged
- turn off the render projection, batching, or the instanced mirror
- draw count fell but frame time did not
- a multi-second freeze when the mirror first engages
- opt out of an engine render optimizer
- thousands of props each with their own material, one colour apart
- a material edit takes a few frames to show up
- check every batched material every frame anyway
Example
renderer: { projection: false } // in threenative.config.tsGood to know
- Unset is the shipping behaviour: the projection runs. Only an explicit
falsedeclines it. - An opted-out game builds no mirror and runs no eligibility scan; the authored scene is what renders, so declining costs nothing rather than being re-judged each frame.
- TN_RENDER_PROJECTION still reports the verdict, with reasonCode
disabledrather than one of the measured declines. materialChecks: 'spread'is the default: a bounded slice of the batched materials is proved per frame instead of all of them, so a frame of 4,096 colour-only materials costs 512 checks rather than 4,096. A base-colour edit is never delayed — that write is O(1) per member.- The price of
spreadis staleness on every other material edit: a material that gains a roughness, a map or a define still leaves its group and is drawn exactly, up tomaterialCheckStaleFramesframes later. TN_RENDER_PROJECTION reports that bound;materialChecks: 'everyFrame'sets it to 0 and restores the per-member, per-frame check. - Any other
materialChecksvalue throws at startup rather than falling back to a default.
Options
- renderer.projection: false declines the whole mirror and costs nothing to decline
- renderer: { projection: { materialChecks: 'everyFrame' } } proves every batched material every frame instead of the default bounded slice