Skip to content

Instantly share code, notes, and snippets.

@a-r-d
Created January 1, 2026 16:32
Show Gist options
  • Select an option

  • Save a-r-d/dffbcc350d21d1a6f90b236a8d3d1366 to your computer and use it in GitHub Desktop.

Select an option

Save a-r-d/dffbcc350d21d1a6f90b236a8d3d1366 to your computer and use it in GitHub Desktop.
3d browser game bootstrap docs

LLM-Driven 3D Browser Game Development Guide

A technical specification for building 3D browser games entirely through LLM code generation, without external binary assets.


Core Philosophy

Traditional 3D game development relies on binary assets (meshes, textures, audio files) created in specialized tools. LLMs cannot produce these directly. This guide defines an architecture where everything is code - geometry, materials, audio, and game logic all exist as generatable text.


Technology Stack

Primary Framework: Three.js

three.js (r160+)
├── Core rendering and scene management
├── Procedural geometry primitives
├── BufferGeometry for custom meshes
├── ShaderMaterial for custom GLSL
└── Built-in post-processing

Why Three.js over alternatives:

  • Largest training corpus in LLM datasets
  • Minimal boilerplate, direct WebGL access when needed
  • Procedural geometry well-documented
  • Single-file deployable (CDN import)

Deployment Target: Single HTML File

All code should compile to a single .html file with inline JavaScript. This maximizes portability and simplifies the generation/iteration loop.

<!DOCTYPE html>
<html>
<head>
  <style>
    * { margin: 0; padding: 0; }
    canvas { display: block; }
  </style>
</head>
<body>
  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
    }
  }
  </script>
  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    
    // Game code here
  </script>
</body>
</html>

Geometry Generation Strategies

Strategy 1: Primitive Composition

Build complex objects from simple primitives. This is the most reliable approach.

function createTree(height = 5) {
  const group = new THREE.Group();
  
  // Trunk
  const trunkGeo = new THREE.CylinderGeometry(0.3, 0.5, height * 0.4, 8);
  const trunkMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
  const trunk = new THREE.Mesh(trunkGeo, trunkMat);
  trunk.position.y = height * 0.2;
  group.add(trunk);
  
  // Foliage layers (stacked cones)
  const foliageMat = new THREE.MeshStandardMaterial({ color: 0x228B22 });
  for (let i = 0; i < 3; i++) {
    const radius = 1.5 - i * 0.3;
    const coneHeight = height * 0.3;
    const coneGeo = new THREE.ConeGeometry(radius, coneHeight, 8);
    const cone = new THREE.Mesh(coneGeo, foliageMat);
    cone.position.y = height * 0.4 + i * coneHeight * 0.6;
    group.add(cone);
  }
  
  return group;
}

Available Primitives:

  • BoxGeometry - buildings, crates, terrain blocks
  • SphereGeometry - characters, projectiles, decorations
  • CylinderGeometry - pillars, trees, pipes
  • ConeGeometry - roofs, trees, arrows
  • TorusGeometry - rings, donuts, abstract shapes
  • PlaneGeometry - ground, walls, UI elements
  • CapsuleGeometry - characters, pills, rounded shapes
  • LatheGeometry - vases, bottles, rotationally symmetric objects
  • ExtrudeGeometry - 2D shapes extruded to 3D (use with THREE.Shape)

Strategy 2: Custom BufferGeometry

For unique shapes, define vertices directly.

