Part 1 was the story and Part 2 was the systems. This one is the part the slice actually exists to show. Making the space feel like somewhere real, then getting it to run in a browser tab.
Two colour temperatures and nothing else
Every room in the house is lit in exactly two colour families. Not three, and not whatever the room seems to need.
| Family | Temperature | Sources | Means |
|---|---|---|---|
| Cold | around 10,000K blue-cyan, desaturated | window shafts, the basement window | the past, the night of 2011 |
| Warm | around 2,700 to 3,000K amber | the few working lamps, a nightlight | the present, information, safety |
One family dominates per room and the other one turns up as an accent. The Entrance Hall is cold dominant because it is a hostile welcome. The Living Room is warm dominant because it is the hub the player keeps coming back to and false comfort is the point. The office is a warm pool on a desk in a cold room. The basement is neither, just the torch, until dawn comes through the window warm at the end.
That one constraint is what makes the screenshots read as art directed rather than just dark.
It is also the cheapest art direction going, to be honest. It costs nothing at runtime and it turns every lighting decision in the project into a two-way choice instead of an open one.
The constraint that decided every fixture
Here is the thing that quietly settled the whole fixture list, and it is worth saying plainly because it catches people out.
A baked light cannot flicker. Its contribution lives in the lightmap and the runtime light object is inert, so flicker scripts and light switches do nothing to it.
Mixed mode is not a way round it either. Direct is realtime but indirect is baked, so switching a mixed light off leaves a residual baked glow in a room that is supposed to go dark.
So anything that flickers or switches has to be fully realtime, and the WebGL budget is a maximum of four realtime or mixed lights visible at once. Which means the question of which lamps in this house are allowed to move had to be answered before a single bake, and the answer had to be very few.
The lighting mode is Shadowmask. Baked GI and baked shadows from static geometry, while the flashlight still casts properly over anything dynamic. It is the only guaranteed realtime shadowed light in the game.
Storm, not gutter
The house has one big moment. The player winds a music box in the child's bedroom and every light in the building loses it while the tune plays.
The first version used a Gutter, which fades the lights to zero and holds. It ran for a full five seconds and it read as a brief flicker followed by nothing. Fading to black and holding just reads as the room going dark, and that is not the same as the room being attacked.
Storm strobes instead. Near-black beats with over-bright snaps up to about 1.7 times the authored intensity. Same duration, completely different read. Gutter is still the right call one beat earlier where the point genuinely is a dip.
The whole beat is timed off one number. The tune is 13.9 seconds long and it starts three seconds after the wind, underneath the wind-up spring sound. So the music ends 16.9 seconds after the player touches it, and the cutscene runs 0.3 to 16.9 to match. It hard cuts from a wide shot of the landing to a close on the box at the exact midpoint of the tune. Wide house, then close on the cause.
Three separate numbers come off one audio clip's length and there is nothing in code linking them. Swap the clip and all three need redoing. I put that warning in the design doc in bold, for me, later.
One detail that only shows up on screen. The pack's table lamps are two renderers, base and shade, so if you wire the flicker to one of them half the lamp stays glowing in the dark. The flicker component takes a renderer array and swaps the whole fitting between worn-off and worn-on materials. A dark beat shorter than a 0.12 second threshold leaves the glass alone, so it never pops on every strobe.
Shafts, dust and the torch
Moonlight comes off a low angle directional light, around 15 to 20 degrees elevation so the shafts rake long across the floors, with a mullioned window cookie on it. Each shaft gets a small particle system, about 40 particles drifting at 0.05 m/s, and they are only visible inside the beam volume.
That combination of a light cookie and particles in the shaft is the best screenshot value per hour in the whole build. It is not subtle and it is not expensive.
The flashlight is the main tool so it got the most attention. Spot, 35 degrees outer and 25 inner, 15m range, warm-neutral rather than pure white, with a dirty lens cookie texture on it. A cookie turns Unity's default cone into something that looks filmic straight away. Realtime shadows on, and the light is parented to a transform that slerps toward camera rotation so the beam drags a bit behind the look. Beam lag sells the weight of it.
Audio is half the environment
Reverb zones per room do more for making somewhere feel like a place than any texture does. Bare concrete basement, tiled bathroom, carpeted living room. It is the difference between rooms as you walk between them that sells it.
Ambient stingers are randomised on a jittered 20 to 60 second timer and played at real world positions with full 3D spatial blend, from valid locations like the floor above you. Never 2D stereo scares. A noise that comes from upstairs is a completely different experience to a noise that comes from your headphones.
Footsteps raycast down and switch clip sets by surface, which is a small job and worth it. The music box motif is three arrangements of one eight note tune, broken and faint, then full, then hummed.
Getting 10GB into a browser
The source project was around 10GB. Source size is not build size, because Unity only ships what is referenced, but a platform switch reimports everything so the audit came first.
What went before I switched target:
- 552MB of unreferenced audio. 110 of 148 clips were not used by anything, including a 223MB drone and a 107MB wind bed.
- 663MB of .unitypackage installers sat inside Assets. Unity never builds them, they just slow every import down.
- 1.4GB of EXR textures for one music box prop. 82 files, including single normal maps at 144MB and 124MB. A reachability scan showed the model never used those materials so they were never in the build anyway. Pure disk and import time weight.
Audio was the real payload at 997MB of referenced WAVs. WebGL only decodes Vorbis or AAC and long clips are the memory risk. Ambient beds went Compressed In Memory at Vorbis around 0.5, short stingers went Decompress On Load, and everything 3D positional got forced to mono because you cannot hear stereo from a point source anyway. Ambience got resampled to 22kHz. I avoided Streaming completely, it is unreliable on WebGL.
Lightmaps needed their settings chosen before baking rather than after. The structural win is not lightmapping the props at all, so only large static surfaces get lightmaps and everything else rides light probes. Hundreds of small objects each claiming atlas area is what bloats a bake. Non-Directional mode as well, because Directional bakes a second texture set and roughly doubles the data for range you will not see at night.
On rendering, depth and opaque textures went off because each is a full screen pass and nothing needed them once depth of field was dropped outside the examine view. Shadow distance came down to 15 to 20m on a single cascade. Occlusion culling is baked, which is a big win in an interior because the house's own walls occlude most of the scene.
For delivery it is Brotli, IL2CPP set to faster and smaller builds, engine code stripped, and IndexedDB data caching on so repeat visits are quick.
The result at the moment is a 64MB compressed build that runs the whole slice in a desktop browser tab. Porch, sealed hall, office safe, the bedroom, the basement and the ending.
One warning worth passing on because it produces the most misleading error message in WebGL work. Brotli needs the host to send the right Content-Encoding header. Without it the browser gets Brotli bytes it thinks are plain text, and you get a syntax error pointing at a JavaScript file that is completely fine.
What I would keep
The two temperature rule, the four light budget, and authoring every beat instead of randomising any of them. All three are constraints and all three made the work faster rather than slower, because a constrained decision is one you can actually finish making.
Play the slice in your browser
Back to Part 1 on the story and Part 2 on the systems.
If you are weighing up a real-time or WebGL project and want a straight answer on what will survive the browser, give me a shout.
