How Winterfell was made
One sentence, one sword, six photographs that never happened, and a sheet of frost you have to wipe away yourself. This is the thinking behind the site, the parts that were thrown away, the shaders, and the numbers.
- The brief, and the version that was rejected
- One idea: it gets colder
- Mood board and palette
- The sword
- Through the glass
- The page is a window
- Every scene has its object
- Scroll turns one scene into the next
- Six photographs that never happened
- The frost wipe
- Snow, grain and the grade
- The depth layer
- Sound
- The reel contract
- Engineering and the numbers
- Before and after
- What is not here yet
The brief, and the version that was rejected
The brief was a flagship portfolio piece: a fan concept for Winterfell that has to stand next to what the studios ship, not next to other fan sites. Twenty-two sections of acceptance criteria, from the one-sentence concept to the security headers. Everything original: no footage, stills, music, marks, typefaces or models from the series, so the credit can actually be claimed.
The first version was built as a real-time castle: procedural block walls, a terrain, a forest, a crypt and a weirwood, all modelled in code and lit in three.js. It worked, it ran at sixty frames, and it was rejected on sight, because it read as a game. The bar was photographs. That version is kept below as the honest before. Everything in it except the snow was deleted.
There were no paper sketches. The first build was the wireframe, and the lesson it taught set the whole second build: anything that has to read as real in a browser in 2026 is either a photograph or a baked object seen from a camera that never breaks the illusion. The second version was designed in the scene, one decision at a time, and every one of those decisions is written down with its reason in the project's decisions log.
One idea: it gets colder
The sentence a stranger should repeat a week later: the site gets colder the deeper you go. It opens on warm torchlight and late summer at the gates. By the Wall the glass you are looking through has frosted over, and the only thing left to do is wipe it. Every call below follows from that sentence.
| Scene | Time | Light | Frost on the glass |
|---|---|---|---|
| The Gates | late summer, dusk | torches, 3200 K | 0 |
| The Courtyard | grey dusk | overcast, one brazier | 0.1 |
| The Great Hall | night, inside | hearth and candles, cold windows | 0.22 |
| The Godswood | night, moon | moonlight on frost; the one red | 0.32 |
| The Crypts | no time | torch pools in black | 0.5 |
| The Wall | a dawn that never warms | the ice itself, 10000 K | 1.0 |
Three things were locked before any code: the concept above; the opening, a photoreal sword that pierces the screen; and the one interaction built perfectly rather than five built halfway, the frost wipe. The weirwood red is the only saturated colour on the site and it appears only in the godswood.
Mood board and palette
- charcoal#0b0e12
- slate#1a2129
- iron#3a434d
- mist#a9b3bd
- frost#e9eef3
- ember#d98b45
- weirwood#b8231c
Six tokens and one accent, and every pixel on the site is one of them. The ember is a light, not a brand colour: it appears where fire is. Type is IM Fell English SC for display, inscribed with an inner-shadow filter so the titles read as cut into stone lit from above, and Spectral for everything else. Both are open licence and self-hosted. No italics, no monospace, no em dashes anywhere.
The sword
The hero object is a longsword of our own design: a diamond-section blade with a shallow fuller, a slender cross, a leather-wrapped grip and a wheel pommel carrying a direwolf head in relief. It was not sculpted and it was not generated from an image, because both give lumpy geometry at the edge where a sword has to be exact. It is built by a script in Blender 5.2, run headless: every part is lofted from analytic cross-sections, so the edge is an edge.




The materials are procedural Cycles node trees, baked to colour, roughness and normal maps at 4096 px per part, then packed into one glTF with Draco meshes and WebP textures: 3.1 MB with the 4K maps, 1.5 MB with the 2K maps that phones and laptops get. Three traps are worth writing down. Metals bake black in a colour pass unless Metallic is set to zero for that pass and restored after. The GPU bake crashed silently on Metal, so the bake runs on the CPU with full logging. And three.js skips tone mapping when it renders into a render target, which the post stack always does, so the sword's material applies its own ACES curve in the shader, or the blade blows out white.