function createCrystal() {
  const vertices = new Float32Array([
    // Top pyramid
     0,  2,  0,   // apex
    -1,  0, -1,
     1,  0, -1,
     
     0,  2,  0,
     1,  0, -1,
     1,  0,  1,
     
     0,  2,  0,
     1,  0,  1,
    -1,  0,  1,
     
     0,  2,  0,
    -1,  0,  1,
    -1,  0, -1,
    
    // Bottom pyramid (inverted)
     0, -1,  0,   // bottom apex
     1,  0, -1,
    -1,  0, -1,
    
     0, -1,  0,
     1,  0,  1,
     1,  0, -1,
     
     0, -1,  0,
    -1,  0,  1,
     1,  0,  1,
     
     0, -1,  0,
    -1,  0, -1,
    -1,  0,  1,
  ]);
  
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
  geometry.computeVertexNormals();
  
  const material = new THREE.MeshStandardMaterial({ 
    color: 0x9966ff,
    flatShading: true,
    transparent: true,
    opacity: 0.8
  });
  
  return new THREE.Mesh(geometry, material);
}

Strategy 3: 2D Shape Extrusion

Define 2D paths and extrude them.

function createStar(points = 5, innerRadius = 0.5, outerRadius = 1, depth = 0.3) {
  const shape = new THREE.Shape();
  
  for (let i = 0; i < points * 2; i++) {
    const radius = i % 2 === 0 ? outerRadius : innerRadius;
    const angle = (i / (points * 2)) * Math.PI * 2 - Math.PI / 2;
    const x = Math.cos(angle) * radius;
    const y = Math.sin(angle) * radius;
    
    if (i === 0) shape.moveTo(x, y);
    else shape.lineTo(x, y);
  }
  shape.closePath();
  
  const extrudeSettings = { depth, bevelEnabled: false };
  const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
  
  return new THREE.Mesh(
    geometry,
    new THREE.MeshStandardMaterial({ color: 0xFFD700 })
  );
}

Strategy 4: Signed Distance Field Raymarching (Advanced)

For organic, abstract, or highly stylized visuals. The entire scene is defined in a fragment shader.

function createSDFScene() {
  const geometry = new THREE.PlaneGeometry(2, 2);
  
  const material = new THREE.ShaderMaterial({
    uniforms: {
      uTime: { value: 0 },
      uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
    },
    vertexShader: `
      void main() {
        gl_Position = vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      uniform float uTime;
      uniform vec2 uResolution;
      
      // SDF primitives
      float sdSphere(vec3 p, float r) { return length(p) - r; }
      float sdBox(vec3 p, vec3 b) {
        vec3 q = abs(p) - b;
        return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
      }
      
      // Smooth minimum for blending shapes
      float smin(float a, float b, float k) {
        float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
        return mix(b, a, h) - k * h * (1.0 - h);
      }
      
      // Scene definition
      float scene(vec3 p) {
        float sphere = sdSphere(p - vec3(sin(uTime), 0, 0), 0.5);
        float box = sdBox(p - vec3(0, -0.5, 0), vec3(1.0, 0.1, 1.0));
        return smin(sphere, box, 0.3);
      }
      
      // Raymarching
      void main() {
        vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution) / uResolution.y;
        
        vec3 ro = vec3(0, 1, 3);  // ray origin (camera)
        vec3 rd = normalize(vec3(uv, -1));  // ray direction
        
        float t = 0.0;
        for (int i = 0; i < 100; i++) {
          vec3 p = ro + rd * t;
          float d = scene(p);
          if (d < 0.001 || t > 100.0) break;
          t += d;
        }
        
        vec3 color = vec3(0.1);
        if (t < 100.0) {
          color = vec3(0.8, 0.4, 0.2) * (1.0 - t * 0.1);
        }
        
        gl_FragColor = vec4(color, 1.0);
      }
    `
  });
  
  return new THREE.Mesh(geometry, material);
}

Material Strategies

Code-Defined Materials (No Textures)

// Solid color with physically-based properties
const metal = new THREE.MeshStandardMaterial({
  color: 0x888888,
  metalness: 0.9,
  roughness: 0.1
});

// Glowing/emissive
const lava = new THREE.MeshStandardMaterial({
  color: 0xff4400,
  emissive: 0xff2200,
  emissiveIntensity: 0.5
});

