Public API, version 1.0.0
Import from the delivered engine/index.js. The exported THREE namespace is the matching bundled Three.js instance. Dimensions use application-defined world units; the shell interprets one unit as one metre. Y is up. Aircraft art points along local -Z.
Engine
new Engine({canvas, quality='high', continuous=true, background=0x172833, onError})
Requires an actual canvas and WebGL2-capable browser. The canvas's CSS rectangle determines render size. High, medium and low are valid quality names. With continuous false, call invalidate when state or the view changes.
| Method | Behavior |
|---|---|
await mount(factory) | Builds a stage, replaces the active scene when ready, compiles its visible materials. Returns false if superseded. |
usePipeline(pipeline) | Installs an application-supplied rendering pipeline and disposes the previous one; null selects direct rendering. |
setQuality(name) | Adjusts resolution and pipeline size; calls the active scene's configure hook. |
resize() | Updates target dimensions and camera aspect from the canvas. Usually triggered by ResizeObserver. |
invalidate() | Requests one frame; repeated requests are coalesced. |
pause(value) | Pauses or resumes updates, retaining the interpolated pose. Omit value to toggle. |
tick(dt) | Performs a deterministic update/render step. Intended for fixtures and manually scheduled integrations. |
pick(clientX, clientY, objects?) | Returns the nearest visible ray intersection in the supplied object list, or null. Coordinates are browser client coordinates. |
diagnostics() | Returns recent CPU frame-work timings, draw work and allocated geometry/texture/program counts. CPU work is not FPS or completed GPU time. |
dispose() | Idempotently stops the engine and releases its owned resources/context. |
Scene factory:
import { Engine, THREE, Daylight } from './engine/index.js';
const engine = new Engine({ canvas: document.querySelector('canvas') });
await engine.mount(({ scene, camera, scope, signal }) => {
const lighting = scope.own(new Daylight(scene, engine.renderer));
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(2, 2, 2),
new THREE.MeshStandardMaterial({ color: 0xd5a46d })
);
scene.add(mesh);
camera.position.set(6, 4, 8);
camera.lookAt(mesh.position);
return {
update({ dt, time }) {
mesh.rotation.y += dt * 0.2;
lighting.update(time, mesh.position, camera);
},
configure({ quality }) { lighting.configure(quality); }
};
});
Factory context contains engine, scene, camera, scope and signal. Optional returned hooks are fixedUpdate(dt), update({dt,time,alpha,engine}), configure({quality}), onPause(paused) and dispose(). Additional returned application fields are available through engine.view. Callers must not replace the context's scene, camera, scope or abort controller with unrelated objects. Asynchronous factories should check signal between expensive batches.
ResourceScope and graph cleanup
scope.own(resource) registers resource.dispose and returns the resource. scope.defer(callback) registers arbitrary cleanup. scope.listen(target,type,callback,options) registers a listener with automatic removal. scope.dispose() runs callbacks in reverse order exactly once.
disposeTree(root,{exclude: new Set()}) releases geometry, materials, textures and skeletons found in the graph, then clears it. Objects that intentionally share resources across scopes need an explicit caller-managed lifetime. Do not dispose a shared material while another scene is using it.
MotionBody and FixedClock
new MotionBody({position, orientation, velocity, radius=1}) clones supplied Vector3/Quaternion values. Defaults are zero position/velocity and identity orientation.
integrate(dt,{acceleration, angularVelocity, drag=0, world=null, slide=true, ignore=[]})snapshots the previous pose, updates velocity, integrates local angular velocity in radians/second, and moves through SpatialWorld if supplied. Returns contact records. Acceleration is world-space; drag is an exponential rate in inverse seconds.sample(alpha, object3D)writes the interpolated position and quaternion to a visual object.teleport(position, orientation?)resets current and previous poses so interpolation does not cross the old location. Velocity is independently caller-owned.snapshot()records the current pose explicitly.
new FixedClock({hz=60,maxSteps=6}) exposes advance(dt, callback), alpha and reset. It caps elapsed catch-up to avoid an unbounded update backlog.
SpatialWorld
const world = new SpatialWorld({cellSize:128,maxCells:64});
world.addTerrain('ground', heightfield);
world.addBox('bridge', new THREE.Vector3(-5, 6, -1), new THREE.Vector3(5, 8, 1));
const hit = world.sweepSphere(start, end, radius);
| Method | Contract |
|---|---|
addBox(id,min,max) | Adds an axis-aligned solid box; vectors are copied. |
addSphere(id,center,radius) | Adds a solid sphere with positive radius. |
addTerrain(id,heightfield) | Adds a heightfield surface, treated as solid below the ground. |
addMesh(id,mesh) | Snapshots a mesh's geometry using its current world transform. |
addGeometry(id,geometry,matrix?) | Adds transformed static triangle surfaces. Default transform is identity. |
remove(id), clear() | Remove registered colliders. Duplicate IDs are rejected on addition. |
sweepSphere(start,end,radius,{ignore=[]}) | Earliest contact of a translating sphere with supported geometry; null when clear. |
raycast(start,end,options) | Zero-radius segment query. |
moveSphere(start,displacement,radius,{slide=true,ignore=[],maxContacts=4,skin=.001}) | Returns final position and hit list after bounded contact response. |
A hit contains id, kind, normalized t along the segment, centre position, surface point, outward/opposing normal, and initial penetration where supported. Terrain initially penetrated from below is recovered vertically. Box/sphere initial containment has explicit recovery. Arbitrary triangle meshes are surface colliders, not a universal inside/outside volume test.
The optional X/Z hash grid is only a conservative broad phase: vertical bounds and all contact calculations remain three-dimensional. Large colliders/queries fall back to the full set. Box triangle geometry is allocated only when needed.
Meshes are static snapshots: remove and re-add when their geometry or transform changes. The current query system does not rotate a swept convex hull, resolve dynamic object pairs, or implement friction/restitution. Applications may combine primitives and interpret contacts; the shell's craft uses an approximate 7-metre sphere.
Heightfield and generated landscape
new Heightfield({size=6000,segments=192,heights=null,height=(x,z)=>0,center=new THREE.Vector2()})
The grid has (segments+1)² heights. center.x is world X and center.y is world Z. Heights must be finite. Bounds and step are exposed. The following methods use the same cell split as the render geometry:
sample(x,z)→{height,normal,cell,triangle}or null outside the region.heightAt(x,z)→ height or null.vertex(gridX,gridZ,target?)→ world vertex.trianglesInBounds(box3)→ iterable of candidate terrain triangles.addDetailPatch({x,z,size=520,cellSize=3})adds aligned finer terrain. Call before creating render meshes, colliders and water. Newer patches replace older overlapping surfaces. Boundary heights match the preceding visible surface. Water supports up to four top-level patches.createMesh({material?,tileCells=32,colorAt?})→ Group of terrain tiles. colorAt receives world x, y, z and returns a Three.Color. Caller owns the returned graph.
generateLandscape({size=8000,segments=320,seed=731}) returns {terrain,seed,seaLevel,riverX,riverLevel,riverWidth,lake,beach,airfield,biomeAt,colorAt,points}. The deterministic art-directed generator produces a river valley and lake surrounded by mountains. It is not real geographic data or an erosion/fluid simulation.
Population belongs to independently copied objects/vegetation and objects/buildings packages. The application registers their colliders. See OBJECT_PACKAGES.md.
createRiverGeometry(landscape,{steps=240}) returns the descending river ribbon up to its lake-level outlet. createRiver(landscape) returns {mesh,update(time)} with flowing-normal material. Register its geometry as a spatial surface separately if water contact is required.
Lighting, water and postprocessing
new Daylight(scene,renderer,{hour=10,scale=4000,shadowExtent=160,fogDensity=.00025,quality='high',sky=true,cascades=false})
setHour(hour)changes the artistic 24-hour light/sky model.configure(quality)sets shadow sizes.update(time,focus,camera)places light targets and follows the camera with the sky.activeDirection,directColor,day,sun,moon,ambientexpose current lighting values.dispose()releases owned lighting, environment target and sky resources.
Use with a scene-specific resource scope. Set scale below the camera far plane. Shadow coverage is finite and centred on focus. With cascades enabled, three nested shadow maps overlap and blend before fading at the outer boundary. Materials are patched locally, including matching animated shadow passes; global Three.js shader chunks are not modified. Dynamic materials are discovered by the scene preparation hook.
new WaterSurface(renderer,{size=400,level=0,segments=64,terrain=null,wind=null,shore=null}) is a Mesh. Add it to the scene and own it through the scope. It supports only a horizontal surface at its declared Y level.
configure(quality)changes reflection size/frequency.update(time,{direction,color,reducedMotion=false,wakes=[]})updates light/wave state; wakes contain position and normalized horizontal direction vectors. Maximum two wake emitters.setFoamBoundaries(nodes=[],obstacles=[])uses at most eight circular boundaries. Node records have x,z; obstacle records have x,z,r.material.uniforms.roughness.valueis the current 0–1 surface-roughness control; foamStrength and mirrorStrength independently scale foam and reflection for inspection/art direction.dispose()detaches and releases geometry, shader, normal texture and reflection target.
A terrain snapshot supplies exact triangulated depth and shoreline clipping. Optional shore is {minX,minZ,maxX,maxZ} and creates a fine 0.5×0.75m wave grid in that rectangle. Shared WindField uniforms drive wave direction and speed. This is a visual surface model, not a fluid solver.
Use one reflected WaterSurface per scene in this release. The lake/river combination uses a single planar lake reflection and a separately shaded sloping river. Multiple recursive mirrors are not supported.
new PostFX(renderer) supplies resize, render and dispose. Pass it to engine.usePipeline. postfx.obscuration exposes the extracted optional obstruction-fade helper for up to six application-supplied boxes. See MODULES.md for its less extensively exercised status.
Content and controls
See MODULES.md for procedural models, articulated actors, flags, room lighting, particles and camera controls. Public exports remain the supported import boundary. Details of returned Three.js objects are available where documented; private shader internals can change between versions.
Shared environment
new Environment(scene,renderer,{preset='clear',quality='high',scale=10000,cascades=true,...overrides}) owns Daylight, WindField and RainField. Register it with a ResourceScope. Exposes state, wind, light and rain.
set({hour,cloudCover,storm,rain,fogDensity,windStrength,windSpeed,windDirection,moonPhase,starBrightness,nightAmbient})changes explicit fields. Wind direction is a Vector2-like{x,y}mapping to world X/Z. Unchanged fields keep their current transition.transitionTo('clear'|'sunrise'|'sunset'|'overcast'|'rain'|'night'|numericFields,{duration=12})interpolates settings; rain-front transitions delay precipitation until clouds advance.update(dt,time,focus,camera,{reducedMotion=false})updates all environmental layers. Call once per displayed frame before responsive objects use their uniforms.addLayer(layer)registers a custom layer receiving{dt,time,state,camera,focus,wind}; returns an unregister function. Registered layers are disposed with Environment.configure(quality),dispose()manage quality and lifetime.
Environmental parameters are an extensible presentation model. There is no meteorological forecast, astronomical ephemeris, fluid dynamics or global seasonal simulation. Night includes procedural stars, a faint galactic band and a moon disc; these are not a scientifically located star catalogue. A new object participates by receiving environment.wind or subscribing as a layer, not by reading a global singleton.
new WindField({direction=new THREE.Vector2(1,.35),strength=.75,speed=7}) exposes stable uniforms, set(parameters), update(time,{reducedMotion}), sample(worldPosition,time), patch(material) and apply(mesh). Strength is clamped to 0–2, speed to nonnegative. sample returns a world-space velocity with deterministic spatial/time gusts. addWindData(geometry,{height,bend,flutter,seed}) supplies mesh attributes for deformation. apply installs matching visible and depth deformation. Call applyInstanceFade(mesh) when instanced LOD geometry has an instanceFade attribute; signed complementary fades avoid holes across levels.
createGroundMaterial({waterLevel=4,size=8000,snowLine=640}) returns a locally generated grass/soil/rock/sand/snow material. It blends slopes and height bands, varies world-space detail and reduces distant bump aliasing. Dispose the material to release its textures.
Portable objects
OBJECT_PROTOCOL is 1. Each package exports classes such as Person, NavalFighter, Forest or Building and accepts (api,options) where api is the imported engine namespace. No package hardcodes an engine path. See OBJECT_PACKAGES.md and docs/objects for exact constructor options, properties, actions, serialization and disposal contracts. The shell's legacy-api module is application compatibility code, not the engine public API.
Bounded effects and geometry helpers
new ParticleField({capacity=256,seed=1,color,size=.15}) exposes mesh, emit(position,{count,speed,lifetime}), update(dt), dispose. Emission beyond capacity is omitted. new PlasmaSun(radius,{compact=false}) returns a Group with update(time,{quality,reducedMotion}), configure(quality), dispose; explicitly own it so all tessellation variants and its noise volume are released.
surfaceLibrary() returns locally generated materials; disposeSurfaceLibrary(library) releases all of their textures/materials, including unused ones. ArchitectureBatch(root) supports add, box, cylinder, beam, finish and finishSteps for compatible static primitive instancing. Finalize a batch once. consolidate(root) is for static compatible position/normal/UV meshes, not skinned actors.
Connected river and lake optics
For a river joining a lake, pass the lake owner explicitly:
const lake = scope.own(new api.WaterSurface(renderer, {
size: landscape.terrain.size,
level: landscape.seaLevel,
terrain: landscape.terrain,
wind: environment.wind
}));
const river = scope.own(api.createRiver(landscape, {
waterSurface: lake,
flowSpeed: 1.8
}));
scene.add(lake, river.mesh);
The connected river borrows the lake's optical uniforms, normal/depth textures and planar reflection capture. It uses the same depth coloring, Fresnel response, light inputs, wind and HDR water mask. Downstream flow adds advection gradually upstream, reaching the shared lake response at the outlet. River geometry supplies a waterFlow attribute and meets the lake at the exact declared surface elevation.
river.setFlowSpeed(value) changes nonnegative surface advection. river.update(time) updates the shared clock in connected mode, so use the same scene time for both surfaces. river.dispose() releases its geometry/material and unregisters it from reflection exclusion; it does not release borrowed textures. Own the river after the lake so scope cleanup releases borrowers first. Reflection users are hidden during the owner's capture to prevent sampling the target being rendered.
Without waterSurface, createRiver retains its independently owned PBR surface and now also exposes idempotent dispose. The shared horizontal capture approximates reflections on sloping upstream water; it is not a separate mirror for every river segment or fluid simulation.
Ground material water references
Ground-material options now include riverRise=0, riverEndZ=size*.18, and optional beach=null. Generic callers therefore receive a constant water reference by default. The supplied regional generator's scenes pass riverRise:105 and beach:landscape.beach explicitly. A designated beach retains broad sand grading; ordinary banks mix narrower soil, gravel, grass and small sand areas. This changes material coverage, not terrain/collision geometry.
Contact-shading precision
The contact pass reconstructs normals from distinct integer depth-pixel neighbors. It rejects samples on the reconstructed surface plane and fades kernels below the usable pixel/depth precision. These precautions prevent shallow-view striping, particularly with small cockpit near planes. Close contacts remain measurable. The effect is screen-space and cannot replace cast shadows or account for occluders outside the current view; direct rendering and disabling the pass remain supported.
Shadow coverage for close and large scenes
Daylight and Environment accept cascadeExtents: [near, middle, far] when cascades are enabled. Values are three increasing positive half-widths in metres. The default [180, 750, 3200] supports the flight landscape. The Beach shell uses [18, 85, 320] so human and prop shadows have useful spatial detail. Flight inspection uses [24, 120, 700] for beach/woodland views and restores the larger coverage for flight.
environment.light.cascades.setExtents(values) changes projection and bias without reallocating shadow maps; it returns whether the ranges changed. Invalid ranges fail before mutation. Coverage remains finite and blends across overlapping maps. The original non-cascade shadowExtent option is unchanged.
Render-pass matrix updates
Engine.tick applies scene updates, updates world matrices once, and reuses that pose through the main and reflection/effect passes. It temporarily suppresses repeated renderer-driven scene traversals and restores the caller's scene setting even if rendering throws. Apply ordinary transform changes in fixedUpdate or update. An advanced callback that deliberately changes transforms during rendering must explicitly update those matrices before using them. This retains one consistent pose across passes and avoids repeated skeleton work.
Shadow material and cube-depth lifetime compatibility
Engine-managed scenes install weakly tracked shadow callbacks while preserving caller callbacks and explicit custom materials. Single mapped source materials own stable depth/distance material pairs, released when that source material is disposed. Other cases retain shader-feature invalidation so a shared shadow sampler cannot re-upload a prior disposed texture. This also avoids repeated shader-variant work in the mapped/unmapped caster sequence.
The bundled renderer's cube-depth allocation bypasses its ordinary texture-source disposal cache. The engine captures only allocated cube shadow depth handles and deletes a still-live handle on the texture's dispose event. The listener removes itself; already-deleted handles are left alone. This is a narrow compatibility measure tied to the bundled renderer, with no vendor-source modification. Callers still own light/shadow lifetimes through their scene scope.
Camera and gamepad input
The engine exports OrbitControls and GamepadInput. OrbitControls adds panBy, lookBy, orbitBy, zoomBy, setMode and optional focused keyboard tick(realDt); keyboard defaults off for compatibility. GamepadInput polls the standard browser layout, filters drift and supplies held/pressed edges with neutral re-arming. Mapping inputs to object actions or aircraft handling belongs to the consumer. See CAMERA_AND_CONTROLLER.md for signatures, directions, lifecycle and examples of consumer integration.