Through the glass
The screen is the glass. The sword is a world object that flies in along its own axis from seven metres out in the storm, and the point ends 8.5 centimetres in front of a plane half a metre from the lens. That plane is where the two halves of the trick meet.
The first build drew the sword twice: a world canvas behind the page with the blade clipped to behind the glass, and a transparent canvas above the whole interface with the point clipped to in front of it, so the steel could hang over the page's own type. It worked, to the pixel, as long as both cameras were the same camera. It also split the lighting, the depth of field and the post stack in two, and left the glass nothing to refract. The second build put the type in the scene instead, and one canvas draws everything; the section after next says how.


The crack is a post-processing effect, not geometry, and it was rebuilt once: the first version was a Voronoi cell web across the whole screen, and it read as a honeycomb overlay rather than glass. Impacted glass is a small crushed star at the entry, eight to fourteen radial cracks that kink, branch and end at uneven lengths, a few ring cracks that run from one radial to the next near the point, chipped shards between them, and clear glass beyond about a third of the frame. The atlas stores the refraction tilt (the chips, and a thin lens along every line), the line intensity and the stress whitening at the point; the fracture grows outward from the entry over 0.42 seconds rather than appearing whole; a crack renders as a dark hairline that the light catches in stretches, with the chromatic split only where there is a line to split. The effect is centred on the point where the blade's axis crosses the glass plane, projected with the current frame's camera, so the web stays nailed to the entry wound while the blade keeps moving. Impact is declared when the point touches the glass, not when the blade comes to rest a little further through; the ease that flies the sword inverts in closed form, so that moment is exact.
The sword itself was re-aimed in the same pass. The first version entered side-on from the right, small, and its cross read as a pipe. It now drives point-first at the lens, strongly foreshortened, the point large and close and the length receding up and to the right into the storm, where the world's fog takes the hilt. The key light sits on the lens's side so the flat that faces you is the lit one, and the ripple reads on the near blade.
The page is a window
The second rejection was the one that mattered. Eli's words: the website is flat, there should be floating pieces and fragments around the place, stuff needs to look like it is popping off the page. He was right, and the diagnosis was simple once said: a photograph behind glass is still a photograph. Depth is only felt when things cross the page.
So the glass breaks for real. At the strike the sheet bursts into a hundred and seventy shards, most of them within a hand of the point, that fly outward and at the lens; a quarter keep going past the camera, and the rest hang in the air around the carved name. Each shard is a thin extruded sliver of real glass: three's transmission pass refracts the scene behind it, dispersion splits the colours on the brightest edges, a faint green sits in its thickness, and a pushed Fresnel edge makes a sliver read as an edge of glass rather than as nothing.
WINTERFELL itself is in the scene: the ten letters extruded from the IM Fell English outlines as pale weathered granite, bevelled wide enough for the bevel to catch the light, soot in the cut and frost on the faces that look up, hanging on the blade's own line a hand short of the guard so the steel comes through the type, as the reference does (one object mid-action punching through the headline). The page's own h1 stays for readers and search, visually hidden while the scene runs. Behind and around the name floats the debris of the place, in a strict order: the sword first, the shards second, and the debris third. Granite chips and forged iron (a bent nail, a hinge plate, a chain link, a band), built and baked in Blender like the sword, instanced in three bands: ten chips right before the lens, small and soft and at the frame's edges; a sparse middle band; and far specks in the gate's air. The name and the copy lane stay clear. Embers rise from the torches. The pointer's parallax is more than doubled. As you scroll, the lens flies two and a half metres forward through the field before the whiteout takes you to the courtyard.
One canvas means one depth of field: focus sits between the point and the name, the near chips and the deep gate go soft, and the shards, the debris, the embers and the storm all share the composer's bloom, grain and vignette. The first attempt kept a second, transparent canvas over the page for anything in front of the glass and spent a day on its consequences (nothing to refract, a depth of field that turned the page black, a seam at the glass plane to hold to the pixel); putting the type in the scene removed all three at once. Every one of these layers is a pure function of time, so the reel's last frame still equals its first.
Every scene has its object
The opening set the grammar and the other five scenes follow it: a photograph with depth at the back, the scene's name cut in stone in the middle of the air, one object caught in the act going through the letters, and the place's own debris floating at every depth between them and the lens. The names are not typed into the scene by hand. Each scene's h2 is laid out by the browser in a hidden box at a giant size, so a phone wraps it the way its CSS would, every character's line box is read back, and the letters are cut at those positions a couple of metres into the scene. They arrive out of the dark behind, one after another, turning as they come, and when the lens flies on they open at the middle and swing past it. The h2 stays in the page for readers, search and the static version.
The objects are the scenes' own. In the courtyard the glass that broke at the gates comes together again as the gate itself: a portcullis of jagged ice being hauled up through THE COURTYARD, which the lens flies under. In the Great Hall an iron chandelier, knocked swinging, goes through THE GREAT HALL with its twelve candles lit, its ring in front of the letters on one side and behind them on the other. In the godswood a gust tears the heart tree's leaves loose and pours them through THE GODSWOOD and past the lens, crimson and translucent, the only red on the site. In the crypts a torch turns end over end through THE CRYPTS and is the only light the letters get. At the Wall the word is cut in the ice, under the frost, and the wipe uncovers it.




