I'm Shifat Masud, a design engineer and student game dev. Shade Verse is my 3D web-swinging game, and it runs in a browser tab.
“Our game name must always be "Shade Verse".”
Shifat Masud
You play Shadow, a dark hero who swings, springs, glides and flies through an open city with a park at its heart: a forest, a round lake, a sandy beach and a footbridge. South of it lies an open sea with Jungle Island and its waterfall, and a long bridge to a second city, Neon Harbor. Shadow's wife Ghost swings with him, and the two talk the whole way. Robots fill the streets, and you fight them with melee combos, web shots and a Web Shock. It plays on a phone with touch buttons and on a laptop with mouse, keyboard or a gamepad. No install, no store, just a link.
I built it with Zero, my AI build partner. I set the vision, the locks and the taste, and made every call. Zero wrote, tested and shipped the code, pass after pass, and showed me a before/after sheet for every change. The goal was simple to say and hard to do: it has to feel like a real swinging game and still be cheap enough for my own budget phone and my old Intel laptop.
This case study is what I learned along the way. Every section ends with something you can take into your own work, and every number says where it came from. Where something isn't measured yet, it says that too.
01
Process: a scorecard and a lock file
49 · first build86 · best so farA game like this can stay "almost fun" forever. So from the very first build, every release got a score out of 100 on five pillars: Visuals, Controls, Performance, Stability and Device compatibility. Controls carry the most weight (25), because a swinging game lives or dies in the hands.
The first build scored 49. The best release so far scored 86. In between, the line goes down as often as it goes up: the giant boss landed at 79, because a big new feature that isn't polished yet should cost points. A score that only rises is decoration.
The second tool is a lock file. It is short, and every builder reads it first: melee combat is locked, the Shadow hero model, the HUD and the controls are locked, and the game is landscape only. It also holds the release rules: cite real visual references, capture before-and-after screenshots every iteration, check both of my devices, and keep what was observed apart from what was estimated.
Locks are what let me say "keep improving" and go to sleep. Nobody "improves" the things I already love by accident.
How we decide
Lake water upgrade direction
Reflection & Refraction
Best Practice (Hybrid)
Hero Object → analytic raytrace
Everything else → SSR reflection + screen refraction
Fallback → cubemap …
Always add Fresnel for realism
{Index of Refraction for Water = 1.33}
Got it, passed to the build as-is … Heads-up: real SSR needs an extra scene render, so it's HQ-tier only.
Ssr reflection & screen refraction (res down, low quality) can be used in phone smartly
Fair, updated: the phone gets SSR + refraction too, at quarter res, ~6–8 steps, only rendered while the lake is on screen, and it drops to cubemap if FPS dips.
02
The concept: a shadow that swings
Swing · spring · glide · flyA park with a lake, inside a cityShade Verse is a 3D game that runs in the browser, on a phone or a laptop, with nothing to install. You play Shadow, a dark hero who web-swings through an open world: a city block of streets and rooftops, and a big park in the middle of it with a forest, a round lake, a sandy beach and a footbridge.
The idea is the feeling of a superhero swinging game, made to fit in a browser tab on a budget phone. You swing, spring between buildings, jump, glide and fly. When robots get in the way, you fight them with your fists, your web and your Web Shock.
The world is built from free low-poly Polyfork models (buildings, trees, the footbridge), shaped and placed in code, with Poly Haven CC0 textures for bark and leaves. Everything else, the water, the sand, the grass and the outlines, is a shader written for this game.
03
Mobile first: two real devices set the budget
Galaxy M02s · Adreno 506Intel UHD 600 · 1366×768The target isn't a gaming PC. It's the two devices I actually own, read from their own GPU reports:
- Phone: Samsung Galaxy M02s (SM-M025F), Adreno 506, WebGL2 through ANGLE on GLES 3.0, max texture 4096, 412×915 at 1.75× density (played in landscape at 915×412), Chrome 155 on Android 10.
- Laptop: Intel UHD Graphics 600, 3 GB of RAM, 2 cores, 1366×768, Edge 155 on Windows 10. Its report flags MSAA as slow.
Those two set the rules for every pass: few draw calls, no new post-processing passes, textures at 1024 px or less. Trees are merged per species and instanced. The water is one mesh. The beach is drawn in the ground shader with no texture at all.