// Transparent/glass
const glass = new THREE.MeshPhysicalMaterial({
  color: 0xffffff,
  transmission: 0.9,
  roughness: 0,
  thickness: 0.5
});

// Toon/cel-shaded
const toon = new THREE.MeshToonMaterial({
  color: 0x44aa88
});

// Wireframe
const wireframe = new THREE.MeshBasicMaterial({
  color: 0x00ff00,
  wireframe: true
});

Procedural Textures via Canvas

function createCheckerTexture(size = 512, squares = 8) {
  const canvas = document.createElement('canvas');
  canvas.width = canvas.height = size;
  const ctx = canvas.getContext('2d');
  
  const squareSize = size / squares;
  for (let y = 0; y < squares; y++) {
    for (let x = 0; x < squares; x++) {
      ctx.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000';
      ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
    }
  }
  
  const texture = new THREE.CanvasTexture(canvas);
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
  return texture;
}

function createNoiseTexture(size = 256) {
  const canvas = document.createElement('canvas');
  canvas.width = canvas.height = size;
  const ctx = canvas.getContext('2d');
  const imageData = ctx.createImageData(size, size);
  
  for (let i = 0; i < imageData.data.length; i += 4) {
    const value = Math.random() * 255;
    imageData.data[i] = value;
    imageData.data[i + 1] = value;
    imageData.data[i + 2] = value;
    imageData.data[i + 3] = 255;
  }
  
  ctx.putImageData(imageData, 0, 0);
  return new THREE.CanvasTexture(canvas);
}

Custom Shaders for Unique Effects

const customMaterial = new THREE.ShaderMaterial({
  uniforms: {
    uTime: { value: 0 },
    uColor1: { value: new THREE.Color(0x0066ff) },
    uColor2: { value: new THREE.Color(0xff0066) }
  },
  vertexShader: `
    varying vec2 vUv;
    varying vec3 vNormal;
    
    void main() {
      vUv = uv;
      vNormal = normal;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform float uTime;
    uniform vec3 uColor1;
    uniform vec3 uColor2;
    varying vec2 vUv;
    varying vec3 vNormal;
    
    void main() {
      float fresnel = pow(1.0 - dot(vNormal, vec3(0, 0, 1)), 2.0);
      float wave = sin(vUv.y * 10.0 + uTime * 2.0) * 0.5 + 0.5;
      vec3 color = mix(uColor1, uColor2, wave + fresnel);
      gl_FragColor = vec4(color, 1.0);
    }
  `
});

Audio Generation

Web Audio API Synthesis

class AudioManager {
  constructor() {
    this.ctx = new (window.AudioContext || window.webkitAudioContext)();
  }
  
  // Simple tone
  playTone(frequency = 440, duration = 0.2, type = 'sine') {
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    
    osc.type = type;  // 'sine', 'square', 'sawtooth', 'triangle'
    osc.frequency.value = frequency;
    
    gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
    
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    
    osc.start();
    osc.stop(this.ctx.currentTime + duration);
  }
  
  // Jump sound
  playJump() {
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    
    osc.type = 'square';
    osc.frequency.setValueAtTime(150, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(400, this.ctx.currentTime + 0.1);
    
    gain.gain.setValueAtTime(0.2, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.15);
    
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.15);
  }
  
  // Explosion
  playExplosion() {
    const bufferSize = this.ctx.sampleRate * 0.5;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const data = buffer.getChannelData(0);
    
    for (let i = 0; i < bufferSize; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.1));
    }
    
    const noise = this.ctx.createBufferSource();
    const filter = this.ctx.createBiquadFilter();
    const gain = this.ctx.createGain();
    
    noise.buffer = buffer;
    filter.type = 'lowpass';
    filter.frequency.setValueAtTime(1000, this.ctx.currentTime);
    filter.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.5);
    
    gain.gain.setValueAtTime(0.5, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
    
    noise.connect(filter);
    filter.connect(gain);
    gain.connect(this.ctx.destination);
    noise.start();
  }
  
  // Coin/pickup
  playCoin() {
    [880, 1108.73].forEach((freq, i) => {
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      
      osc.type = 'square';
      osc.frequency.value = freq;
      
      const startTime = this.ctx.currentTime + i * 0.08;
      gain.gain.setValueAtTime(0.15, startTime);
      gain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.1);
      
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start(startTime);
      osc.stop(startTime + 0.1);
    });
  }
}

