@thai-kit/level-runtime
v0.2.5
Published
Loads a level baked by the thaikit level editor into a three.js game: cells with LOD, lights and lightmap, physics colliders, per-cell BVH. Headless entry for servers.
Maintainers
Readme
@thai-kit/level-runtime
Load a baked thaikit level into a
three.js game. One GLB is the whole map — geometry, LOD tiers, lightmaps, sky,
lights, colliders and spawn points — and loadLevel() is the only call you make.
npm i @thai-kit/level-runtime threeimport * as THREE from 'three';
import { loadLevel } from '@thai-kit/level-runtime';
const level = await loadLevel('/levels/soi.glb', {
scene, renderer, camera,
transcoderPath: '/basis/', // where you serve basis_transcoder.{js,wasm}
});
const spawn = level.spawns.pick();
camera.position.fromArray(spawn.position);
renderer.setAnimationLoop(() => {
level.update(clock.getDelta(), camera.position);
renderer.render(scene, camera);
});level.update() steps physics, switches LOD tiers by distance to each cell's
box, keeps the moon's shadow frustum snapped around the camera, re-faces
billboards and drifts the sky. loadLevel adds the level root to your scene
itself, builds the sky as domes, and sets scene.environment from a PMREM probe
prefiltered off the sky's own shader.
What you must not do
The level arrives already lit. Do not add an ambient or hemisphere light, and
do not add a sun — the bake put sky light and bounce in the lightmap, and the
manifest's directional light comes back with its shadow settings intact. Adding
yours on top double-counts the bake. Set camera.far to at least 2000; the sky
domes sit at radius 1200. And as with three itself: two copies of three and
nothing draws.
Full integration guide, including migrating a game that loads one GLB per prop:
docs/using-a-baked-level.md.
Physics is your choice
Colliders ship as plain data — boxes, cylinders, spheres and capsules in metres —
so no engine is baked in. A Rapier adapter is included
(@thai-kit/level-runtime/physics/rapier), or implement four methods on
PhysicsAdapter, or use none: level.raycast(ray) is a three-mesh-bvh query
against real lod0 triangles and works regardless.
Ladders are data too. A placement tagged ladder in the editor (or labelled
ladder_* in Unreal) ships its collider entry with tags: ['ladder'], and
level.colliders.ladders lists them as { placement, shapes, tags, bounds }:
the shapes are still part of the static solid (a ladder is stood on as well as
climbed) and bounds is a world AABB around them grown by 0.35 m of reach, so
"the player is at a ladder" is a box test. Test the box against the CAPSULE, not
its axis alone: a player pressed against the rungs has their axis one player
radius plus the controller's skin off the face, which is just outside a box
padded by reach only -- grow it by the radius in XZ as well, or pass a larger
reach to ladderVolumes(manifest, { reach }). The runtime does not move the
player; your controller decides what a grab, a climb and a boost are. A level
baked before tags were carried lists none.
Headless
@thai-kit/level-runtime/node never imports three. A game server reads the same
GLB and builds the same bodies from the same manifest:
import { loadLevelHeadless } from '@thai-kit/level-runtime/node';
const level = await loadLevelHeadless('./level.glb', { physics });This is why three is an optional peer dependency: the browser entries all
require it, the /node entry genuinely does not.
MIT.
Levels built in Unreal
A level laid out in Unreal Editor loads through the same loadLevel() with no
option and no second code path. The route is: export the Unreal level with
Unreal's own glTF Exporter (metres, lights, cameras, baked materials), run
npm run level:import-unreal -- --level <id> to turn that file into the bake
pipeline's raw scene, and bake it with npm run level:bake -- --level <id>
--baker unreal (adopting Unreal 5.6+'s exported lightmaps) or --baker blender
(re-lighting the same geometry and lamps in Cycles). The result is an ordinary
baked level: cells, LOD tiers, one KTX2 lightmap, colliders rebuilt from the
thaikit compounds wherever a SM_TK_* Static Mesh was placed, spawns from the
spawn_* cameras. manifest.source says it came from Unreal and which lightmap
it carries; everything else is identical, which is the point. The whole
procedure is docs/unreal-level-export.md in the thaikit repo.
Scenery without baked lighting
Schema-3 levels can contain cells marked bakeLighting: false. These retain
spatial LOD switching but receive no lightmap binding and cast no shadows.
Their authored live or unlit materials remain active. Distant tree billboards
are individual nodes that face the camera during level.update().
The source runtime supports manifest schemas 1, 2 and 3. A game loading a new schema-3 level must use a runtime release containing this support; previously published runtimes may reject that level.
