Shapes and rendering
threeplay draws with three.js: WebGPU where the browser has it, WebGL2 otherwise. You describe what things look like with a few components and the engine takes care of the rest.
import threeplay from "threeplay";
threeplay({ width: 960, height: 540, background: "#0b132b" });setFog({ color: "#0b132b", near: 10, far: 40 });sun.intensity = 1.2; // the default sun; see "Lighting"
// A floor that is a little shinyadd([plane(60), color("#3a506b"), material({ roughness: 0.35, metalness: 0.3 })]);
// Different looks for the same shapeadd([sphere(1), pos(-4, 1, 0), color("tomato")]);add([sphere(1), pos(-1.3, 1, 0), color("gold"), material({ type: "toon" })]);add([sphere(1), pos(1.3, 1, 0), color("skyblue"), material({ wireframe: true })]);add([box(1.6), pos(4, 0.8, 0), color("#9d4edd"), material({ emissive: "#9d4edd", emissiveIntensity: 0.6 })]);
// A glowing orb that carries a point light aroundconst orb = add([ sphere(0.2), pos(0, 2, 3), color("orange"), material({ type: "basic" }), // unlit: it always looks fully bright pointLight({ color: "orange", intensity: 60 }),]);onUpdate(() => (orb.pos.x = wave(-5, 5, time() * 1.5)));
camPos(0, 5, 12);camLookAt(vec3(0, 1, 0));Primitive shapes
Section titled “Primitive shapes”All shapes are centered on the object’s origin, in meters.
| Component | Defaults | Notes |
|---|---|---|
box(width, height, depth) |
1, same as width, same as width |
box(2) is a cube. |
sphere(radius) |
0.5 |
|
capsule(radius, height) |
0.5, 2 |
height is the total height, including the round caps. |
cylinder(radius, height) |
0.5, 1 |
|
cone(radius, height) |
0.5, 1 |
The tip points up. |
plane(width, depth) |
10, same as width |
A flat rectangle on the XZ plane, facing up. Handy for floors. |
mesh(geometry, material?) |
Any THREE.BufferGeometry and, optionally, a THREE.Material. |
Every primitive takes an optional last argument with these options:
sphere(1, { segments: 48, castShadow: false, receiveShadow: true });castShadow/receiveShadow: bothtrueby default.segments: detail of round shapes (24 by default).
A shape is a THREE.Mesh that is a child of the object’s node, so scale and rotation apply to it. You can reach it with obj.mesh, and obj.shape holds a plain description ({ kind: "box", width, height, depth }) that physics reads to build colliders. The geometry and the default material are disposed when the object is destroyed (but not the ones you pass to mesh()).
A shape component has the id "shape", so an object has one shape. Loaded models (model()) use the same id.
// `THREE` is the three.js namespace the engine uses (`k.THREE` without global mode).add([mesh(new THREE.TorusKnotGeometry(1, 0.3, 128, 16), new THREE.MeshNormalMaterial()), pos(0, 2, 0)]);Color and opacity
Section titled “Color and opacity”color() accepts any CSS name, hex string or number, or 0–255 channels:
color("tomato");color("#ff7800");color(0xff7800);color(255, 120, 0);color([255, 120, 0]);obj.color is a THREE.Color shared with the material, so both of these change the object’s color on screen:
ball.color.set("white");ball.color = "crimson"; // the setter copies the valueopacity(0.5) makes the object translucent. obj.opacity goes from 0 to 1 and can be animated; the object only becomes transparent for the renderer while the value is below 1.
const ghost = add([sphere(1), pos(0, 1, 0), color("white"), opacity(0.4)]);ghost.opacity = 0.8;ghost.fadeOut(1, true); // fade to 0 in one second, then destroy the objectMaterials
Section titled “Materials”Shapes get a standard (PBR) material, light gray, roughness 0.8. Add material() to change it:
material({ type: "standard", roughness: 0.2, metalness: 0.8 });material({ type: "toon" }); // cel shadingmaterial({ type: "basic" }); // unlitmaterial({ wireframe: true });material({ flatShading: true }); // faceted lookmaterial({ emissive: "red", emissiveIntensity: 2 });material({ map: "grass", repeat: 10 }); // a loaded texture, tiled 10x10material(new THREE.MeshPhysicalMaterial({ clearcoat: 1 })); // your own material| Option | Meaning |
|---|---|
type |
"standard" (default), "basic", "toon", "lambert", "phong". |
roughness, metalness |
For "standard" only (defaults 0.8 and 0). |
emissive, emissiveIntensity |
Self-illumination. |
wireframe, flatShading |
Rendering style. |
map |
The name of a texture loaded with loadTexture(), or a THREE.Texture. |
repeat |
How many times the texture repeats, a number or [x, y]. |
color() still works with material(): the color multiplies the texture. Load textures like this (details in Assets and audio):
loadTexture("grass", "grass.png");scene("main", () => add([plane(50), material({ map: "grass", repeat: 10 })]));A texture given by name may finish loading after the object was created; it is applied as soon as it is available. repeat clones the texture, so the one you loaded is never modified.
Renderer and canvas options
Section titled “Renderer and canvas options”All of these go in threeplay({ ... }).
| Option | Default | Meaning |
|---|---|---|
width, height |
fill the page | Fixed logical size. HUD coordinates and the camera aspect use it, and the canvas is scaled to fit. Give both or neither. |
letterbox |
true with a fixed size |
Keep the aspect ratio with bars. With false the game stretches to fill the container. |
background |
"#9fd3ff" (sky blue) |
Clear color; null keeps the canvas transparent. |
renderer |
"auto" |
"auto" (WebGPU, falling back to WebGL2), "webgpu", "webgl", or "none" (headless, see Debugging). |
pixelRatio |
min(devicePixelRatio, 2) |
Cap of the device pixel ratio. Lower it for speed on high-density screens. |
antialias |
true |
Antialiasing. |
shadows |
true |
Shadow maps. false turns them off everywhere. |
lighting |
"default" |
"default" adds the sun and ambient light; "none" adds nothing. |
canvas |
created for you | Draw into your own <canvas>. |
root |
document.body |
Element that receives the canvas and HUD. Anything but the body makes the game fit inside that element. |
Embedding the game in a part of a page:
threeplay({ root: document.querySelector("#game"), width: 800, height: 450 });The container needs a size (CSS width and height). With the default root, the game fills the whole window and body gets margin: 0; overflow: hidden.
width(), height() and center() return the logical size and its center; onResize(fn) runs when the size or scale changes.
Background and fog
Section titled “Background and fog”setBackground("#10002b");setBackground(null); // transparent canvassetFog({ color: "#10002b", near: 5, far: 30 }); // linear fogsetFog({ density: 0.04 }); // exponential fog (color defaults to the background)setFog(null); // no fogBackground and fog are not reset by go(): set them at the start of each scene if scenes differ.
Lighting
Section titled “Lighting”By default threeplay lights the scene with a warm sun (a DirectionalLight that casts shadows) and a hemisphere ambient light. Both are exposed so you can tune them:
sun.intensity = 1.5;sun.color.set("#ffd6a5");ambient.intensity = 0.6;The area around the camera is the one that gets shadows (a box about 40 m wide that follows the camera), so shadows stay sharp as you move around. That follow logic rewrites the sun’s position and target every frame, so re-aim it with setSunDirection instead of moving it. The argument is the direction the light travels:
setSunDirection({ x: -1, y: -2, z: -1 }); // from above, low on the +X/+Z sideFor your own lights set lighting: "none" (then sun and ambient are null) or just add more. Lights are components, so they hang from an object and move with it:
| Component | Defaults |
|---|---|
pointLight({ color, intensity, distance, decay, castShadow }) |
intensity 20, distance 0 (no limit), decay 2 |
spotLight({ color, intensity, angle, distance, penumbra, castShadow }) |
intensity 100, angle 45 degrees, penumbra 0.3 |
dirLight({ color, intensity, castShadow }) |
intensity 2 |
castShadow is false by default for lights (shadows are expensive). Spot and directional lights shine along the object’s forward direction (−Z), so rotate the object to aim them:
const lamp = add([pos(0, 6, 0), rotate(-90, 0, 0), spotLight({ color: "white", angle: 30, castShadow: true })]);lamp.light.intensity = 150; // `obj.light` is the THREE.LightLight intensities use three.js’ physically based units: a point light of intensity 20 is a modest lamp; increase it for bigger scenes.
Screen projection
Section titled “Screen projection”toScreen(worldPoint) converts a 3D point to logical screen coordinates (null if it is behind the camera). screenRay(screenPoint) returns the ray through a screen point, which is useful for aiming.
const p = toScreen(enemy.worldPos());if (p) debug.log(`enemy is at ${p.x | 0}, ${p.y | 0} on screen`);
const ray = screenRay(center()); // through the middle of the screenTo attach a label to a 3D object you don’t need to project by hand: a text() without fixed() floats over its position. See HUD.
Screenshots
Section titled “Screenshots”screenshot() returns a PNG data URL of the current frame (an empty string when headless):
onKeyPress("f2", () => { const a = document.createElement("a"); a.href = screenshot(); a.download = "screenshot.png"; a.click();});Escape hatches
Section titled “Escape hatches”threeplay never hides three.js. Everything is one property away:
| What | How |
|---|---|
| The three.js namespace the engine uses | THREE (global mode) or k.THREE |
| The three.js scene | k.scene3d |
The camera (a THREE.PerspectiveCamera) |
k.camera3d |
The raw renderer (WebGPURenderer or WebGLRenderer) |
k.renderer.three (null until the renderer finished loading) |
| Which backend is running | k.renderer.kind ("webgpu", "webgl" or "none") |
| The node of a game object | obj.object3d |
| The mesh of a shape | obj.mesh |
| The canvas | k.canvas |
// Add post-processing, tweak the renderer, or draw any three.js thing you like.scene3d.add(new THREE.GridHelper(40, 40));onLoad(() => { if (renderer.three) renderer.three.toneMappingExposure = 1.2;});