3D Framework
Four modules that wrap Three.js into the same DN pattern as the 2D core: a game3DLoop that mirrors gameLoop.js, a scene3D that mirrors scene.js, and camera/lighting utilities. The architecture principle: game logic stays pure (positions, velocities, state) and Three.js is just a renderer reading that state.
Setup — Three.js via CDN
No bundler. Load Three.js via an ES module importmap, then load the DN 3D modules as regular scripts.
<!-- In <head> — importmap must come before any module scripts -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.165.0/examples/jsm/"
}
}
</script>
<!-- DN 3D modules (load after importmap) -->
<script src="/js/dn/3d/scene3D.js"></script>
<script src="/js/dn/3d/camera3D.js"></script>
<script src="/js/dn/3d/lighting3D.js"></script>
<script src="/js/dn/3d/game3DLoop.js"></script>
<script type="module" src="game.js"></script>
Note: game.js must be type="module" to use Three.js imports.
Coordinate System
Y-up, 1 unit = 1 meter. This matches Decentraland’s coordinate convention — deliberate, not accidental. A terrain GLB exported from terrainGen3D.js loads directly into a DCL scene via GltfContainer without conversion. Underground areas are at negative Y; the world surface sits at Y=0.
scene3D.js
Wraps Three.js Scene, WebGLRenderer, and canvas setup.
Constructor
const scn = new Scene3D({
canvas: document.getElementById('game-canvas'),
backgroundColor: 0x1c1c1e, // hex color
antialias: true,
pixelRatio: window.devicePixelRatio,
shadows: false, // enable if scene needs shadow maps
});
Properties + Methods
| Description | |
|---|---|
scn.scene | The underlying THREE.Scene |
scn.renderer | The underlying THREE.WebGLRenderer |
scn.render(camera) | Render one frame. Call from the game loop’s render callback. |
scn.handleResize() | Update renderer size + camera aspect. Hook to window.addEventListener('resize', ...). |
camera3D.js
Three standard camera configurations.
Constructor
const cam = new Camera3D({
fov: 60,
aspect: canvas.width / canvas.height,
near: 0.1,
far: 1000,
});
Modes
// Top-down — looking straight down at the play area
cam.setTopDown({ height: 15, zOffset: 0 });
// Third-person — orbiting behind/above the player
cam.setThirdPerson({ distance: 8, height: 5, angle: 0 });
// Follow — lock to a position each frame
cam.follow(mesh.position, { offsetY: 10, offsetZ: 5 });
Properties
cam.camera | The underlying THREE.PerspectiveCamera |
cam.updateAspect(w, h) | Call after canvas resize. |
lighting3D.js
Three preset lighting configurations tuned for the DN low-poly aesthetic.
const lights = new Lighting3D(scene);
lights.setOutdoor(); // DirectionalLight (sun) + soft AmbientLight — open worlds
lights.setCave(); // dim AmbientLight + a few PointLights — interiors, dungeons
lights.setNeutral(); // balanced ambient-only — prototyping, UI scenes
Custom Lights
lights.addPointLight({
color: 0xffa060,
intensity: 1.2,
position: { x: 0, y: 3, z: 0 },
distance: 10,
decay: 2,
});
game3DLoop.js
Fixed-dt game loop for 3D — identical pattern to gameLoop.js.
const loop = new Game3DLoop({
fps: 60,
updateCallback: (dt) => { /* physics, input, state — no Three.js here */ },
renderCallback: () => { scn.render(cam.camera); },
});
loop.start();
// loop.stop();
// loop.renderOnce(cam.camera); // render a single frame without starting the loop
dt is always 1/fps seconds. Keep all game state updates in updateCallback and all Three.js calls in renderCallback.
Minimal 3D Game Template
// Separate game logic (pure) from Three.js rendering
let ballX = 0, ballVelX = 2;
let ballMesh;
function init() {
const geo = new THREE.SphereGeometry(0.5);
const mat = new THREE.MeshLambertMaterial({ color: 0x4bb8d5 });
ballMesh = new THREE.Mesh(geo, mat);
scn.scene.add(ballMesh);
}
function update(dt) {
// Pure logic — no Three.js
ballX += ballVelX * dt;
if (Math.abs(ballX) > 5) ballVelX *= -1;
}
function render() {
// Sync Three.js state from game state
ballMesh.position.x = ballX;
scn.render(cam.camera);
}
init();
const loop = new Game3DLoop({ fps: 60, updateCallback: update, renderCallback: render });
loop.start();
Low-Poly Aesthetic
DN 3D games use MeshLambertMaterial (no specular, fast) or MeshToonMaterial (flat-shaded cartoon). Typical poly budget: 200–500 polys per object. Avoid MeshStandardMaterial (PBR overhead isn’t worth it for this aesthetic).