The honest part
Real-device frame rates are not measured yet. Every number in this case study that sounds like speed came from a headless browser with a software renderer, which is far slower than either device and behaves differently. The biggest known risk is the lake: its scene copy roughly doubles draw calls while the lake is on screen (estimated from the code, not measured). It runs every other frame when the resolution scaler drops to 0.6 and turns off at 0.5. The next step is a real fps check near the lake on both devices.
04
Movement: swing, spring, glide, fly
Swing cap 46 m/sHold Space to glideMovement is the game. It's split into four verbs that chain into each other without a menu or a mode switch:
- Swing. Hold the swing control and a web line attaches to a building anchor, at least 12 m long. Speed builds through the arc up to a cap of 46 m/s.
- Web spring. A tap webs you straight to the nearest real anchor and launches you along it. Jump while swinging does the same from the line you're on.
- Jump, glide, fly. One button does all of it. Tap to jump: you leave the ground at 24 m/s, reach about 4.5 m in 0.39 s and fall faster than you rise, so the whole jump takes 0.74 s. Tap again in the air for a double jump. Hold past 0.18 s to glide, sinking at about 4 m/s and speeding up to 32 m/s forward; the glide stays on after you let go. Hold again while gliding to fly up at up to 44 m/s, and tap while gliding to drop out with a jump.
- Walls and water. Run up walls, jump off them, vault over edges, and swim when you fall in the lake.
Those numbers aren't from feel alone. They're constants in src/sim.ts, and the flight caps were raised once when I asked for faster flight (swing cap 34 → 46, glide cap 14 → 22 → 32). On the desktop, Space does exactly what the phone's jump button does, 1:1, so both feel like the same game.


05
Combat: melee, web shot, Web Shock
4-hit chainWeb Shock · up to 5 targetsThe streets are full of robots, drones and armoured heavies with red eyes, 26 of them on the map at a time. Combat has three tools, each with one job:
- Melee. A 4-hit ground chain: jab, cross, a full 360° spin hook kick, and a launcher that pops the robot into the air. Melee again while it hangs and Shadow leaps after it for a diving spike kick. Input is buffered, so a press during a strike starts the next one at its cancel point. Every hit has its own hit-stop and a small camera shake.
- Web shot. An auto-targeted plasma bolt (55 m/s, up to 3 in flight) that bursts in an 8 m radial blast with knockback. Shoot a launched robot and the web yanks it down into the street.
- Web Shock. Hold the attack (on a mouse, hold left click past 0.22 s): up to 5 targets within 24 m are webbed, pulled together and shocked in one 1.65 s move, with a 0.10 s hit-stop. It used to be called Shadow Snare. I renamed it everywhere a player can see it.


