AgentGames

Search titles, descriptions, models and tags. Press Enter to see results.

🧊 Guide

How to make a 3D game with AI

3D is not much harder to prompt than 2D. It fails differently: instead of a game that plays badly, you get a black screen and no error, and almost always for one of four reasons.

Updated 8 September 2026 10 min read By the Agent Games team
Jump to a section
  1. Why three.js and not something else
  2. The API drift problem, and how to defeat it
  3. The smallest scene that draws anything
  4. Why AI 3D looks grey, and the four lines that fix it
  5. Cameras and movement
  6. Collision without a physics engine
  7. Where 3D models come from when you have no artist
  8. Keeping the frame rate up
  9. A complete 3D prompt
  10. Will it run on a phone?
  11. Common questions
  12. Where to go next

Ask for one HTML file using three.js, loaded from a CDN through an import map with the version pinned. That single instruction prevents the most common failure in AI-generated 3D, which is a file that loads, throws nothing useful, and shows a black rectangle.

The second instruction that matters is four render settings. Without them a three.js scene looks like a university assignment from 2014, and models will not add them unless asked.

The black screen is almost always one of four things

The import map is missing or its version does not match the addons path. The code uses an API removed years ago. There is no light in the scene. Or the camera is inside the object it is meant to be looking at. In that order of likelihood.

Why three.js and not something else#

For a browser game written by a model into one file, three.js is the right target. The reason is boring but decisive. There is more three.js in training data than every other browser 3D library combined. A model writing three.js is recalling; a model writing Babylon or PlayCanvas is more often reconstructing.

OptionGood forWhy not here
three.jsAlmost every browser 3D game. Loads from a CDN, works in one file, enormous training corpus.Nothing. It is the default for this.
Babylon.jsBigger projects that want a physics engine and an editor included.Larger API surface, less of it memorised accurately, and models mix versions more often.
React Three FiberReact apps that need a 3D view.Needs a build step, which loses the double-click-and-it-runs property that makes iteration fast.
Raw WebGL or WebGPUCustom renderers and shader work.Hundreds of lines before anything appears on screen. Nothing to gain at this scale.
Unity or Godot WebGL exportReal games with a team and a pipeline.The model cannot see the editor, and the output is not a file you can read or fix by hand.

The API drift problem, and how to defeat it#

three.js changes fast, and it removes things. A model's memory of it is an average over many years of code, so a first build routinely mixes current calls with ones deleted several years ago. This is the difference between 3D and 2D: the Canvas 2D API has barely changed in a decade, so there is nothing to get out of date.

What models still writeWhat it is nowHow it fails
new THREE.Geometry()THREE.BufferGeometryThrows immediately. The easy case, because you get a real error.
renderer.outputEncoding = THREE.sRGBEncodingrenderer.outputColorSpace = THREE.SRGBColorSpaceSilent. Colours come out washed out or too dark and nothing tells you why.
renderer.physicallyCorrectLightsRemoved. Lighting is physically based by default.Silent. The property is ignored and lighting intensities look wrong.
import ... from 'three/examples/jsm/...''three/addons/...' via the import mapModule resolution error, and the whole script never runs. This is the black screen.
new THREE.WebGLRenderer({ antialias: true }) with no pixel ratio capStill valid, but cap the ratioWorks, then runs at fifteen frames per second on a phone.

The silent ones are the expensive ones. A thrown error gets fixed in one message; washed-out colour gets blamed on the art.

The instruction that fixes most of this

Put this in the first prompt: Use the current three.js API. Do not use THREE.Geometry, outputEncoding, or physicallyCorrectLights, all of which have been removed. Load three.js from a CDN with an import map, pinning one exact version, and use the same version for the three/addons/ path.

htmlThe import map that makes the rest work
<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
  }
}
</script>

<script type="module">
  import * as THREE from 'three';
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  // ... your game
</script>
Both entries must be the same version

If three and three/addons/ point at different releases you load two copies of the library. Objects made by one are not recognised by the other, and the failure looks like nothing rendering rather than like a version problem. The version above is an example: check the current release and pin that.

The smallest scene that draws anything#