Game Architecture Template

// ============================================================
// GAME CONFIGURATION
// ============================================================
const CONFIG = {
  debug: false,
  physics: {
    gravity: -20,
    friction: 0.9
  },
  player: {
    speed: 5,
    jumpForce: 10
  },
  world: {
    size: 50
  }
};

// ============================================================
// CORE ENGINE
// ============================================================
class Game {
  constructor() {
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.clock = new THREE.Clock();
    this.input = new InputManager();
    this.audio = new AudioManager();
    this.entities = [];
    
    this.init();
  }
  
  init() {
    // Renderer setup
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    this.renderer.shadowMap.enabled = true;
    document.body.appendChild(this.renderer.domElement);
    
    // Lighting
    const ambient = new THREE.AmbientLight(0x404040, 0.5);
    this.scene.add(ambient);
    
    const sun = new THREE.DirectionalLight(0xffffff, 1);
    sun.position.set(10, 20, 10);
    sun.castShadow = true;
    sun.shadow.mapSize.width = 2048;
    sun.shadow.mapSize.height = 2048;
    this.scene.add(sun);
    
    // Background
    this.scene.background = new THREE.Color(0x87CEEB);
    
    // Resize handler
    window.addEventListener('resize', () => {
      this.camera.aspect = window.innerWidth / window.innerHeight;
      this.camera.updateProjectionMatrix();
      this.renderer.setSize(window.innerWidth, window.innerHeight);
    });
    
    this.setup();
    this.animate();
  }
  
  setup() {
    // Override this method to set up your game
  }
  
  update(delta) {
    // Override this method for game logic
    this.entities.forEach(e => e.update?.(delta));
  }
  
  animate() {
    requestAnimationFrame(() => this.animate());
    const delta = this.clock.getDelta();
    this.update(delta);
    this.renderer.render(this.scene, this.camera);
  }
  
  addEntity(entity) {
    this.entities.push(entity);
    if (entity.mesh) this.scene.add(entity.mesh);
    return entity;
  }
}

// ============================================================
// INPUT MANAGER
// ============================================================
class InputManager {
  constructor() {
    this.keys = {};
    this.mouse = { x: 0, y: 0, buttons: {} };
    
    window.addEventListener('keydown', e => this.keys[e.code] = true);
    window.addEventListener('keyup', e => this.keys[e.code] = false);
    window.addEventListener('mousemove', e => {
      this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
      this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
    });
    window.addEventListener('mousedown', e => this.mouse.buttons[e.button] = true);
    window.addEventListener('mouseup', e => this.mouse.buttons[e.button] = false);
  }
  
  isPressed(key) { return !!this.keys[key]; }
  isMouseDown(button = 0) { return !!this.mouse.buttons[button]; }
}

// ============================================================
// BASE ENTITY CLASS
// ============================================================
class Entity {
  constructor(game) {
    this.game = game;
    this.mesh = null;
    this.velocity = new THREE.Vector3();
  }
  
  get position() { return this.mesh?.position; }
  
  update(delta) {
    // Override in subclass
  }
  
  destroy() {
    if (this.mesh) this.game.scene.remove(this.mesh);
    const idx = this.game.entities.indexOf(this);
    if (idx > -1) this.game.entities.splice(idx, 1);
  }
}

// ============================================================
// SIMPLE COLLISION DETECTION
// ============================================================
class Physics {
  static boxesIntersect(a, b) {
    const boxA = new THREE.Box3().setFromObject(a);
    const boxB = new THREE.Box3().setFromObject(b);
    return boxA.intersectsBox(boxB);
  }
  