Melee combat is locked. Once the choreography felt right, frame by frame against real martial arts references (Muybridge's boxing plates, a taekwondo 540 hook kick, a capoeira armada), nobody could touch it without my OK. Only Web Shock was left open to change.
06
The lake
Fresnel · IOR 1.33Hero ray-traced, the rest from a low-res copyThe lake is the centrepiece of the park and the most expensive thing in it, so it got the most thought. I gave the direction in one message: a hybrid. The hero gets an analytic ray-trace, everything else gets screen-space reflection plus screen refraction, there's a fallback, and Fresnel is always on, with water's index of refraction at 1.33.
- Fresnel, always. IOR 1.33 gives a reflectance of about 2% looking straight down, rising toward the horizon. That's why the lake is clear at your feet and a mirror in the distance.
- The hero, ray-traced. Shadow is traced into the water analytically as three spheres, so his reflection is always there and costs almost nothing.
- Everything else, from a low-res copy. Reflection and refraction read a low-resolution copy of the scene: quarter resolution with 7 steps on phones and laptops, half resolution with 16 steps plus refinement on the high tier.
- Only when it matters. The copy is made only while the lake is on screen. It drops to every other frame when the resolution scaler is at 0.6 or below and turns off at 0.5. Then the water falls back to a sky-and-treeline reflection and lets the real lakebed show through.
- Life on top. Animated ripples, colour that deepens with depth, and shallows that fade into the sand. The water is one draw call of about 2.7k triangles (from the code, an estimate of cost, not a measurement).

Before this pass the water was a flat disc that floated about half a metre above the grass at its rim. Now the lake is round, its shore shelves down into a bowl, and the bridge lands on the trail at both ends.
07
Beach, trees and a bridge
Pine · oak · cherry · palmSand ring with a wet bandA lake on its own is a puddle. What makes it a place is the ring around it, and I asked for three things: a beautiful lake, better trees (pine, oak, cherry blossom, palm) and a sand zone.
- The beach. A ring of sand all the way round, widening into a beach on the side away from the trail, with a darker wet band where the water meets it. It's drawn inside the ground shader, with no texture, and grass stays off it.
- Four tree species, four silhouettes. Pines and spruces in the forest, oaks with thicker trunks, 18 cherry blossoms ringing the lake with falling petals, and 9 palms on the sand. Each species is merged geometry drawn with instancing: 18 draw calls for all the trees plus 1 for the petals (from the code).
- The bridge. A Polyfork footbridge spans the round lake, and its ramps slope down to touch the trail at both ends, because I asked that its start and end must touch the path.



Every species started from real photos, cited in the repo: Lake Tahoe's Sand Harbor and Lake Wanaka for the shore, Yoshino cherry blossoms, coconut palms, a Jura oak. Palm fronds were flat blades up close at first; they were rebuilt as folded, leafleted fronds in env-iter3.
08
Quality tiers: one game, three budgets
low · auto · highFXAA on integrated GPUsThe same build has to run on my phone and look its best on a strong desktop, so there are three tiers. There's no settings menu: the game picks one, and a URL parameter overrides it.
- Auto is the default for touch screens and integrated or mobile GPUs. Both of my devices stay on auto.
- High is picked only for a clearly strong desktop GPU (discrete NVIDIA or AMD Radeon, Apple M-series), or with
?q=high. It raises the lake to half resolution with 16 steps, grass from 6,500 to 9,500 blades, the grass radius from 57 to 72 m, and anisotropic filtering from 4× to 8×. - Low (
?q=low) drops grass to 3,500 blades and, on a desktop, caps pixel density at 1×.
Anti-aliasing follows the same logic. MSAA only runs where it's cheap: not on touch screens, not on integrated or mobile GPUs. Everywhere else gets FXAA, one full-screen pass that can be switched on and off at runtime from measured frame time. MSAA can't, because it's fixed when the WebGL context is created. My Intel UHD 600 laptop flags MSAA as slow, so it gets FXAA.

09
Desktop controls, modelled on Spider-Man PC
WASD + mouseTap = shot · hold = Web ShockThe phone has touch buttons. The desktop had to feel like a real PC action game, so the model was Marvel's Spider-Man on PC: movement on the left hand, camera and web on the mouse. We went through several layouts in one evening. I picked the final one myself:
- WASD
- Move
- Mouse
- Camera (click to lock the pointer, Esc frees it)
- Left click
- Tap: web shot. Hold: Web Shock
- Right click
- Tap: web spring. Hold: swing
- Space
- Jump, glide and fly, exactly like the phone
- K, Ctrl, M or .
- Melee, so both hands always have one
- Gamepad
- Left stick move, right stick camera, A jump, RB tap shot / hold Web Shock, LT tap spring / hold swing, X melee
Tap and hold share one button with a 220 ms threshold. The game detects what you're using and shows a small popup ("Keyboard & mouse detected" or "Gamepad connected") instead of on-screen icons, and the controls are listed on the title screen. All of it is routed onto the same input flags the phone uses, so there's only one game underneath.
My pick:
WASD = Move
Mouse move = camera move
mouse left tap = web shot, hold = shadow snare (rename it to web shock everywhere)
Mouse right tap = web spring, hold = web swing
Space = current mobile versions jump, glide, flight behavior exact 1:1
K = Melee
Ctrl = melee too
M = melee too
. = melee too
Locked, your pick is final … Shadow Snare renamed to Web Shock everywhere. Everything else from the earlier maps is dropped.
What isn't verified yet: pointer lock, the gamepad and the hold ring can't be tested in a headless browser. The popup itself was seen working. They need a check on a real laptop with a real pad.
10
Key moments
Turning points, not averagesOct 27 → Oct 29 · 172 commits- v0.1
A first build with a score on it
The first build was graded 49/100. In a headless browser a test bot chain-swung to 61 m/s with no page errors, and every line about my real phone and laptop said UNTESTED. That word stayed in the scorecard until something was actually measured.
- v0.6
Free roam
The timer and the game over came out. Falls cost nothing, and the city became a place to explore instead of a race against the clock.
- v0.9 – v0.10
The street becomes a floor
Street level became walkable (it used to respawn you), the first robots arrived with punch and dodge, and hard landings started to cost something. A swing from street level now zips you up toward the anchor instead of leaving the rope slack.
- 5:31 AM
Walls, then water
I asked for wall running, wall jumps, vaults and a higher jump. They shipped as tested logic first (jump apex 2.4 m → 4.6 m), with the camera framing honestly marked as not yet checked by eye.
- v0.76 – v0.77
Combat with weight
Martial arts choreography studied frame by frame: the 4-hit chain, launcher, aerial spike, web pull-in and web yank, hit-stop and camera shake. Score: 85. Then melee was locked.
- Ghost & the boss
New characters cost points
A companion, Ghost, and a giant boss built from three times Shadow's own body landed. The score dipped to 79, and the scorecard said so, until the boss got full armour and its own rooftop flight.
- City rebuild
A real park
The city, park and lake were rebuilt with Polyfork models: buildings, trees and a footbridge. Then the original hand-authored park forest and its detailed foliage were restored on top.
- env-iter1
A round lake
The lake became a circle: a polar-grid water sheet, a round shoreline and a bowl under it.

- env-iter1
The bridge touches the path
"Bridge start end must touch path." Eight modules now span the round lake, and the ramps land on the trail at both ends.

- 29616c4
Shade Verse
Hybrid water, the beach, four tree species, quality tiers, the desktop controls, Web Shock everywhere, and the title screen with its real name. The build these shipped on.
- 30a3b3a
Real palms
"Palm fronds look basic." Each frond became an arched, V-folded strip with about 60 leaflet pairs and its own sway. Every tree got a natural tapered trunk, the meadow grass got one outline instead of one per blade, and the underwater bubbles started following the real swell.
- 24b325f
The robots shoot back
Enemies raise a glowing palm for 0.4 s, then fire a repulsor bolt. The boss fires 3-shot volleys and charges a sweeping chest beam. Web Shock pops incoming bolts. A full sound pass landed with it: 35 procedural recipes, an original score, CC0 footsteps and birds, no sirens.
- 4b6a410
The boss follows you
"Boss should follow heroes & keep attacking." The boss left its rooftop: it hover-dashes after you, lands on whatever roof, street or park you're on, walks you down up close and keeps firing. The leafy trees got about 65% more leaves, and the bubbles became a random emitter: seabed vents and bursts plus a stream off you, each bubble with its own size, speed, wobble and life.
- 666a030
The city takes flight
"Flying cars must have collision & web must work on them." Cars got real collision (ride the roof, get shoved sideways, bonk the underside), and web and spring anchors now ride along with the car.
- 3ae41a1 – 4e1c60e
The South Sea
An open ocean out to 1.1 km south of the city, with a camera-following sea, a seabed that deepens offshore, screen-space reflections, boats you can web onto, gulls, fish schools and a whale. Underwater got sharp caustics and slanted god rays.
- 730e3a6
Everyone talks
49 family-friendly meme lines for Shadow, Ghost, the robots, the boss and the people on the street, all voiced, levelled and short enough not to get in the way.
- 46cabfb – f0ce5e6
An island and a second city
Jungle Island rose out of the sea with a waterfall, a lighthouse with soft god rays and long grass, built from the same park assets. A long bridge with flying traffic runs east to Neon Harbor, a second city of canals and neon with its own robots and a mini-boss.
- dba020b – 5f152c8
Husband and wife
Ghost and Shadow are married, and now they sound like it: 55 exchanges with captions that duck the music. "Too romantic" sent it back once, and v2 made it caring instead: checking on each other, teamwork, pride after a win, one "Love you" kept on purpose. Then they got equal volume and started talking about three times as often.
- Oct 29, 9:07 PM
One night of upgrades
"Keep iterating without waiting for my permission." Zero shot every area day and night, wrote a plan with targets and shipped a before/after for each fix: a see-through boss zone, soft round sparkles for Ghost, ~1,750 rooftop props dropped onto real roof surfaces, wet sand and ripples on the beach, and robots that dissolve when they block the camera.
- bb2ff9e · 0bf1d6c
A river you can swim under
"Make water fall to sea a direct curved river… make them deep." The waterfall now feeds one curved river straight to the sea. The channel is 4.2 m deep, the island lake 7.5 m, and at the mouth the foam dies down and the colour turns to the sea's, so river and ocean read as one surface.
- 9f9e471
They tell each other stories
"Funny catchy stories." 12 short stories they tell and react to (a pigeon that stole Shadow's lunch, Ghost catching a wedding bouquet, pirate treasure that turned out to be an expired taco coupon) and 10 more teases: 83 new lines in the same voices. The same pass found 14 Ghost lines about 14 dB too quiet and fixed them.
- 049bf38 · 27b863c
One button, five moves
Space now does it all: tap to jump, tap again for a double jump, hold to glide, hold while gliding to fly, tap while gliding to drop out with a jump. Glide went 22 → 32 m/s, flight 44. Then "jump faster, fall faster", twice: the whole jump now takes 0.74 s instead of 1.19 s for about the same height. This is the build that's live now.
11
UI design: the title screen says the name
One mark, one nameAny key or tap to startThe HUD is locked and stays minimal: small custom icons, no gameplay text. So the one place the game can introduce itself is the title screen, and until this pass it carried an old working title. Now it says Shade Verse.
It uses my own mark, the same two-shape logo as my portfolio and the Surface Tension case study, taken unchanged. The mark springs in, the name settles from wide letter spacing to tight, then a play button and "by Shifat Masud". On desktop it also lists both control maps, mouse + keyboard and gamepad, and any key starts. On a phone it's simply "Tap to start".



One rule I keep: the game's name is always written the same way, two words, Shade Verse. On the title, in the page title, in this case study.
12
Reflections: maths instead of a mirror

The textbook way to reflect a city in water is to draw the city twice: once for you, and once upside down for the water. That second picture is exactly what a budget phone can’t pay for, so on Oct 31 I locked it out. No reflection probe, no second renders. The sea still had to reflect the city.
What followed was the longest search in the project. Zero built a reflection, I looked at it, and most of them died:
- A probe, screen-space reflections, a mirror pass (Oct 30). They looked right, and every one of them drew the world a second time. The lock removed them.
- A skyline strip (Oct 31). The towers were projected once on the CPU into a strip the sea reads with one texture lookup. Zero renders, but the shapes floated instead of sitting on the world.
- Four more tries (Nov 1), each behind its own test switch: a baked top-down height map, flat picture cards of the skyline, real screen-space as a test only, and a full mirrored scene.
none of 4 ok
Analytic raytrace reflection has more potential... trace every building, bridge, islands. Lets go with this if performance good
heavy blurry low detail cheap
The trick: maths instead of a mirror
Every landmark goes into a small data texture once, as a box with a colour. A coarse grid over the map, 40 m per cell, lists which boxes touch each cell. For every sea pixel, the shader bounces the view ray off the rippled surface and walks it across that grid cell by cell, the same walk the old raycasting games used, and tests only the boxes in the cells it crosses. Open sea is skipped in jumps: each cell knows how far away the nearest building is, so a ray over empty water leaps up to 7 cells at once.
Then I asked for less detail. Each cell became one soft block: all its buildings merged into one footprint, the tallest top, the average colour, and a blur that grows with distance. The ray climbs at half speed, so every mirrored shape stretches about twice as long toward you, the way reflections smear on real rippled water. At night each block carries its own light: warm windows in the city, magenta and cyan neon in Neon Harbor. The same trace is reused from below the surface, which is the next chapter.
How much of it you see is decided by Fresnel, using the exact formula for light going from air (1.0) into water (1.333) rather than the usual shortcut. Clear at your feet, a mirror toward the horizon.
13
Refraction: one line between two worlds

Water isn’t a surface, it’s a border between two worlds. From above you look through it into the sand. From below you look up through it at the sky. And when the camera sits half in, you see both at once, cut by one wavy line. Each of those views had to agree with the others.
Looking down
Each sea pixel bends the view ray with Snell’s law (air 1.0 into water 1.333) and follows the bent ray down to the seabed, which is a baked height map, in a short two-step march. Light dies along the way the way it does in real water: red first, then green. The bed only shows through where it’s less than about 9 m down; deeper, it’s just water colour. The bright webs on the sand come from a caustics tile simulated once by refracting a million rays through a rippled surface. It’s the same water path on the phone and on the laptop.
Looking up

From below, the surface is a window to the air. Inside Snell’s window, the cone of sky you can see from underwater, the same maths that draws the reflections runs along the refracted ray, so the skyline and the sun show through the waves, with a slightly different bend per colour for a hint of rainbow at the edge. Past the critical angle of 48.6°, total internal reflection takes over and the surface mirrors the seabed. I wanted it soft, so it was rebuilt after studying photos of real Snell’s windows: blurred, semi-transparent, no hard rim.
The line
The hardest pixels in the game sit on the waterline when the camera is half under. The first version solved that line just 1.2 m out, across 9 screen columns, on the swim physics’ swell, which had no chop and no sideways push. It could sit up to 0.4 m off the waves actually drawn. My note was short:
“Water line must cover more distance & must not bleed underwater or wave to air.”
Shifat Masud
Now the line is solved for 25 screen columns, smoothed with a spline, on the exact waves the graphics card draws, out to 250 m. One shared function cuts the sea, tints the underwater half, draws the soft white meniscus and stops the god rays, so they can never disagree again. It only runs while the lens straddles the surface: about 500 wave evaluations a frame. During the speed pass it got 33% cheaper (0.41 → 0.28 ms a frame) and was checked bit-identical over 300 random camera poses.
Diving is one world too. Haze, seabed, kelp and terrain blend over the first metres of a dive instead of switching at the surface, and the half-under view uses the same fog as the dive itself.
14
The post-process stack: five passes, in order
A frame of Shade Verse isn’t drawn once. It’s drawn, then handed down a short line of passes, and each one works on the finished picture the last one left. The order isn’t a detail; it decides what each pass can see.
- The world, at 0.8 resolution. Every device draws the world at a fixed 0.8 pixel density. Inside that, the sea, the heaviest shader in the game, gets its own path: the world draws first without it, the sea draws alone at half pixel density with the world’s depth already filled in so it tucks under every edge, and then it’s laid back in at full size with a per-pixel depth test. See-through things (particles, glass, rays) go on top.
- Ink outline. One pass reads the depth of the frame and inks an edge wherever depth jumps, but only on the nearer side, so the line stays 1 px and sits inside the silhouette. It fades out between 22 and 55 m. Surfaces carry a quiet tag in the alpha channel: lights are never inked, water is never inked, and grass inks only the outline of the whole meadow, not every blade. The same pass paints the underwater half and the meniscus when the camera is half under.
- Smoothing. Edge smoothing (FXAA), plus a gentle sharpen close to the camera: full within about 6 m, gone by 28 m, never on the sky, clamped so it can’t halo.
- Glow. Only things tagged as lights are drawn again, into a target a quarter of the size, blurred there, and added onto the frame with a single lookup. Always on, day and night.
- Heroes in HD. The world is scaled up to the screen, and then Shadow and Ghost are drawn again at the screen’s full resolution (up to 2×), depth-tested so trees and buildings still hide them.

Why this order
Ink comes before smoothing, so the smoothing softens the ink lines too. Glow comes after ink, so lights spill over the outlines and never get inked themselves. Heroes come last, so they stay crisp over everything, including the glow.
During the speed pass we tried to merge these passes, and the numbers said no. Smoothing reads 9 neighbouring pixels of the outlined frame, so a merged pass would have to run the outline 9 times per pixel. Folding the glow into the smoothing would save one cheap pass, under 1%, and round the colours slightly differently. And the last pass is a lock. Five passes it stays.
15
Speed is mostly what you don’t draw

On Nov 2 I asked Zero for a long list of everything that could make the game faster, and then to work through all of it. One rule: measure first, and cut only if you can’t see the cut. Every idea was timed on a pinned phone-sized test with identical before and after shots, and nothing shipped if the shots differed.
Thirteen changes shipped. Thirteen ideas died on the numbers. Almost everything that shipped is the same idea in different clothes: the fastest thing to draw is the thing you don’t draw.
What shipped
- Don’t draw what’s far away. Lilies past 420 m: −145k triangles, everywhere. Harbor rooftop props only near the harbor: −30k. Far lily patches as simple pads: island 93k → 57k. The park ground grid: 41k → 18k, same look.
- Don’t draw what’s behind you. Culling on the static island and harbor meshes. The crowd, drones and robots outside the camera view are packed out of the draw: street −14% triangles, harbor −19% and 5 fewer draw calls.
- Don’t do the same work twice. Every sea vertex was computing the full wave maths twice at the same point, once for shape and once for lighting. One pass now, same values. The waterline solve: 0.41 → 0.28 ms a frame, bit-identical. The glow blur runs once at quarter size and is added with one lookup instead of 13. No new objects created every frame, so no garbage-collection stutter.
- Draw fewer pixels. The world renders at a fixed 0.8 pixel density on every device while the heroes stay sharp. The biggest single cut.
- Carry less. Models 2.8 → 0.82 MB, losslessly compressed. Voice lines are decoded when first heard instead of all at once: about 93 → 5 MB of memory.

How the timings were taken: in a headless browser with a software renderer, on a shared machine, which is far slower than a real phone. They are good for before/after on the same scene, not as frame rates. The glow change: phone harbor 348 → 320 ms per frame, island 381 → 364, ocean 336 → 315. The sea change: high sea views about 360–420 → 230 ms, and because the machine was busy with other work at the time, treat that one as indicative.
What we threw away, and why
This list taught me more than the first one.
- Merging the screen passes. The outline would have to run 9 times per pixel; the only safe merge saved under 1%.
- Smaller textures. There are only 37, all preloaded on Play. Going lower would blur the ground at grazing angles.
- Splitting big meshes so their off-screen parts skip drawing. Those parts are plain static shapes and cheap to throw away; splitting would add 5–15 draw calls that cost a phone more than they save.
- Merging draw calls. Almost every draw has its own material, so nothing merges without changing the look. Skipping empty draws was exact, but saved 0.05 ms, so it was reverted.
- Thinning the see-through layers. Even removing all of them would save 10–24%, spread over 24–43 small glows and halos. Nothing could go without being missed.
- Re-encoding the audio. There are no lossless masters, so it would be compressing already-compressed sound. The best setting cut only 28% and audibly changed the clips.
- And a few that were already done or not worth it: there are no shadow maps to optimise, shaders already compile up front, idle effects already hide themselves, and the matrix updates cost 0.06 ms.
16
Copy the real world, not other games

The night of Nov 1 was the sky. Every element was rebuilt by putting a real photograph next to the game and changing the game until the two felt the same:
- The moon started from a real photo of the full moon, then became what a camera actually sees on a fjord at night: a blown-out glowing disc, a bright corona and a big milky halo.
- The sun is a blown-out white-hot disc, golden when low, with a wide glow and no ring.
- The rainbow has bright saturated feet fading toward the top, a brighter sky inside the bow and the dark band between the two bows, like the photo.
- The stars were blocky square dots. Now they are round points with a long tail of sizes and brightness, and a gentle twinkle.
- The aurora took seven versions, each checked against a real photo: from faint curtains, to a true 3D curtain sampled at 7 heights, to purplish light on the tops.
Clouds, moon, stars, aurora and rainbow are all mirrored in the sea through the same shared functions, so the sky and its reflection run on one clock. On Nov 2 I locked the whole sky as a restore point.
The fire that looked fake

When a robot went down it burst into solid orange shapes, a spray of cones, a ring on the ground and a pale cyan disc. It read as a cartoon. I asked for “softer, realistic particles glow based burst”, then “Study real first burst explosion images”, then one word: “Fire”.

Zero studied 12 real photos of explosions and wrote down what they show. A white-yellow flash for a few frames. A lumpy, cauliflower-shaped fireball that grows fast, then slows, cooling from white to yellow to orange to deep red. Dark smoke rolling up as it cools. Sparks that streak, slow down in the air, then fall.
The burst was rebuilt from that list: a 0.1 s flash, 14 lumpy fireball puffs on that cooling ramp, 26 sparks with glowing streaks, and 10 dark smoke puffs lit warm from below while they’re young. A few sparks carry a faint tint of the hero who landed the KO. It went from 5 draw calls to 2, with no measurable frame cost. The first try got one thing wrong: the particles were about 2.6× too big, and it looked like a flat disc. Fixed before it shipped.

The same rule spread to every effect on Nov 2: soft, glowing, cinematic, no hard edges. The comic hit-star became a white-hot core with sparks and a soft bloom. Every solid ring was removed, from punches to the Slam shockwave. The aim-lock went from a sharp cyan square to soft glints in the hero’s colour, and the Web Shock and web shot followed: purple for Shadow, pink for Ghost.

17
A hold is a duration, not a frame count
On Nov 2 the phone buttons were merged the way the desktop works: tap the web button to spring, hold it to swing; tap to fire a web shot, hold for a Web Shock. A hold is anything longer than 220 ms. No cooldowns anywhere.
Then a real bug turned up on desktop. A press only became a hold if a frame happened to run after the 220 ms mark while the button was still down. On a slow frame, a hitch or a weak laptop, that frame never came in time, and a 300–450 ms hold was read as a tap: you meant to swing and you sprang instead. Measured with a scripted test: 8 of 24 desktop holds misread before, 0 of 24 after. The fix is one idea: decide hold or tap from the actual press time at release, not from whether a frame noticed. A swing caught late is held for 2 frames so the game always sees it. The phone buttons already used a timer and read 0 of 24 wrong both times; they now also check the press time at release, in case the timer runs late.
The other small things that make it feel solid
- Swing, spring and flight work from anywhere: from a wall crawl and from a rooftop, because I said they must.
- The title screen sizes itself from its own box, not from the window’s height, so a stale browser viewport can’t push the logo or the Play button off a phone screen.
- A guard on the jump button. A stale or duplicate touch could throw an error on the jump button, the move pad and the camera drag; they now guard it the way the other buttons already did.
- Dead code out. A tree kit that was never placed in the world was still loading 10 models (252 KB) every time you pressed Play. Removing the unused code removed that wait too.
18
Restore points let you be brave
Most of the best things in this game started as “lets try”. That only works if going back is free. So whenever something felt right, it became a named restore point: a tag on GitHub I can return to with one word, and a line in the lock file so nobody changes it by accident.
- water (Oct 31). The water looks I locked that afternoon.
- boss (Oct 31). Taken right after the boss got its new face, before the islands and the weather went in.
- sky (Nov 2). Sun, moon, aurora, clouds and the underwater look.
- fire-ko (Nov 2). The fire KO burst, the soft aim-lock, the sea speedup, the crowd cull and the new water line.
The undo button got used, and that’s the point. City reflections streaking down the wet streets: I said “looks bad”, and they were gone in five minutes. Storm spray across the sea: tried, reverted. Drops on the lens after a dive: tried, reverted. When my phone stopped drawing the sea one night, two sea shader rewrites came out at once, and went back in a minute later. None of it was scary, because nothing could be lost.
19
Learning to move from a 30-second clip
On Nov 2 I sent Zero a 30-second clip of Ultimate Spider-Man swinging through the city at night, with one line: “Learn animations from this.”
Our heroes already went where the physics sent them. Their bodies just didn’t feel any of it. The whole swing was one fixed pose: a lean and one arm up. Every fall and every web spring froze the jump halfway. Landings were straight legs tipping forward, which read like a stumble. And the run leaned about 2° whether you jogged or sprinted.
So the clip was studied the way an animator would study it: at 10 frames a second, one move at a time, writing down poses and how many frames each phase lasts. The notes read like a choreographer’s. After the web hits, the body stays laid out along its path for 4–5 frames before the line pulls it round. After a release, it’s upside down 0.2–0.3 s later and upright again by about 0.6 s. Those numbers became the targets.
Swing: hang, lag, whip
The body now hangs from the line hand and lags the rope like a pendulum on a spring. Right after the web hits it’s still laid out along the direction of travel. At the bottom it hangs straight under the line. On the way up the legs, which trailed behind on the way down, whip forward with the knees up, and the free hand comes up to join the line.

Release: keep the spin
Letting go on the way up carries the swing’s spin into a tucked backflip: knees to the chest, upside down about 0.25 s after the release, upright again at 0.62 s. The flip is one smooth curve from the release angle to a full turn, so it never pops at either end. A low release skips it, and a new web, a landing or an attack takes over from wherever the body is.

Freefall: a skydiver who looks down
A fall turns the body belly-down into a skydiver’s frog: arms wide, knees apart, back arched, head up. It banks into turns, and it grows stronger the faster you fall. The part I like most: it checks how far the roof or street below is, and turns back to feet-first in the last ~0.35 s. Nobody lands on their belly, and short hops stay upright.

Landing: the drop decides the crouch
Touchdown is a crouch now, deeper for a bigger drop. A hard landing is the superhero landing: one knee down, one hand planted, the other arm out behind, held about 0.2 s and pushed out of in 0.55 s total.

Web spring: my own catch
Halfway through, I watched the clip again and saw something nobody had listed: “It has reference to improve our web spring too.” The zip moments. Now the body coils for a few frames first, knees in, chest curled. Then the yank snaps it in line with the pull, head first, legs trailing, with a stretch of about 9% along the body right as the line goes taut. Letting go while still rising rolls the pull into a front tuck.

Run: lean into the speed
The run leans with speed: nothing at a jog, about 17° at a full sprint, a bit more while you speed up and back when you brake. The head stays level the whole time.

What stayed out
A wall run was on the list. The clip has a quick hug of a wall and a vault over a ledge, but no long run up a building to copy. So we didn’t make one up. A move you can’t see in your reference is a move you’d be guessing at, and guessed motion is the stiff kind.
One rule held across all six: only the body changes. Input, timing, physics and hitboxes are exactly what they were, so the game plays the same and simply reads better. Everything is procedural, in one file of about 230 lines, with no animation files to download. It works on both Shadow and Ghost, the ink outlines follow every pose, and each move shipped as its own commit. The six landed within 27 minutes of each other.
20
Technologies & tools
React 18 · three 0.187 · R3FVite · custom GLSL- 3D and rendering
- three 0.187, React Three Fiber 8.17, custom GLSL shaders (water, ground and sand, grass, cel outline), a selective bloom pass, FXAA.
- App
- React 18.3, TypeScript 5.6, Vite 5.4, Framer Motion 11 for the title screen motion, GSAP 3.12.
- World
- Polyfork low-poly models (buildings, trees, footbridge, robots), modified in code. Poly Haven CC0 textures for bark, leaves and shrubs.
- Testing
- TypeScript checks, Vite builds, and puppeteer scripts in headless Chrome with a software renderer: motion, combat, walls, audio, environment, a 5.5-minute soak. A debug build exposes test hooks that the release build never ships.
- Shipping
- GitHub, with each pass on its own branch and never straight to main, and a Vercel preview to play it.
- Build partner
- Zero, who wrote, tested and shipped the builds with me.
One note on those tests: a software renderer in a headless browser says the code runs and the picture is right. It says nothing about frame rate on my phone. An early 5.5-minute soak there held 45–53 fps with a flat heap, and that proves stability, not speed. Know what your tools are actually measuring.
21
How we work together
Read the lock file firstBefore/after sheet every passI direct, Zero builds. Most of this game was made in short messages on my phone, often while I was doing something else, and sometimes while I was asleep. That only works with a few hard rules:
- The lock file comes first. Every builder reads it before touching code: melee, the hero, the HUD and the controls are locked, landscape only.
- Real references, cited. Lakes, beaches and each tree species from real photos; fights from Muybridge plates and martial arts frame sequences. The sources are in the repo.
- Screenshots, always. Every update comes with a labelled before/after sheet at phone and laptop sizes, so I can judge it in five seconds.
- Light checks, not heavy ones. Type check, build, a few focused tests. I care more about the next iteration than a long report.
- Branch, preview, then main. Each pass lives on its own branch with a preview link I can play. Main only moves when I say so.
- Truth rules. Observed and estimated are always written apart. No speed claim until it's measured on my two devices.
- Parallel hands. Separate builders take separate jobs at the same time (the game build, the trailer, this case study), each on its own copy, and one place merges.
Updates while I sleep
On the night of the environment pass I gave one instruction, "Now I give you full power. Keep working", and went to sleep. The work kept going in passes, each one landing as a short update with a preview link, a branch name and a sheet, and each one saying plainly what was tested and what wasn't.
22
What I'd tell you to steal
Budget for the weakest deviceLock what you love- Budget for the weakest device you care about. Write its numbers down first, and measure there before you call anything fast.
- Spend on what the eye looks at. One great lake with a cheap fallback beats ten good effects.
- Lock what you love. Then you can say "keep going" and mean it.
23
Credits
- Game design, direction & development
- Shifat Masud, built with Zero.
- 3D models
- Polyfork low-poly models, used and modified inside Shade Verse.
- Textures
- Poly Haven CC0 bark, leaf and shrub textures.
- Inspiration
- Marvel's Spider-Man on PC, for the control scheme and the feel of the swing. Ultimate Spider-Man, for the traversal animation study.
- Reference photos
- Real photographs, many from Wikimedia Commons (explosions, the moon, rainbows, aurora, Snell’s window), studied as references and shown small beside our frames.
To everyone in my life: you inspired this.