Five objects. A renderer that owns the canvas, a scene that holds things, a camera to look through, at least one light, and something to look at.

Everything a three.js scene needs, and nothing it does not WebGLRenderer owns the <canvas> Scene a list of things in space Camera Perspective, 60 to 75 fov. Move this, not the world, unless the world is small. Lights One directional for the sun, one hemisphere for the sky. Two is usually enough. Mesh Geometry (the shape) plus Material (the surface). Reuse both across copies. renderer.render(scene, camera) runs once per frame
If nothing appears, walk this graph. A missing light and a camera positioned inside the mesh both produce exactly the same symptom, which is a black screen with no error in the console.
javascriptA scene, sized correctly, with a resize handler and a clocked loop
import * as THREE from 'three';

const renderer = new THREE.WebGLRenderer({ antialias: true });
// Capped at 2. A 3x phone renders nine times the pixels for a difference
// nobody can see, and it is the single biggest cause of slow 3D on mobile.
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(65, innerWidth / innerHeight, 0.1, 500);
camera.position.set(0, 4, 9);
camera.lookAt(0, 1, 0);

// MeshStandardMaterial needs light. MeshBasicMaterial does not, which is why
// switching to Basic is the fastest way to test whether "nothing renders" is
// really "nothing is lit".
const sun = new THREE.DirectionalLight(0xffffff, 2.2);
sun.position.set(6, 12, 5);
scene.add(sun);
scene.add(new THREE.HemisphereLight(0x99bbff, 0x334422, 1.1));

const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1.6, 1.6, 1.6),
  new THREE.MeshStandardMaterial({ color: 0xef6c00, roughness: 0.55 })
);
cube.position.y = 1;
scene.add(cube);

addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();   // forgetting this stretches everything
  renderer.setSize(innerWidth, innerHeight);
});

// Clock gives you seconds since the last frame, same as dt in a 2D game.
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
  const dt = Math.min(clock.getDelta(), 0.05);
  cube.rotation.y += dt * 1.2;
  renderer.render(scene, camera);
});
Use setAnimationLoop, not requestAnimationFrame

It does the same thing, pauses correctly when the tab is hidden, and is the only version that works if you ever put the game in a headset. There is no reason to write the manual version in three.js.

Why AI 3D looks grey, and the four lines that fix it#

A first build almost always renders correctly and looks terrible. Flat grey shapes floating on black with no shadow, no horizon and no sense of scale. This is not an art problem. It is four settings.

The four lines between a grey demo and something that looks made Default settings Flat, chalky, shadowless Four settings later Lit, grounded, with air in it renderer.toneMapping = THREE.ACESFilmicToneMapping stops bright surfaces clipping to white renderer.shadowMap.enabled = true gives every object a floor to stand on scene.fog = new THREE.Fog(sky, 20, 160) puts distance between near and far scene.background = a colour, never black black reads as broken, not as space
Same geometry, same lights, same code. The difference is tone mapping, a shadow map, fog, and a background colour that is not black.
javascriptThe pass that changes how everything looks
// 1. Tone mapping. Without it, anything bright clips to flat white and the
//    whole image reads as cheap.
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;

// 2. Shadows. An object with no shadow is an object floating in a void; the
//    shadow is what tells the eye where the ground is.
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
// The default shadow camera is a 10-unit box. Anything outside it has no
// shadow at all, which is why "shadows work but only near the middle".
sun.shadow.camera.left = -40;
sun.shadow.camera.right = 40;
sun.shadow.camera.top = 40;
sun.shadow.camera.bottom = -40;
sun.shadow.camera.far = 120;
sun.shadow.bias = -0.0005;          // removes the stripes on lit surfaces
cube.castShadow = true;
ground.receiveShadow = true;

// 3. Fog, matched to the background. Gives distance and hides where the
//    world stops, which means you can build a much smaller world.
const SKY = 0x9ec7e8;
scene.background = new THREE.Color(SKY);
scene.fog = new THREE.Fog(SKY, 25, 160);

// 4. An environment, so metal and smooth surfaces have something to reflect.
//    Three lines, and it does more for the look than another light would.
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
The shadow camera is the one nobody mentions