  static spheresIntersect(posA, radiusA, posB, radiusB) {
    return posA.distanceTo(posB) < radiusA + radiusB;
  }
  
  static raycast(origin, direction, objects) {
    const raycaster = new THREE.Raycaster(origin, direction.normalize());
    return raycaster.intersectObjects(objects);
  }
}

// ============================================================
// UI OVERLAY
// ============================================================
class UI {
  constructor() {
    this.container = document.createElement('div');
    this.container.style.cssText = `
      position: fixed;
      top: 20px;
      left: 20px;
      color: white;
      font-family: monospace;
      font-size: 16px;
      text-shadow: 1px 1px 2px black;
      pointer-events: none;
    `;
    document.body.appendChild(this.container);
  }
  
  setText(text) {
    this.container.innerHTML = text;
  }
}

Example Game: Collect the Orbs

A complete, minimal game demonstrating the patterns above.

// Player entity
class Player extends Entity {
  constructor(game) {
    super(game);
    
    // Body
    const bodyGeo = new THREE.CapsuleGeometry(0.3, 0.6, 4, 8);
    const bodyMat = new THREE.MeshStandardMaterial({ color: 0x4488ff });
    this.mesh = new THREE.Mesh(bodyGeo, bodyMat);
    this.mesh.castShadow = true;
    this.mesh.position.y = 1;
    
    // Eyes
    const eyeGeo = new THREE.SphereGeometry(0.08);
    const eyeMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
    [-0.12, 0.12].forEach(x => {
      const eye = new THREE.Mesh(eyeGeo, eyeMat);
      eye.position.set(x, 0.35, 0.25);
      this.mesh.add(eye);
    });
    
    this.grounded = false;
  }
  
  update(delta) {
    const input = this.game.input;
    const speed = CONFIG.player.speed;
    
    // Movement
    if (input.isPressed('KeyW') || input.isPressed('ArrowUp')) this.velocity.z = -speed;
    else if (input.isPressed('KeyS') || input.isPressed('ArrowDown')) this.velocity.z = speed;
    else this.velocity.z *= CONFIG.physics.friction;
    
    if (input.isPressed('KeyA') || input.isPressed('ArrowLeft')) this.velocity.x = -speed;
    else if (input.isPressed('KeyD') || input.isPressed('ArrowRight')) this.velocity.x = speed;
    else this.velocity.x *= CONFIG.physics.friction;
    
    // Jump
    if ((input.isPressed('Space') || input.isPressed('KeyW')) && this.grounded) {
      this.velocity.y = CONFIG.player.jumpForce;
      this.grounded = false;
      this.game.audio.playJump();
    }
    
    // Gravity
    this.velocity.y += CONFIG.physics.gravity * delta;
    
    // Apply velocity
    this.mesh.position.add(this.velocity.clone().multiplyScalar(delta));
    
    // Ground collision
    if (this.mesh.position.y < 1) {
      this.mesh.position.y = 1;
      this.velocity.y = 0;
      this.grounded = true;
    }
    
    // World bounds
    const bound = CONFIG.world.size / 2;
    this.mesh.position.x = THREE.MathUtils.clamp(this.mesh.position.x, -bound, bound);
    this.mesh.position.z = THREE.MathUtils.clamp(this.mesh.position.z, -bound, bound);
  }
}

// Collectible orb
class Orb extends Entity {
  constructor(game, x, z) {
    super(game);
    
    const geo = new THREE.SphereGeometry(0.3, 16, 16);
    const mat = new THREE.MeshStandardMaterial({ 
      color: 0xffdd00, 
      emissive: 0xffaa00,
      emissiveIntensity: 0.3
    });
    this.mesh = new THREE.Mesh(geo, mat);
    this.mesh.position.set(x, 1.5, z);
    
    this.bobOffset = Math.random() * Math.PI * 2;
  }
  
