read this before building a webgame // how to build fast efficient cheap
practical notes from building a Three.js browser game with AI coding agents, Blender, TripoAI, generated assets, compression, collision meshes, and too many broken systems
2 months ago while scrolling Twitter I bumped into the AI vibecoding contest organized by x.com/levelsio so I've dived into the most intense weeks of focus in my life. Even though I had experience with vibecoding sites and games this one had to be serious since who doesn't want a 20k?
Luckily at that period there were promotion with qwen 3.6 plus running for free so I had managed to bootstrap the progress fairly fast. I've first used meshy ai for my models (in the end went full tripoai) only meshy left assets are player character and frog + some obsolete ones
This article will be separated in sections
contents
- optimization
- collision and map in general
- animations and 3d models
- animation should not move the character
- track names lie
- physics
- blender mesh names are the contract between art and code
- terrain took longer than expected
- navmesh belongs in blender
- debug menu
- cannon mechanics: describe states, not vibes
- destruction sounded simple until GLB reality arrived
- move everything possible away from runtime and throttle useless updates
- cheap visuals beat expensive post-processing
- billboard effects is a SAVING
- fog is a performance feature
- visual and sound feedback
- audio is production not polish
- codex skills and gamedev plugins
- project context helps but stable rules matter more
- final advice
optimization
use this for as online .glb viewer: https://gltf-viewer.donmccurdy.com/
use this for compression and ask ai whats best for you particular model but at the end of the day draco compression must be checked always: https://glb.babylonpress.org/
use this to compress the HUD, or any textures you may use: https://squoosh.app/ - use the "reduce pallete" feature. what i noticed is that greatly reduces the filesize and doesnt affect the image. i personally dont see any difference between the 256 colors image and 125 one. cheese this feature. for .png transparent images dont forget to change compress method to "browser png", mostly the color pallete advice is aimed for .png images since compressing them in terms of quality doesnt bring result great enough as if you were to compress a jpeg. using .webp is also viable if you dont need transparency
brotli compression. is faster to download by about 30% on average
add this script during a build: https://gist.github.com/dusy4/7b744ab72cf7395486703f267d39984b (how?: https://docs.google.com/document/d/1jElzd5gBB6eXC_qcsQyI7MYhjSiQ2McAHfAREEGsMok/edit?tab=t.0)
browser game dies on loading size. optimize assets before inventing 50 postprocessing passes.
collision and map in general
for mapping use blender and export the map as .glb (also properly compressed via babylonpress, but dont overcompress since may get bugs, i had bugs that collision was disappearing time to time)
the pipeline is as follows: use the 3d assets you generated via tripo, use decimate modifier and dont forget to apply it, place in desired places - name it something like object_house then go to object mode and create a new mesh (cube), place it near the house and make it big, place many cubes to imitate the shape of the house, basically the cube is the actual collision box player will hit, either go into edit mode of that cube and place many other cubes in it or go into object mode and place them there and just then merge them via ctrl+j with initial cube. name then that mesh object_house_collision and tell the ai to remove the bvh collisiion from object_house and use the _collision as actual collision
visual mesh can be detailed and cursed. collision mesh should be boring.
physics doesn't care that collision is pretty. it cares that its simple and stable.
this is why ai generated models are bad for direct collision. they can be hollow, uneven, have terrible triangles, weird inner geometry. cubes dont.
this is also why Blender mesh names are important. the mesh name becomes the contract between Blender and the code.
animations and 3d models
generate the models via tripo3d.ai, dont use meshy.ai they have TERRIble triangles idk its too bad. tripo3d has deidcated web optimized generator where 5k triangles is enough for any character (one character) and any object model too (except the big hero ones that you will use in the game)
For animations use tripoai if you already have the models generate here but their library is very small so you either use meshy with terrible trimeshes and textures (tripo has smart meshes functions specifically made for web optimized 3d model generation) but rich animation library or use tripo. But I discovered that industry standard (obviously not for AAA projects) is the miximo. Even though I found it unreliable since it doesn't accept .glbs and only .fbx! So you have to convert it in blender, but why not online converter? Most online converters don't do the job right I had problems with them so blender is must here. Just import the .glb and immediately export it as .fbx after that go through miximo and download the animations
Yes, an .fbx file can have textures in it, but it doesn't by default. Most software programs reference external image files instead of including them. I used .glb to have everything in one file
Also problem I encountered is that it doesn't allow you to upload already rigged character. Rigged means is the model has it's bones assigned to movement and miximo uses different bones naming for example it could be called hip instead of hips but in most cases it's more complex so upload the unrigged character.
animation should not move the character
Animation should not move the character
This is the mistake that makes enemies moonwalk, rotate wrong, or slide across the floor.
In a game, physics should move the entity. The animation should move the bones.
Some animation clips contain root motion. That means the animation itself moves the root bone through space. This is fine if your whole system is built around root motion. Mine was not. There's option on tripo3dai for this that keeps the character animations anchored, choose it when exporting the character model
The mental model is simple: physics/controller owns world position, animation owns pose. If those two start doing the same job, everything becomes weird. Also add animation state display to debug menu, because sometimes you think movement is broken but actually the wrong animation track is playing. See track names lie.
track names lie
dont trust comments, filenames, or "track 4 should be this".
animation files can contain clips with names like NlaTrack, ArmatureAction, mixamo.com, Take 001, etc. sometimes code says track 0 is idle, track 1 is walk, track 2 is run, but actual .glb says something else.
before wiring animation states, dump the real animation clips: clip index, clip name, duration. then give it to AI and say use this actual mapping, don't infer from comments.
You can inspect the same file visually in Don McCurdy glTF Viewer: drop the .glb there, open the animations panel, and compare the visible clips with your dumped track names.
this is especially important for combat animations, hit reactions, sprint jumps, enemies, etc.
AI can write the logic but it can't magically know what is inside your binary model file unless you inspect it.
same thing as cannon mechanics: if AI can't see real data, it guesses.
physics
Create collision meshes manually (blender) it's the most fps efficient since game doesn't have to build a collision and calculate it based on visual mesh (bhv) since it's ai generated and rough edgy clunky uneven, its also better for the player since there lower chance he clips into the model. Even though I am still didn't perfect it and in my game you can fairly often get into collision it is way better than it was with the fully bhv based collision
For player use capsule collider, not box. Box catches on every micro edge. Capsule slides over imperfect geometry better.
And again: don't make collision too detailed. Collision should be "good gameplay shape", not exact visual shape.
See collision and map in general for the cube workflow.
blender mesh names are the contract between art and code
A mistake I almost made was trying to map everything manually from code.
Place object here. Save object there. Add debug gizmo. Save constants. Teleport to object. Move object. Scale object. Repeat forever.
Some of that is useful, especially for weapons, UI offsets, camera offsets, and small interaction tuning. But for world layout it becomes its own editor project.
The better workflow was to place the world in Blender and make mesh names meaningful.
If a mesh is called:
object_blacksmith_door
object_anvil
object_big_bell
terrain_plane
water.001
enemy_path
terrain_plane_navmesh
castle_gateway_gate
object_dungeon_doorway
then the engine can find it and attach behavior.
This made Blender the actual level editor. The game did not need to know manually that the blacksmith door is at some coordinate. Blender already knows. The engine just needs to parse names and register the matching system behavior.
This helped with:
- doors and knocking interactions
- blacksmith / anvil placement
- castle gate state
- dungeon doorway HP
- enemy path nodes
- navmesh surfaces
- water areas
- collision meshes
The advice:
Do not make code remember what Blender already knows.
Name meshes intentionally and let the engine read the map.
The engine side is basically a naming parser.
When loading the world GLB, traverse all meshes and check their names:
if name includes "_collision" -> register physics body and hide mesh
if name starts with "object_" -> register interactable object
if name starts with "enemy_path" -> collect as path node
if name includes "navmesh" -> use for enemy walking surface
if name is "castle_gateway_gate" -> attach gate system
terrain took longer than expected
I lost a couple of days on terrain.
The last time I used Blender seriously was maybe six or seven years ago. So I had to relearn basic things: generating terrain, sculpting it, texturing it, making it look intentional instead of like a melted blanket.
The final workflow was not fancy.
I had cliff meshes. I generated or refined textures with an image model using Blender screenshots. I honestly do not remember whether that part was Nano Banana or GPT image generation, but the surprising part was how well the image model understood the screenshot.
I could send a Blender scene screenshot and ask for a cliff texture or dark forest grass direction, and it understood where the texture would sit on the mesh. Then I refined contrast and brightness in Blender's shading tab.
Same with terrain: screenshot, generated texture direction, apply, check, refine brightness/contrast, repeat.
This is one of the AI workflows that actually felt useful:
Blender screenshot -> image model texture iteration -> Blender material tuning -> game export
This connects to cheap visuals, because good texture + fog can do more than expensive postprocessing.
navmesh belongs in blender
I tried runtime navmesh logic.
Bad idea.
The map was too irregular, and generated navmeshes were either slow or wrong. The better solution was to author a separate navmesh in Blender:
terrain_plane_navmesh
This mesh defines where enemies can walk. The game reads it at load time.
The same applies to enemy path nodes:
enemy_path
enemy_path.001
enemy_path.002
...
Make them real mesh objects, not curves or empties. Export them with the map. Let the game parse them.
The fun bug: enemies chased the player, then when chase ended they walked backward through the entire path because the AI returned to the first unvisited node.
The fix was to continue from the nearest unvisited node instead.
This is the kind of bug AI can fix, but only after you explain the actual game rule.
"Enemy pathfinding broken" is vague.
"After chase ends, choose nearest unvisited enemy_path node, not first remaining node" is fixable.
I built navmesh by going into edit mode while having the terrain mesh selected, then I manually chose the allowed to walk faces and copied them into a new mesh (P / separate by selection, I believe is the shortcut)
debug menu
Its your first priority, if youre adding a new feature ask ai to add a way to debug it. Keep it sane since keeping up with all events isn't possible and the debug menu will grow exponentially. Always add generic features like noclip, teleport to npc, model, mesh. If you need to regulate some positioning ask ai to expose a constant for it and wire the debug to save to constant so that for example you can position some items correctly in the game instead of vaguely saying the sword faces the wrong direction and is too small, even if you attach the screenshot it won't save you in most cases. Add fps, perf, player position and state display with animations currently in use (file.glb and track index + name) many models confuse index with the name since it can be that index 2 and the track is named nlatrack01.
Build the debug menu before the game gets big.
I am serious. Before polish. Before quests. Before adding ten enemies. Before adding crows.
My essential debug tools became:
- noclip / fly mode
- teleport to NPC/entity/object
- player coordinates always visible
- copy player position to clipboard
- animation state display
- FPS and frame spike graph
- god mode
- collision wireframe toggle
- entity labels with ID/type/HP/state/distance
- skeleton display for animation debugging
- camera position debug mode
- save gizmo transforms to constants
The last one is huge.
You cannot describe exact offsets to an AI forever:
move sword slightly down, rotate it a bit, no not that much, now forward, now it clips, now it is invisible during attack
This is hell.
Instead expose constants:
SWORD_HAND_OFFSETSWORD_HAND_ROTATIONCANNON_CAMERA_OFFSETGRAPPLE_AIM_CAMERA_OFFSETPADLO_HUD_POSITION
Then add a gizmo in-game that lets you move/rotate/scale the object visually and save the values back.
Do not prompt-position things. Tool-position them. The debug menu should not just show values. It should let you copy and reuse them.
For example, player position display is useful, but "copy player position" is much better because then you can paste exact coordinates into constants.
Same for object placement:
- Select object in debug menu.
- Show transform gizmo.
- Move/rotate/scale object visually.
- Press save.
- Write values into a constants/config file.
This changes the workflow from:
"AI, move it slightly left."
to:
"Use this exact saved offset."
That removes 80% of visual placement guessing.
cannon mechanics: describe states, not vibes
The cannon was the first mechanic that made the prototype feel like an actual siege game.
The useful lesson was not "add a cannon." The useful lesson was that a cannon is a state machine.
A bad request is:
make the cannon draggable and shoot
A better request is:
cannon state: wall
- locked to wall position
- aimable
- shootable
- not draggable
cannon state: ground
- draggable with G
- still shootable
- physics-aware
- H asks for confirmation and returns it to wall
This matters because the AI can easily implement one happy path and break the rest. My cannon system went through a lot of that. Dragging worked, then shooting broke. Shooting worked, then the muzzle vector was wrong. The cannonball physics started flying from the wrong point or in a direction that did not match the cannon/camera. At some point collision/damage logic was also split between "ball is flying" and "ball landed," which made hits feel inconsistent.
For any projectile mechanic, expose debug information immediately:
- muzzle position
- muzzle direction vector
- predicted trajectory
- cannon state
- cannonball velocity
- last hit object
- damage radius
- current drag/mount state
Without that, the cannon is impossible to reason about from screenshots. With it, you can tell the AI exactly which vector or state is wrong.
The broader advice:
Physics gameplay needs debug handles before polish.
Implementation detail that matters most is muzzle transform. Don't shoot from cannon object origin unless origin is actually at muzzle. Usually it's not.
Most cannon bugs were not "physics broken". They were transform bugs: wrong origin, wrong forward axis, wrong offset, wrong state.
destruction sounded simple until GLB reality arrived
The original fantasy was obvious:
Shoot a castle wall with a cannon. Make a hole. Walk through the hole.
That sounds like a game mechanic. It also sounds like something AI should be able to "just implement."
It was not.
The first idea was to cut holes into GLB models. But many GLB models are basically hollow visual shells. If you carve into them, you do not get a nice destructible wall. You get weird openings, missing insides, and surfaces that do not make sense.
Then there was the "fill around the hole" idea. If the wall is hollow, generate new geometry around the impact so it looks thick.
That also gets weird fast.
If the player shoots from different directions, the generated filling can appear in the air or in unnatural places. It starts looking less like destruction and more like scooping a chunk out of ice cream. Technically a hole appears, but it does not feel like a wall breaking.
Then I looked at Voronoi destruction.
The idea was better: have a normal object and a pre-fractured version. Cannon hits swap or break pieces. This is much more game-like.
But it requires asset discipline. You need clean models. You need prepared fracture pieces. You need collision that still makes sense.
Then my asset pipeline changed toward TripoAI models, and I did not want to make Voronoi versions again for every changing model. AI-generated meshes are also not always clean enough for this. They can have spacing, weird topology, and internal geometry. Voronoi makes that more cursed, not less.
So I cut the dream destruction system.
The shipped direction became authored destruction:
- cannonballs damage specific important objects
- dungeon doorway has HP
- gate can open/close
- trees can fall
- some objects swap state or disappear
- VFX sells the hit even when geometry is not truly fractured
This is less impressive in a tech demo.
It is much more reliable in an actual web game.
Real-time arbitrary destruction is a trap if your assets are AI-generated and your deadline is measured in days.
Example: object_dungeon_doorway has HP, cannon hit subtracts HP, when HP is 0 hide mesh, disable collision, play VFX/SFX, allow player through.
Trees: sword hit increments hit count, plays axe sound, spawns cheap wood/dust particles. After 3 sword hits or 1 cannon hit, disable collision, rotate tree down like falling, hide/remove after animation, play tree_falls.mp3.
move everything possible away from runtime and throttle useless updates
Move everything possible away from runtime. And throttle useless updates
Everything updating every frame
Chickens do not need 60 FPS AI.
The minimap does not need 60 FPS.
NPC idle checks do not need 60 FPS.
Throttle systems:
- chickens: every 3rd frame
- wizard/NPC checks: every 5th frame
- minimap: 24 FPS
- shadow budget: update every 0.5s
We don't need dynamic lighting during the runtime because the map is mostly static (depends on your game)
Bake EVERYTHING. It's fairly easy if you know how to use blender but the tradeoff is that you have to for example rebake the lighting and AO and everything after moving or adding new objects to your blender scene (game world.glb)
Ask: does this need to change during gameplay?
If no, bake it or author it in Blender.
If yes, keep it runtime. Player movement, enemy current state, cannonball physics, door open/closed, quest progress, damage numbers.
cheap visuals beat expensive post-processing
Cheap visuals beat expensive post-processing
I wanted the game to look like a moody, saturated fantasy forest — strong greens, warm rim light, fog, bloom, painterly contrast.
The first instinct was to enable the old post-processing stack.
Bad idea.
The stack had AO, bloom, color grading, radial blur, god rays, bokeh, and other expensive stuff. Turning it all on murdered FPS.
In the end I focused on mechanics first and kept the rendering cheaper.
The better visual strategy was:
- bake AO into the map/terrain texture in Blender
- remove N8AO completely
- keep simple color grading
- keep subtle bloom if it is affordable
- use fog to hide render distance
- avoid depth-texture-dependent shader tricks unless you really need them
- use billboard effects instead of 3D particle soup
Cheap consistent look > expensive unstable look.
billboard effects is a SAVING
Billboard effects is a SAVING. I have crow. I added crows to the game but initialy I wanted them to be a 3d model to be "realistic" but since it makes no sense for the webpage because it's not a main gameplay core loop requirement but just a misc asset that adds atmosphere I decided to use billboards
| Term | Definition | | --- | --- | | Billboard | A flat 2D plane with a texture that always rotates to face the camera, so it reads like a sprite/effect in 3D space without needing a full 3D model. |
For my crow, only 2 images were needed: crow_fly.png and crow_stand.png. If I had used a 3D model, I would have spent hours iterating through model import, animation, wing logic, and walking behavior for a background detail.
Fire should be a billboard
The fire hazard caused one of the worst freezes.
The first version spawned complex 3D fire on the character, plus particles, plus audio, plus camera shake. The moment the player entered fire, the game froze for seconds.
The fix was almost embarrassingly simple:
- preload fire audio
- use a flat
fire.jpgtexture - make it a billboard that faces the camera
- cut it into a flame shape in shader/UV/material
- use a cheap point light with no shadows, or no light at all if the texture sells it
- keep camera shake subtle
Same visual idea. Fraction of the cost.
A lot of game feel is fake. Fake is good.
The billboard implementation is usually:
- plane geometry
- transparent PNG/JPG texture
- always face camera
- optional animation by swapping texture frames
- optional tiny point light if needed
- no shadows
For crows:
standing crow = crow_stand.png
flying crow = crow_fly.png
fog is a performance feature
Fog is a performance feature
Fog is not just atmosphere.
Fog hides the cutoff.
If your camera far plane is 63 units and fog ends at 46, you have 17 units where objects just exist clearly and then pop out. That looks bad.
Make the fog end before the render cutoff so objects dissolve before culling.
This is one of the cheapest visual tricks in the whole project.
visual and sound feedback
Add visual and sound feedback for player actions. Player jumped? Shake camera add Hop sound add landing sound and landing shake. Player hit the enemy? Camera shake, sfx, animations
Mechanics feel broken when nothing responds. Jump without sound feels weak. Hit without SFX feels like enemy ignored it. Cannon without smoke/recoil feels like debug tool.
Keep it cheap: short sounds, small camera shake, pooled particles, billboard VFX, preloaded audio.
Don't make every action spawn expensive effect stack.
audio is production not polish
Music, sfx, npc dialogs did with elevenlabs
This helped because placeholder systems started feeling like actual game systems. Wizard quest feels different with voice. Cannon feels different with impact sound. Tree falling feels different with fall sound.
Workflow: generate voice / sfx / music, export short files, organize folders, preload common sounds, use one-shot playback for actions, use loops only for ambience, expose volume constants.
Example structure: audio/sfx/cannon_fire.mp3, audio/sfx/tree_falls.mp3, audio/sfx/axe_the_tree.mp3, audio/sfx/jump_landing.mp3, audio/voice/wizard_quest.mp3, audio/music/night_theme.mp3.
Don't let every new feature invent its own audio loading logic.
Make one audio system and route sounds through it.
codex skills and gamedev plugins
If you code in Codex or similar agent setup, install relevant skills/plugins early.
For this kind of project: Three.js skill, gamedev plugin, browser preview tooling.
Skills are usually global for most IDE / agent workflows so you don't need to explain same rendering/game-dev basics every session.
This doesn't make AI understand your game fully, but it gives better defaults when request touches rendering, animation, physics, assets, UI, performance at same time.
project context helps but stable rules matter more
At start I tried to have project context / README-style direction. It helped bootstrap game but I didn't keep updating it properly while game kept mutating.
Concept changed constantly: movement, frog, skeleton enemy, graphics, cannon, map, destruction, quests, NPCs, audio, UI.
Lesson is not "write huge perfect project document".
Lesson is keep stable rules documented and don't rely on stale plans for changing details.
Good stable rules: animation does not move character, Blender mesh names define world behavior, collision meshes are separate from visual meshes, cannon is a state machine, debug info must exist for physics systems, important world objects need predictable names.
Bad things to over-document too early: exact final quest structure, exact final map layout, exact final enemy list, exact final UI.
With AI coding, stable rules matter more than stale roadmap.
final advice
Use Blender as source of truth for world. Name meshes like API endpoints. Use TripoAI web optimized models. Avoid Meshy meshes unless you specifically need something from it. Inspect .glb files in Don McCurdy viewer. Compress .glb files with BabylonPress but test every time. Use Squoosh reduce palette for HUD PNGs. Use Brotli during build. Keep collision simple and separate from visuals. Build collision from boring cubes if needed. Don't let animations move physics bodies unless controller is root-motion based. Inspect animation tracks before wiring states. Make debug tools before features multiply. Treat cannon/projectile mechanics as state machines. Don't chase arbitrary real-time destruction with messy AI generated GLBs. Bake what you can. Throttle what player doesn't feel. Use billboards for atmosphere and cheap VFX. Use fog as mood and optimization. Add sound and visual feedback to every important player action.
AI can build a lot very quickly, but only if you give it systems it can reason about.
The more your game becomes random objects with vibes, the more it breaks.
The more your game becomes named meshes, explicit states, debug values and simple runtime rules, the more AI can actually help.