A directional light's shadow covers a small box around the origin by default. Everything outside it renders with no shadow, so a player walking away from the centre of the level watches their own shadow disappear. It looks like a bug in the game rather than a setting.

Two more that cost nothing. Never use pure black as a background, because it reads as broken rather than as space; a very dark blue reads as night. And give the ground a texture or a subtle grid. A perfectly flat single-colour plane gives the eye nothing to judge speed against, so the game feels like it is not moving.

🚀Space games made with AI

Playable now, no account needed. Each page lists the model and the prompt behind the game.
View all ›

Cameras and movement#

Pick one of three, and say which in the prompt. Left unspecified you usually get orbit controls bolted onto a game that needed a follow camera.

CameraUse forThe thing to ask for
OrbitPuzzle games, builders, anything where the player inspects a scene rather than inhabiting it.OrbitControls from three/addons, with damping enabled and the pan limits set.
Third person followPlatformers, racing, most action games. The most forgiving to control.A camera that lerps toward a point behind and above the player, never one parented to the player.
First personShooters, exploration, horror.PointerLockControls, plus an explicit click-to-start overlay, because pointer lock cannot be requested without a user gesture.
javascriptA follow camera that does not induce motion sickness
const camOffset = new THREE.Vector3(0, 5.5, 10);
const camTarget  = new THREE.Vector3();
const camDesired = new THREE.Vector3();

function updateCamera(dt) {
  // Where the camera would like to be: behind and above the player, rotated
  // with the player so it swings around as they turn.
  camDesired.copy(camOffset).applyQuaternion(player.quaternion).add(player.position);

  // Frame-rate independent smoothing. The naive lerp(0.1) version is faster
  // on a 120Hz screen than a 60Hz one, which is a subtle and horrible bug.
  const k = 1 - Math.pow(0.001, dt);
  camera.position.lerp(camDesired, k);

  camTarget.copy(player.position).add(new THREE.Vector3(0, 1.5, 0));
  camera.lookAt(camTarget);
}
Never parent the camera to the player

It is the obvious thing to do and it makes the game unplayable. Every small rotation of the player becomes an instant rotation of the whole view. A camera that lags behind by a fraction of a second is what makes third-person movement readable.

Collision without a physics engine#

Most 3D browser games do not need a physics engine, and adding one is a large amount of complexity for a model to get subtly wrong. Two techniques cover the majority of cases.

A ray downward, for standing on things#

javascriptGround detection and step-up in about ten lines
const down = new THREE.Vector3(0, -1, 0);
const ray  = new THREE.Raycaster();
ray.far = 4;

function groundHeightUnder(pos) {
  // Start above the player so the ray cannot begin inside the floor.
  ray.set(new THREE.Vector3(pos.x, pos.y + 2, pos.z), down);
  const hit = ray.intersectObjects(collidables, false)[0];
  return hit ? hit.point.y : null;
}

function updatePlayer(dt) {
  player.velocityY -= 24 * dt;                     // gravity
  player.position.y += player.velocityY * dt;

  const g = groundHeightUnder(player.position);
  if (g !== null && player.position.y <= g + 0.9) {
    player.position.y = g + 0.9;                   // 0.9 = capsule half-height
    player.velocityY = 0;
    onGround = true;
  } else {
    onGround = false;
  }
}

Spheres and boxes, for everything else#

Treat every object as a sphere and compare squared distances, exactly as in 2D with one extra axis. For static level geometry, three.js gives you Box3 and Box3.intersectsBox, which is fast and needs no maths from you.

When you do need a real engine

Stacking, ragdolls, vehicles, or anything where objects push each other around. Then use Rapier, which compiles to WebAssembly and is fast. Ask for it explicitly and pin its version too, because it has the same drift problem three.js does.

Where 3D models come from when you have no artist#

A model will invent asset URLs, confidently

Ask for a spaceship model and you may get loader.load('/models/ship.glb') pointing at a file that has never existed. The scene renders empty and there is no error, only a failed request in the network tab. Say no external asset files in the prompt unless you are supplying the assets yourself.

Build them out of primitives#

This is the right default and it goes much further than people expect. A spaceship is a cone, two boxes and a cylinder in a THREE.Group. A tree is a cylinder and two cones. A low-polygon look built from primitives is coherent, it costs nothing, and it never fails to load.