  update(delta) {
    this.mesh.rotation.y += delta * 2;
    this.mesh.position.y = 1.5 + Math.sin(performance.now() * 0.003 + this.bobOffset) * 0.2;
  }
}

// Main game
class OrbGame extends Game {
  setup() {
    this.score = 0;
    this.ui = new UI();
    
    // Camera position
    this.camera.position.set(0, 10, 15);
    this.camera.lookAt(0, 0, 0);
    
    // Ground
    const groundGeo = new THREE.PlaneGeometry(CONFIG.world.size, CONFIG.world.size);
    const groundMat = new THREE.MeshStandardMaterial({ color: 0x44aa44 });
    const ground = new THREE.Mesh(groundGeo, groundMat);
    ground.rotation.x = -Math.PI / 2;
    ground.receiveShadow = true;
    this.scene.add(ground);
    
    // Player
    this.player = this.addEntity(new Player(this));
    
    // Spawn orbs
    for (let i = 0; i < 10; i++) {
      const x = (Math.random() - 0.5) * CONFIG.world.size * 0.8;
      const z = (Math.random() - 0.5) * CONFIG.world.size * 0.8;
      this.addEntity(new Orb(this, x, z));
    }
    
    this.updateUI();
  }
  
  update(delta) {
    super.update(delta);
    
    // Camera follow
    this.camera.position.x = this.player.mesh.position.x;
    this.camera.position.z = this.player.mesh.position.z + 15;
    this.camera.lookAt(this.player.mesh.position);
    
    // Check orb collisions
    this.entities.forEach(entity => {
      if (entity instanceof Orb) {
        if (Physics.spheresIntersect(
          this.player.mesh.position, 0.5,
          entity.mesh.position, 0.3
        )) {
          entity.destroy();
          this.score++;
          this.audio.playCoin();
          this.updateUI();
          
          // Spawn new orb
          const x = (Math.random() - 0.5) * CONFIG.world.size * 0.8;
          const z = (Math.random() - 0.5) * CONFIG.world.size * 0.8;
          this.addEntity(new Orb(this, x, z));
        }
      }
    });
  }
  
  updateUI() {
    this.ui.setText(`Score: ${this.score}<br>WASD to move, Space to jump`);
  }
}

// Start game
new OrbGame();

Visual Style Recommendations

For LLM-generated games, embrace styles that work well with procedural generation:

  1. Low-poly / Flat-shaded - Use flatShading: true on materials
  2. Geometric / Abstract - Lean into primitives as an aesthetic choice
  3. Voxel - Build everything from cubes
  4. Neon / Glowing - Dark backgrounds with emissive materials
  5. Wireframe - Everything rendered as wireframe
  6. SDF Raymarched - Smooth, blobby, demoscene aesthetic

Prompting Tips for LLMs

When asking an LLM to build or modify 3D games:

  1. Be specific about visual style - "low-poly", "neon wireframe", "pastel voxels"
  2. Describe interactions clearly - "player shoots projectiles toward mouse cursor"
  3. Reference the patterns - "use primitive composition for the enemies"
  4. Request complete files - "output a single HTML file I can run directly"
  5. Iterate in small steps - "add a scoring system" → "add particle effects when scoring"

Limitations & Workarounds

Limitation Workaround
No detailed textures Procedural materials, vertex colors, canvas textures
No complex character models Primitives composition, stylized/abstract characters
No skeletal animation Procedural animation, tween between poses
No pre-made sound effects Web Audio synthesis
No physics engine Simple AABB/sphere collision, basic kinematics

Further Enhancements

For more advanced games, consider:

  • Instanced rendering for many similar objects
  • Object pooling for projectiles/particles
  • Spatial hashing for efficient collision detection
  • Post-processing (bloom, SSAO) via Three.js EffectComposer
  • Procedural terrain via noise functions

This document is designed to be included as context when prompting an LLM to build 3D browser games.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment