@driftengine/core
The runtime of DriftEngine, a 3D game engine for the web
and for installed applications. It draws through WebGPU where the browser has a device and through
WebGL2 everywhere else, behind one API, and carries the fixed-step loop, the renderer, cameras,
input, the scene graph, geometry, text and the whole of @driftengine/physics, which it re-exports.
Everything else is a separate package a game adds when it reaches for it.
Cost: 966.4 KB gzipped, importing createRenderer. Measured by scripts/size-gate.test.mjs,
which fails the build if the figure drifts by more than 3%.
The manual is at driftengine.dev/docs, with an example running beside every chapter, and every export is in the reference.
Install
npm install @driftengine/core
It resolves to compiled JavaScript, so any ES module bundler takes it as it is; Vite is what the examples use. A browser needs WebGL2 at least, which every current one has. Installation covers the page, TypeScript and the backends.
Quickstart
A lit cube turning on a fixed step, on a page with <canvas id="stage">:
import {
Camera,
MeshBuilder,
SceneNode,
createEnvironment,
createRenderer,
srgbColor,
startLoop,
} from '@driftengine/core';
import { bindModule } from '@driftengine/script';
import { loadModule, patchModule } from 'driftscript';
import * as spinScript from './spin.drs';
const canvas = document.querySelector<HTMLCanvasElement>('#stage');
if (canvas === null) throw new Error('the page must carry <canvas id="stage">');
/*
* Asynchronous because it has to be: asking for a WebGPU adapter returns a promise, and there
* is no synchronous way to learn whether a usable device exists. The fall back to WebGL2 is
* automatic and silent, so `reason` is the only thing that says why. Print it: the first
* question asked about any picture is what drew it.
*/
const { renderer, backend, reason } = await createRenderer(canvas, {
maxDevicePixelRatio: 1.75,
});
const readout = document.querySelector('#backend');
if (readout !== null) readout.textContent = `${backend}: ${reason}`;
renderer.resize();
addEventListener('resize', () => renderer.resize());
/**
* Light, air and ground bounce. Chosen once, because these are allocation-sized decisions.
*
* Every colour here and below was picked by eye, so it goes through `srgbColor`: the renderer
* lights in linear values and encodes them for the screen at the end, and a colour typed as it
* looks would come out too light. The clear colour in the loop is one of them.
*/
const CLEAR = srgbColor(0.05, 0.06, 0.09);
const ENV = createEnvironment({
directionalDir: [0.4, 0.7, 0.35],
directionalColor: srgbColor(1, 0.96, 0.88),
ambient: srgbColor(0.2, 0.22, 0.28),
ambientGround: srgbColor(0.08, 0.08, 0.1),
emissiveGain: 0,
nightFactor: 0,
fogColor: srgbColor(0.16, 0.18, 0.24),
fogDensity: 0.004,
fogHeightFalloff: 0.03,
fogBaseY: 0,
});
/*
* Colour is vertex data here, which is what lets a whole world be flat-shaded draw calls.
* `addBox` takes a centre and *half* extents, so this ground is 24 units across and the cube
* is two units on a side.
*/
const shapes = new MeshBuilder();
shapes.addBox([0, 1, 0], [1, 1, 1], srgbColor(0.85, 0.45, 0.25));
const cube = renderer.createMesh(shapes.build());
const slab = new MeshBuilder();
slab.addBox([0, -0.25, 0], [12, 0.25, 12], srgbColor(0.3, 0.32, 0.36));
const ground = renderer.createMesh(slab.build());
const spinner = new SceneNode();
spinner.setPosition(0, 1, 0);
const stillness = new SceneNode();
stillness.updateWorld();
const camera = new Camera();
camera.position[0] = 6;
camera.position[1] = 4.5;
camera.position[2] = 8;
camera.lookAt(0, 1, 0);
/*
* How fast the cube turns is a rule, and rules live in DriftScript: `spin.drs`. The bundler compiles
* the file, `loadModule` makes a module of it, and `bindModule` gives it the engine capabilities it
* imports. Saving the file while the page is open replaces its function in place.
*/
const spinModule = loadModule(spinScript as Record<string, unknown>);
const bound = bindModule(spinModule, {});
if (!bound.bound) throw new Error(bound.reason);
const rules = spinModule.exports as unknown as { spinRate(time: number): number };
if (import.meta.hot) {
import.meta.hot.accept('./spin.drs', (next) => {
if (next !== undefined) patchModule(spinModule, next as Record<string, unknown>);
});
}
/*
* Two angles, because that is what `alpha` is for. The simulation advances in fixed steps and
* the display does not, so a frame almost never lands on a step boundary: drawing `spin`
* directly judders at any refresh rate that is not a multiple of the step. Interpolating
* between the last two states is the whole reason the loop hands `alpha` over.
*/
let spin = 0;
let previousSpin = 0;
let time = 0;
startLoop({
simulate(dt) {
previousSpin = spin;
time += dt;
spin += dt * rules.spinRate(time);
},
render(alpha) {
spinner.setRotationAxisAngle(0, 1, 0, previousSpin + (spin - previousSpin) * alpha);
spinner.updateWorld();
camera.updateMatrices(canvas.height > 0 ? canvas.width / canvas.height : 1);
renderer.beginFrame(CLEAR);
renderer.bindMeshPass(camera, ENV);
renderer.drawMesh(ground, stillness.worldMatrix);
renderer.drawMesh(cube, spinner.worldMatrix);
renderer.endFrame();
},
});
How fast the cube turns is a rule, so it lives in DriftScript, in spin.drs beside the page:
// How the cube turns. The page owns the cube; this file only says how fast it goes.
//
// Save it while the page is open and the cube takes the new rate without the page reloading. Try a
// steady 2, a cube that stops every few seconds, or one that turns the other way.
import { sin } from "std/math"
// Radians a second: a steady turn that quickens and eases every few seconds.
fn spinRate(time: f32) -> f32 {
return 0.8 + 0.4 * math.sin(time * 0.5)
}
The bundler compiles it with DriftScript's Vite plugin, set up as in the starter's vite.config.ts;
Installation has the two packages and
the config.
This is the engine's examples/starter, and a test keeps the two the same.
Hello world walks through it, and
Your first game builds a complete one.
What is in it
| The frame | A fixed-step loop with interpolation, two backends behind one API, construction-time quality profiles, and a frame whose order you can read. |
| Drawing | Meshes, materials, instancing, lights and shadows, the sky, fog and weather, water, reflections, post-processing, particles, text and passes of your own. |
| Light at scale | DriftLight for thousands of fixed lights, DriftRay for bounced light traced every frame, DriftTR for temporal reconstruction, and the GPU-driven pipeline. |
| Many things | Instanced batches that cull, crowds animated from bone textures in the vertex stage (createBoneAnimation), and static draws recorded once and replayed every frame (createStaticDraws). |
| Textures | Images and compressed blocks through one call: BC for desktops, ETC2, EAC and ASTC for phones, each uploaded as it is where compressedFormats says the device samples it. updateSurfaceTexture can replace an image with blocks behind the handle a draw already holds. |
| Worlds | Worlds bigger than a float, streamed in cells, with hierarchical detail and worlds built from a kit. |
| Physics | Deterministic rigid bodies, joints, queries, characters, vehicles, and ragdolls and cloth. |
| Play | Input from every device through one action map, cameras that cut, picking and the scene graph. |
| Around the game | Saves and preferences through a store the game supplies, a host seam for windows, displays and files, and the visual-gate scripts — @driftengine/core/scripts/browser.mjs and its siblings — that photograph a page on the hardware GPU. |
The other packages
Each is added when a game reaches for it, and a game that never imports one carries none of it.
| Package | What it adds |
|---|---|
@driftengine/animation | Skeletons, clips, blend trees and IK. Guide |
@driftengine/nav | Navigation meshes and routes. Guide |
@driftengine/entities | Entities, components, systems and prefabs, with rewind. Guide |
@driftengine/chemistry | Heat, burning, phase changes and smoke. Guide |
@driftengine/ai | Behaviour trees and agents. Guide |
@driftengine/audio | A mix of buses, placed sounds, rooms and rhythm. Guide |
@driftengine/assets and @driftengine/drft | Model importers, KTX2, and the streaming .drft container. Guide |
@driftengine/terrain | Terrain drawn as a clipmap. Guide |
@driftengine/splats | Gaussian splat captures among meshes. Guide |
@driftengine/texture | Materials as programs decoded on the device. Guide |
@driftengine/capture | A video clip turned into a surface, a collider and entities. Guide |
@driftengine/ui2d | Sprites, tilemaps and interfaces. Guide |
@driftengine/script | DriftScript, the engine's scripting language, with hot reload. Reference |
@driftengine/network | Lockstep and rollback netcode. Reference |
@driftengine/xr | WebXR sessions. Reference |
@driftengine/media | Recording and exporting clips. Reference |
@driftengine/editor and @driftengine/tools | The editor and in-game tools. Reference |
@driftengine/package and @driftengine/native-host | Installable apps built from the same game. Reference |
Licence
Apache-2.0. Distributing it means reproducing its NOTICE, which every bundle of it already
carries. Part of DriftEngine; see
docs/ARCHITECTURE.md for
how the packages divide.