PromptAsking for procedural models
Build all 3D models procedurally from three.js primitives grouped into
THREE.Group objects. No external model files, no texture files, no asset URLs
of any kind.

Write one factory function per object type, for example makeShip(), makeRock(),
makeTurret(). Each returns a Group. Vary size, proportion and colour from
arguments so a scene of twenty rocks has twenty different rocks from one
function.

Use a low-polygon flat-shaded look: MeshStandardMaterial with flatShading
true, a small palette of five colours defined once at the top, and no
textures.

Flat shading plus a small fixed palette is what makes procedural primitives look deliberate rather than like placeholder geometry.

Free model libraries, if you want real assets#

If you do want to supply assets, use glTF or GLB, which is the format three.js loads natively. Kenney and Quaternius both publish large game-asset packs in the public domain, and Poly Pizza aggregates low-polygon models. Sketchfab has far more but its licences vary per model, so check each one rather than assuming.

Whatever the source, download the file and host it yourself. Hotlinking someone else's asset means your game breaks when they reorganise their site.

Generating models with AI#

Text-to-3D tools now produce usable game-ready meshes, and they are genuinely good for hero objects such as a single ship or a character. They are not yet good for a whole level, and the output usually needs its polygon count reduced and its origin re-centred before it behaves in a game. For a first project, primitives will get you further per hour spent.

Keeping the frame rate up#

3D on the web is fast until it suddenly is not, and the cliff is almost always the number of separate things being drawn rather than the number of triangles. A phone will happily draw a million triangles and choke on two thousand small objects.

Do thisWhyRoughly worth
Cap the pixel ratio at 2A 3x phone renders nine times the pixels of a 1x one for no visible gain.The single biggest win on mobile
Use InstancedMesh for repeated objectsTwo hundred identical rocks become one draw call instead of two hundred.Large, above about fifty copies
Share geometries and materialsTwo thousand meshes made from one geometry cost far less than two thousand geometries.Large, and free
Shadow map at 1024 or 20484096 is four times the cost of 2048 and almost never visibly better.Noticeable on mobile
Turn antialias off on small screensBarely visible at high pixel density, and it is not cheap.Moderate on mobile
Keep the fog and camera far plane close togetherNothing gets drawn that fog would have hidden anyway.Moderate
Never allocate in the loopA new Vector3 per object per frame gives the collector work sixty times a second, which shows up as stutter.Removes hitching
Ask for the draw call count on screen

renderer.info.render.calls is the number that matters, and putting it in a corner during development tells you instantly whether a change helped. Aim to stay in the low hundreds for a game that should run on a phone.

A complete 3D prompt#

This puts everything above into one message. It is long, and that is the point: nearly every line of it exists because leaving it out produced a specific fault.

PromptThird-person 3D collector, complete first prompt
Build a complete 3D browser game as ONE self-contained .html file.

SETUP
- Load three.js from a CDN using an <script type="importmap">, pinning one
  exact version, and use that same version for the "three/addons/" path.
- Use the current three.js API. Do NOT use THREE.Geometry, outputEncoding,
  or physicallyCorrectLights; all have been removed.
- No external model, texture or audio files. No asset URLs of any kind.

GAME
Third-person. The player drives a small hovering craft around an island,
collecting twenty glowing crystals before a 120-second timer runs out.
Floating mines drift around; hitting one costs five seconds.

CONTROLS
WASD or arrows to steer, shift to boost. On touch, a left thumbstick to
steer and a right-side button to boost. Show the touch controls only when a
touch input is detected.

CAMERA
Third-person follow, positioned behind and above the craft, smoothed toward
its target with frame-rate-independent damping. Do NOT parent the camera to
the player.

WORLD
Island built procedurally from primitives: a low-polygon terrain, scattered
rocks and trees from factory functions, water as a large flat plane. Use
InstancedMesh for the rocks and trees. Flat shading, a fixed six-colour
palette defined once at the top.

LOOK
- ACESFilmicToneMapping, exposure 1.15
- Shadow map enabled, PCFSoftShadowMap, 2048, with the directional light's
  shadow camera bounds widened to cover the whole island