Scroll turns one scene into the next
The scenes do not cut to each other. Twelve sparks rise out of the courtyard's air, ride the lens through the whiteout and land on the chandelier's twelve wicks, where they become the hall's candle flames. When the hall is left behind the flames lift off the wicks, swirl in front of the lens and cool into twelve leaves, still lit by the fire for a moment, that join the godswood's gust. Twelve of them are let go before the cut, fall slowly through the crypts in the torchlight, and at the Wall rime grows over each one until it breaks into a burst of real ice that goes back through THE WALL. Each of these is a pure function of the scroll position and the reel's periodic clock, so scrolling up plays it backwards and the reel still loops on itself.


Six photographs that never happened
Nothing modelled in real time was going to read as a photograph at 4K, so the environments are photographs. They were generated from our own prompts (a named camera, lens, film stock and ISO on every one, so the model reaches for optics rather than illustration), one 16:9 and one 9:16 composition per scene, because a phone deserves its own framing and not a crop. Twelve plates at 3840 by 2160.
A flat photograph dies the moment the camera moves. Each plate gets a depth map from Depth Anything V2 Large, run locally on the Mac, and is shown as a dense displaced mesh: the depth drives the relief, the depth's gradient gives a normal for a touch of relighting, and distance fades into fog. The camera is allowed to dolly, drift and tilt within the depth budget, and never further, because a plate has no back. The walk between scenes is a push-in through a brief, thin mist of snow, well under a beat, so what carries one scene into the next is always seen going through it and the frame is never empty. The frost on the glass follows the same idea: none at the gates, in the courtyard or in the hall, a creep at the edges in the godswood, a frame in the crypts, and all of it at the Wall.




The frost wipe
The frost is the concept made touchable: the glass gets colder scene by scene, and at the Wall it has frozen over completely. You drag to clear it and it refreezes slowly behind your hand. On a phone it is a finger; on a desk it is the mouse; in the reel it is a scripted finger driving the same code.
It is one fragment shader on the post stack. A coverage field decides, per pixel, at what cold level that spot freezes: the edges of the glass first and the centre last, shaped by slow noise and by the crystals themselves so the front advances as feathers rather than a fog. The crystals are generated macro photographs of hoar frost, two of them, star growth for the broad structure and fern fronds rotated for the veins, mirror-repeated so no seam shows, and everything is measured against the short side of the screen so a phone gets the same ferns as a monitor. Where there is frost, the scene behind is refracted by the crystal's gradient, blurred to milk between the ridges, and lit with a small specular so the ridges catch the light.


The wipe lives in a ping-pong render target: the red channel is cleared glass, painted from the pointer's trail with a soft brush and refreezing at 0.045 per second, and the green channel is breath, which blooms where a pointer rests and fades. How much of the Wall you have revealed is not read back from the GPU; it is the work the pointer has done there, accumulated on the CPU, and it is what fades in the Wall's one line of copy and unlocks the ending.


