3d/

3D Framework

Three.js-based 3D modules — scene, camera, lighting, and game loop. Y-up / 1m coordinates matching Decentraland.

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.sceneThe underlying THREE.Scene
scn.rendererThe 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.cameraThe 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).