- scene.background and scene.fog both set to the same sky colour
- RoomEnvironment via PMREMGenerator for reflections
- Never pure black anywhere

TECHNICAL
- renderer.setAnimationLoop, with delta time in seconds clamped to 0.05
- renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
- Resize handler that updates camera.aspect AND calls
  camera.updateProjectionMatrix()
- Ground contact by raycasting downward; collision with rocks and mines by
  sphere distance. No physics engine.
- Allocate no vectors inside the animation loop; reuse module-level scratch
  vectors.
- Show renderer.info.render.calls in the corner.
- Best time in localStorage under "crystals.best".
- All tunable numbers in one CONFIG object at the top.

Return the complete file and nothing else.

If the result is a black screen, check in this order: the console for a module resolution error, then whether there is a light, then where the camera is. That covers nearly every case.

🏆The most played games on Agent Games

Worth opening a few to see where the bar sits before you start.
View all ›

Will it run on a phone?#

Yes, and better than most people assume. Every current phone runs WebGL well. Two things bite. The first is thermal throttling: a game running at 60fps for a minute settles at 40 once the device warms up. The second is that a phone renders far more pixels than a laptop unless you cap the pixel ratio.

  • Test on a real device. A narrow browser window tells you nothing about GPU load or heat.
  • Budget for the throttled frame rate, not the first-minute one. If it is at 60 when cold and 35 when warm, it is a 35fps game.
  • Design for portrait or lock to landscape deliberately. A 3D game that assumes a wide viewport and gets a tall one usually ends up with the camera looking at nothing.
  • Remember the address bar. Mobile browsers change the viewport height as it hides and shows, so a resize handler that assumes a fixed height will letterbox strangely.

Common questions#

Why is my three.js scene just a black screen?

Four causes, in order of likelihood. The import map is missing or its two entries point at different versions, so the module never loads. The code uses an API removed years ago, such as THREE.Geometry. There is no light in the scene, and MeshStandardMaterial renders black without one. Or the camera is positioned inside the object it is supposed to be looking at. Switching a material to MeshBasicMaterial tells you instantly whether the problem is lighting.

Can AI make 3D models, or only the code?

Both, but the reliable answer for a first project is neither: have it build models procedurally from primitives grouped together. Text-to-3D tools produce usable meshes for a single hero object and are worth it there, though the output usually needs decimating and re-centring. What a model must not do is reference an asset URL, because it will invent one that does not exist and the scene will render empty with no error.

Do I need a physics engine for a 3D game?

Usually not. A downward raycast for ground contact and sphere-distance checks between objects cover most action, racing and collecting games. Add Rapier when you need objects to stack, push each other or behave as vehicles, and pin its version the same way you pin three.js.

Why does my 3D game look flat and grey?

Four settings are missing. ACES filmic tone mapping, an enabled shadow map with the directional light shadow camera widened to cover the level, fog matched to the background colour, and an environment map for reflections. Together they take about ten lines. They change the result more than any amount of extra geometry would.

How many objects can a browser 3D game handle?

Draw calls matter far more than triangles. A phone will draw a million triangles comfortably and struggle with two thousand separate meshes. Merge static geometry, use InstancedMesh for anything repeated, share geometries and materials, and watch renderer.info.render.calls. Staying in the low hundreds keeps a game comfortable on mobile.

Is three.js or Babylon.js better for AI-generated games?

three.js, for one practical reason: there is far more of it in training data, so models recall it rather than reconstruct it. Babylon is a good library and includes more out of the box, but a first build in it more often mixes API versions, and that costs you passes.

Why do shadows only appear near the middle of my level?

A directional light's shadow camera defaults to a small box around the origin, and anything outside it is rendered with no shadow at all. Widen shadow.camera.left, right, top and bottom to cover your level and set shadow.camera.far past its furthest point.

Where to go next#

The fundamentals that apply to both dimensions, including the brief and the iteration loop, are in how to make a game with AI. Ten complete prompts, mostly 2D but built on the same structure, are in the prompt guide.

To see what other people's models produced, the space games and made-with pages list real games with the prompt behind each one. When yours works, publish it.