A technical specification for building 3D browser games entirely through LLM code generation, without external binary assets.
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.
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)
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>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 blocksSphereGeometry- characters, projectiles, decorationsCylinderGeometry- pillars, trees, pipesConeGeometry- roofs, trees, arrowsTorusGeometry- rings, donuts, abstract shapesPlaneGeometry- ground, walls, UI elementsCapsuleGeometry- characters, pills, rounded shapesLatheGeometry- vases, bottles, rotationally symmetric objectsExtrudeGeometry- 2D shapes extruded to 3D (use with THREE.Shape)
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);
}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 })
);
}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);
}// 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
});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);
}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);
}
`
});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 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;
}
}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();For LLM-generated games, embrace styles that work well with procedural generation:
- Low-poly / Flat-shaded - Use
flatShading: trueon materials - Geometric / Abstract - Lean into primitives as an aesthetic choice
- Voxel - Build everything from cubes
- Neon / Glowing - Dark backgrounds with emissive materials
- Wireframe - Everything rendered as wireframe
- SDF Raymarched - Smooth, blobby, demoscene aesthetic
When asking an LLM to build or modify 3D games:
- Be specific about visual style - "low-poly", "neon wireframe", "pastel voxels"
- Describe interactions clearly - "player shoots projectiles toward mouse cursor"
- Reference the patterns - "use primitive composition for the enemies"
- Request complete files - "output a single HTML file I can run directly"
- Iterate in small steps - "add a scoring system" → "add particle effects when scoring"
| 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 |
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.