Snow, grain and the grade
The storm is a GPU particle system of flakes, seeded, drawn as short shutter streaks from each flake's position this frame to its position last frame, so they read as snow on film rather than dots. No two flakes are the same shape: a few lobes on the outline, phased by each flake's seed. Near the lens a flake is out of focus, so it grows into a wide, faint disc, the lens's bokeh; far away it is a small crisp fleck. The flakes swirl around the pointer through the same trail buffer the frost reads, and their density and the dust in the air change per scene. Film grain is a periodic hash whose seed is the reel's time, so the loop seals; bloom is small; depth of field is reserved for the two upper tiers; the vignette is 0.3. Every lit material carries the ACES curve in its own shader, and the finished frame gets one more grade on top, per scene: a black point, an S-curve in display space and a split tone (cold shadows, the torches' warmth kept in the highlights). The first version of this site had lifted blacks and read as a hazy beige photograph; the grade is what made the gates a night. The scene's own fog only starts at depth, so the hilt and the far debris sink into the air of each place, night blue in the godswood and black in the crypts, while the name and the point stay clean.
The depth layer
Spectacle alone is a demo reel. Under it there are forty-seven lore entries, a Stark family tree and a map of the North, all fed from one typed content layer: JSON in the repository, validated at build against the TypeScript types, read through a ContentSource interface with a local adapter today and a hosted-CMS adapter stub for tomorrow. The lore is prerendered to static HTML at build, so every entry has its own title, description and share card, is crawlable, and opens with a cross-document view transition that feels like turning a page rather than a reload. The six scenes are deep-linkable paths as well, and the address follows the scroll.



Sound
Off by default, remembered, started only by a gesture. One ambient bed per scene (wind and a campfire at the gates, chimney wind in the courtyard, the hearth and a room tone in the hall, a low drone in the godswood, dark air and a fire loop in the crypts, hurricane gusts and an ice field at the Wall), each a twenty-second loop cut from the Sonniss GDC library and levelled to minus 27 LUFS, crossfaded as the story moves. Short hits for the moments: the blade's flight, the pierce (four layered transients built to land on the same frame), the crack's ticks, a torch, a gust, the door. A looping rub while a finger wipes the frost, its level following how hard the finger moves. Every meaningful sound has a text caption in a live region, and the mix runs through a compressor so it never clips.
The reel contract
The showreel is not a separate render. With ?reel=1 the site becomes a deterministic fifteen-second film at thirty frames per second: time comes only from performance.now(), which the capture tool replaces with a stepped clock; the scroll, the finger and the sword are pure functions of that time; and frame 450 must equal frame 0, so the loop is seamless when it plays on a phone. Getting the seam to close meant removing every piece of history from the reel path: no damped camera, no running noise clock, and the previous frame's camera for the snow streaks computed from the same pure functions rather than remembered. The measured seam is a mean absolute difference of 0.04 on a 0 to 255 scale between the last frame and the first.




Engineering and the numbers
The site detects the GPU and picks a tier before anything loads. Tier 1 is a phone or an integrated GPU: 2K sword maps, a 512 px environment, a 1024 px crack atlas, medium plates, eight thousand flakes, no depth of field. Tier 2 adds depth of field and twenty thousand flakes. Tier 3, a discrete or Apple GPU on a large screen, gets the 4K maps, the full plates and forty-eight thousand flakes. A governor watches the frame time and steps the resolution down twice before it turns the frost's refraction off. Rendering pauses when the tab is hidden, and every geometry, texture and render target is disposed when its component leaves.
The first paint is a real image: the gates plate at 1280 px, 117 KB, declared in the HTML with a high fetch priority, so the largest contentful paint is the loader's backdrop at the first second rather than the title after every asset has landed. The loader's rising snow moves by transform, never by height, so it is not a layout shift. Three.js, the renderer and the scene boot behind a dynamic import, so the first bundle is the interface alone.
| What | Tier 1 (phone) | Tier 3 (desktop) |
|---|---|---|
| Interface bundle (main), gzipped | 40 KB | 40 KB |
| three.js, the renderer, React and the scene, gzipped, behind the dynamic import | 466 KB | 466 KB |
| A lore page, all of its script, gzipped | 90 KB | 90 KB |
| The sword (Draco, WebP) | 1.37 MB | 3.11 MB |
| Environment map | 0.44 MB | 1.74 MB |
| Crack atlas (lossless WebP) | 0.44 MB | 0.54 MB |
| Frost crystals, two | 1.0 MB | 3.2 MB |
| The chandelier (Draco, WebP), loaded a scene ahead | 0.85 MB | 2.58 MB |
| A scene's fragments, loaded a scene ahead | 0.4 to 1.1 MB | 1.7 to 3.0 MB |
| A plate, per scene, streamed | 0.56 MB | 1.7 to 2.0 MB |
| Fonts, three files | 102 KB | 102 KB |
| Sound, all nineteen cues, on demand | 2.2 MB | 2.2 MB |
| Lighthouse 13, local build | Desktop | Mobile (throttled) |
|---|---|---|
| Performance | 100 | 95 |
| Accessibility | 100 | 100 |
| Best practices | 100 | 100 |
| SEO | 100 | 100 |
| Largest contentful paint | 0.6 s | 2.4 s |
| Cumulative layout shift | 0.002 | 0.001 |
| Total blocking time | 30 ms | 140 ms |
The honest reading of that table: the static layer scores where a static site scores, and the WebGL layer costs blocking time at boot, because compiling the post stack's shaders and uploading textures is work the main thread has to do once, and Lighthouse's mobile run draws WebGL on a throttled CPU. The carved headlines were one new cost this pass added: cut all at once at boot they blocked the main thread for over half a second, so each glyph is now extruded once and kept, the cache warms one glyph at a time after the visitor's first move, and a scene's name is assembled from it only as the scene comes near. The other was the boot itself. A profile showed the first frames drawn behind the loader compiling every shader synchronously and uploading every texture in one task. Now the scene stays hidden while it loads, every program compiles in parallel once the files are in, the textures go up one per frame, and only then is the scene shown and the loader told; phones get the same models with textures a quarter the size, because a fragment there is a few dozen pixels wide. Mobile went from 81 to the mid nineties with nothing taken out of the picture. In a headless Chrome on an M4 the site holds sixty frames per second at 1440 by 900 with every effect on, and the phone tier holds it on a 390 by 844 profile at a device scale of three. The checks that produced these numbers, the screenshot matrix at four viewports, the synthetic finger, the keyboard walk, the still and static modes, and the reel's loop seam, are scripts in the repository and ran before this page was published.
Under the WebGL there is semantic HTML: real headings, one landmark per region, every scene's copy in the page before any script runs, skip links to the content and past the intro, keyboard navigation (j and k walk the journey, Escape closes the menu), captions for sound, a reduced-motion mode that honours the system setting and can be chosen from the menu, and a static mode that shows the plates as stills with every word in place. Each page has its own title, description, canonical URL, share card and structured data; there is a sitemap, a robots file, a web manifest, and the usual security headers with a content security policy that allows no inline script.
Before and after
The difference is not polish on the same approach. It is a different approach: a camera that never breaks a photograph, one baked object that is allowed to be close, and effects that live on the glass rather than in the world.
What is not here yet
Some of the brief's checklist is deliberately not in this version, and saying so is part of the case study. There is no composed score, only the beds. There is no direwolf character and no ravens; a baked object that close to the lens has to be perfect, and the wolf's head that was built read as a skull, so it was held back. The courtyard's gate and the Wall's ice are made from the opening's shards and the godswood's leaves rather than from new sculpted pieces, which is the idea, and also the budget. The content layer is CMS-ready but not CMS-hosted, because a hosted CMS is an account to create and pay for. There are no analytics, so the privacy page is one paragraph long. The textures ship as WebP inside the glTF rather than KTX2, because the Khronos encoder is a separate install. And the phone testing here is a simulator and a profile; the real-device pass is the next thing on the list.