Specimens,
opened up
Every Gaitkit specimen is a small JavaScript module that builds a three.js group. This page covers the format, the knobs, how things move, and how to take them into your engine. 518 specimens in 51 kits follow it, checked by the same validator on every change.
01Quick start
Paste this into an HTML file and serve it. It loads a cottage with brick walls and a porch from the CDN.
<script type="importmap">
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js" } }
</script>
<script type="module">
import * as THREE from 'three';
import { createAsset } from 'https://gaitkit.com/cdn/cottage.mjs';
const scene = new THREE.Scene();
scene.add(new THREE.HemisphereLight('#ffffff', '#8a8f80', 2.5));
const cottage = createAsset({ walls: 'brick', porch: true }); // every knob is optional
scene.add(cottage); // rests on y = 0, front faces +Z
</script>Not sure which knob does what? Open the specimen in the lab: it shows every knob, and the code tab writes the createAsset() call for what you see. Agents get the same through MCP.
02What a specimen is
One ES module per specimen, with four exports. No mesh is drawn by hand; GLB and OBJ files are built from the module.
export const meta = {
id: 'spider-bot', name: 'Spider Bot', class: 'mech', kit: 'Mech Hangar', legs: 8,
tags: ['spider', 'robot', 'arachnid', '8 legs', 'sci-fi', 'drone'],
describe: 'A knee-high eight-legged robot spider with a glowing eye cluster. Walks with a wave gait
and keeps its feet planted on uneven ground.'
};
export const params = {
colorway: { type: 'choice', options: ['steel', 'hazard', 'toxic', 'ivory'], default: 'steel',
describe: 'Named palette for body, legs and eyes: steel (grey/red), hazard (black/yellow), toxic (green), ivory (white/blue).' },
legLength: { type: 'range', min: 0.75, max: 1.4, step: 0.05, default: 1,
describe: 'Scales both leg segments. Higher = taller, longer stride, more spindly.' },
eyes: { type: 'choice', options: [2, 4, 8], default: 4, describe: 'Number of glowing eye lenses on the head.' },
turret: { type: 'toggle', default: false, describe: 'Mounts a small dorsal sensor turret on the abdomen.' }
};
export const presets = { 'lab drone': { colorway: 'ivory', turret: true } };
export const night = { eye: { zone: 'eye', intensity: 1.2, describe: 'Eye lenses glow after dark.' } };
export function createAsset(params = {}) { /* builds and returns a THREE.Group */ }meta: id, name, class (creature, mech, vehicle, building, nature, prop, character), kit, tags, leg count, and one sentence an agent can act on.params: the knobs. Every one has adescribewritten for people and agents.presets: named knob combinations, one click in the lab.night: which parts glow after dark (optional). A zone without a colour glows in the part's own colour.createAsset(params): builds the group. With no arguments it always builds the same published look.
03Knobs and presets
| Type | Fields | Used for |
|---|---|---|
choice | options, default | Named looks or parts: a colourway, a roof type, a weapon. The describe text can explain each option in brackets, and the matcher learns those words. |
range | min, max, step, default | Sizes, lengths and counts. A step of 1 makes it a count ("3 floors"). |
toggle | default | A part that is there or not: a porch, a horn, a turret. |
color | default as #rrggbb | Any colour. Colour words like "dark green" map to it. |
Pass only the knobs you want to change; the rest keep their defaults, and unknown keys are ignored. Stay inside each range: the validator only checks the published bounds. Each specimen's schema is also published as /cdn/{id}-params.json.
04Scale, orientation, budget
- Y-up, metres, real-world scale. A door is about 2 m, a car about 4 m, a beetle a few centimetres unless a knob says giant.
- Rests on y = 0, centred on x and z. Front faces +Z; for walkers, front is the direction of travel.
- One material: flat-shaded vertex colours, one draw call after merging. GLB exports split it into one material per colour for engines that prefer that.
- At most 6,000 triangles, so whole kits stay light enough for phones and Quest.
- Deterministic. Randomness comes from a seeded generator, never
Math.random; aseedknob gives you new rocks or trees on demand.
05Walkers
Creatures, mechs and characters with meta.legs > 0 walk with two-bone IK. Each leg is a named chain leg_i > hip_i > knee_i > foot_i, and userData.gait holds stride, duty factor, lift, phases and foot home positions.
import { createAsset } from 'https://gaitkit.com/cdn/spider-bot.mjs';
import { walker, raycastGround } from 'https://gaitkit.com/runtime/walk.mjs';
const spider = createAsset({ legLength: 1.25, eyes: 8 });
scene.add(spider);
const gait = walker(spider, { ground: raycastGround([floorMesh]) }); // omit ground for a flat floor
let dist = 0;
renderer.setAnimationLoop(() => {
dist += speed * clock.getDelta(); // metres
spider.position.z = dist; // move the model yourself...
gait.poseAt(dist); // ...and pose the legs for that distance
renderer.render(scene, camera);
});The pose is a pure function of distance travelled, not time. That is why a planted foot stays planted at any frame rate, in a scrub bar or in an offline render; the validator rejects any specimen whose planted foot slides more than 1 mm.
Knees bend up (insects, spiders, crabs), forward (a mammal's front leg) or back (hocks, birds, reverse-jointed mechs). To re-implement the IK in another engine, use the pole vector (0, 0, ±1) in body space and the per-leg knee field from get_asset().gait.segments.
06Wheels, wings and hinges
Vehicles
const car = createAsset(); // meta.motion === 'drive'
// each frame
dist += speed * dt;
car.position.z = dist;
car.userData.tick(dist); // wheels turn exactly dist / radiusFlyers and swimmers
The model still rests on y = 0; userData.altitude says how high to hold it.
const bird = createAsset(); // meta.motion === 'fly' | 'swim'
// each frame
dist += speed * dt;
bird.position.set(x, groundY + bird.userData.altitude, dist);
bird.userData.tick(dist); // wings beat (or the tail waves) with the distanceHinged and sliding parts
Doors, lids, drawers, turrets and hatches are named groups with their pivot on the hinge, listed in userData.rig as rotate, slide or spin with an axis and a range.
const chest = createAsset();
// userData.rig = { lid: { type: 'rotate', axis: 'x', range: [-110, 0] } }
const lid = chest.getObjectByName('lid'); // pivot sits on the hinge
lid.rotation.x = THREE.MathUtils.degToRad(-80);07Night lights
Specimens with windows, lamps, eyes or engines export night: each entry names a zone, and every mesh whose own name or a parent's name is that zone (or starts with zone_) glows in its colour. Add your own point lights where you want real light; the zones only set what looks lit.
import { night } from 'https://gaitkit.com/cdn/street-lamp.mjs';
const zones = Object.values(night); // [{ zone: 'lamp', color: '#ffd38a', intensity: 1.4 }]
lamp.traverse(o => {
if (!o.isMesh) return;
const z = zones.find(n => [o, ...ancestors(o)].some(a => a.name === n.zone || a.name.startsWith(n.zone + '_')));
if (!z) return;
o.material = o.material.clone();
o.material.emissive.set(z.color);
o.material.emissiveIntensity = z.intensity ?? 1;
});08Engines
The module is the asset: import it from the CDN or copy it with runtime/ next to it. Map the bare three import with an import map or your bundler; r150 and later work.
import { createAsset } from 'https://gaitkit.com/cdn/horn-beetle.mjs';
const beetle = createAsset({ shell: 'scarlet' });
scene.add(beetle);import { useMemo, useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import { createAsset } from 'https://gaitkit.com/cdn/horn-beetle.mjs';
import { walker } from 'https://gaitkit.com/runtime/walk.mjs';
export function Beetle({ shell = 'scarlet', speed = 0.4 }) {
const model = useMemo(() => createAsset({ shell }), [shell]);
const gait = useMemo(() => walker(model), [model]);
const dist = useRef(0);
useFrame((_, dt) => { dist.current += speed * dt; model.position.z = dist.current; gait.poseAt(dist.current); });
return <primitive object={model} />;
}Drop the GLB into your project. Walkers carry an in-place Walk clip that covers gait.clip_speed metres per second at speed 1; vehicles, flyers, swimmers and animated props carry a Loop clip. Moving parts are separate nodes with their pivot on the hinge or axle.
@onready var anim: AnimationPlayer = $Beetle/AnimationPlayer
func _physics_process(delta):
velocity = transform.basis.z * speed
move_and_slide()
anim.play("Walk")
anim.speed_scale = speed / clip_speed # gait.clip_speed from get_asset(), in m/sImport the GLB with glTFast. The Walk or Loop clip becomes a legacy or Mecanim animation; characters carry a Humanoid bone map (see below) so retargeting works. FBX export is on the roadmap.
animator.Play("Walk");
animator.speed = speed / clipSpeed; // gait.clip_speed from get_asset(), in m/sFile → Import → glTF 2.0. Colours arrive as one material per colour, clips as actions, rigged parts as named empties at their pivots. Use the OBJ only for static geometry (it carries no colour yet).
09Characters
People and humanoids are built on one skeleton with a style knob (realistic, stylized, chibi, blocky). Their parts are rigid (no skin weights), and the GLB's root node carries a Humanoid bone map in extras.humanoid: Hips → body, Spine → spine, Head → head, LeftUpperLeg → hip_0, LeftHand → hand_L and so on, 21 bones in all. Unity's Humanoid avatar, Godot's SkeletonProfileHumanoid and Blender rigs use the same names, so you can retarget onto them.
10Files
| File | What is in it |
|---|---|
/cdn/{id}.mjs | The module itself, with every knob. Imports three and /runtime/kit.mjs. |
/cdn/{id}.glb | The default look, one material per colour, with a Walk or Loop clip when it moves. |
/cdn/{id}.obj | Static geometry, no colour yet. |
/cdn/{id}-params.json | Knob schema, presets and night zones. |
/catalog.json | Every specimen's metadata: size, palette, knobs, gait, rigged parts, file links. |
/runtime/walk.mjs | walker() and raycastGround(), free to ship with your project. |
Downloads from the lab bake the knobs you set into the GLB. Use of all files is covered by the licence.