<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Cinevva Blog</title>
        <link>https://app.cinevva.com</link>
        <description>Product updates, creator notes, and experiments.</description>
        <lastBuildDate>Sun, 12 Jul 2026 20:21:02 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright 2024-present Cinevva</copyright>
        <item>
            <title><![CDATA[A Breaker Belt: Snake meets Arkanoid, vibe coded in three days]]></title>
            <link>https://app.cinevva.com/blog/2026-02-18-a-breaker-belt-medium</link>
            <guid>https://app.cinevva.com/blog/2026-02-18-a-breaker-belt-medium</guid>
            <pubDate>Sun, 12 Jul 2026 20:16:35 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="a-breaker-belt-snake-meets-arkanoid-vibe-coded-in-three-days" tabindex="-1">A Breaker Belt: Snake meets Arkanoid, vibe coded in three days <a class="header-anchor" href="#a-breaker-belt-snake-meets-arkanoid-vibe-coded-in-three-days" aria-label="Permalink to &quot;A Breaker Belt: Snake meets Arkanoid, vibe coded in three days&quot;"></a></h1>
<p>We made a game where your snake is the paddle and the bricks fight back. It shipped on web, mobile, and PC. It took two of us about three days. On and off, not crunching. And it's fun to play.</p>
<h2 id="the-mashup-nobody-asked-for" tabindex="-1">The mashup nobody asked for <a class="header-anchor" href="#the-mashup-nobody-asked-for" aria-label="Permalink to &quot;The mashup nobody asked for&quot;"></a></h2>
<p>Snake is about growth and spatial awareness. Arkanoid is about reflexes and angle prediction. They come from completely different design philosophies, and mashing them together sounds like the kind of pitch that gets politely declined.</p>
<p>But <a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">A Breaker Belt</a> makes it work. You're a cosmic serpent, a living arc of neon current threading through an asteroid field of breakable blocks. Your head is the paddle. Your growing tail is both your greatest weapon and your most constant threat. The orbs ricochet off your body to shatter bricks, but one wrong turn into your own tail and you're done.</p>
<p>It's the kind of weird cross-genre experiment that usually dies before anyone gets to play it, because the development cost of finding out whether a weird idea works has traditionally been measured in months. Here it was measured in afternoons.</p>
<h2 id="what-actually-shipped" tabindex="-1">What actually shipped <a class="header-anchor" href="#what-actually-shipped" aria-label="Permalink to &quot;What actually shipped&quot;"></a></h2>
<p>The scope is what makes this interesting. This isn't a game jam prototype with placeholder rectangles and no sound.</p>
<p>The game runs 50 waves deep. That's not 50 variations of the same brick wall. The formations evolve from gentle onboarding arcs into fortress rings that demand angled shots through side gaps, then into layered diagonal mazes with one-tile-wide openings that require precision steering. Explosive bricks blow their neighbors apart. Phantom bricks flicker in and out of existence. Regenerating bricks heal back after you break them. Portal bricks teleport your orbs across the arena. Gravity wells bend your shots into slow, curving hymns. Laser emitters sketch red lines across the void. Mimic bricks look harmless until they decide they're not. By wave 15, you're navigating something that feels less like a puzzle and more like a living system that's learning your habits.</p>
<p>The music isn't a loop. It's a reactive soundtrack that builds with your gameplay. Bass, lead synth, pad, drums all running in E minor, and as the action intensifies, additional layers fade in. When things calm down between waves, ambient pads take over. The music breathes with you. A dedicated composer would spend weeks tuning that kind of responsiveness. Here it was part of the creative flow.</p>
<p>The sound effects aren't samples pulled from a free pack. Every brick shatter, orb bounce, and collision is synthesized in real time. Different pitch for head contacts versus tail contacts. Warm reverb for the spacey feel. When you chain a combo, the audio tells you before the screen does.</p>
<p>And then there's the storytelling. Each wave opens with a narrative beat delivered by AI-generated voice. Wave one: &quot;They call it the Breaker Belt: a ribbon of engineered debris that circles the old star like a warning.&quot; By wave 38: &quot;The Belt stops feeling like a wall and starts feeling like a mind. It tests not your reflexes, but your habits.&quot; It's 50 chapters of cosmic mythology that makes you care about why you're a snake breaking bricks in space.</p>
<p>The backgrounds evolve too. Early waves are calm indigo starfields with soft meteor rain. By mid-game, aurora bands and nebula clouds appear. Late game drops you into Ion Storm territory with sharp cyan streaks against near-black space. The game communicates progression through atmosphere as much as difficulty.</p>
<p>All of this runs on keyboard, gamepad, or touchscreen. Published to web, mobile, and PC from one codebase.</p>
<h2 id="the-team-that-wasn-t-needed" tabindex="-1">The team that wasn't needed <a class="header-anchor" href="#the-team-that-wasn-t-needed" aria-label="Permalink to &quot;The team that wasn't needed&quot;"></a></h2>
<p>A game with this depth would typically need a game designer, a couple of programmers, an artist, a sound designer, a composer, a level designer, a narrative writer, and QA. Eight or nine people. Three to six months of coordinated work. Standups, Jira tickets, asset pipelines, platform-specific debugging.</p>
<p>Two of us made this in a long weekend on the <a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">Cinevva Engine</a>.</p>
<p>The talent required didn't change. The ratio between creative intent and implementation overhead did. The time was spent deciding what the game should feel like, not fighting tools to make it happen.</p>
<h2 id="why-this-matters-if-you-make-things" tabindex="-1">Why this matters if you make things <a class="header-anchor" href="#why-this-matters-if-you-make-things" aria-label="Permalink to &quot;Why this matters if you make things&quot;"></a></h2>
<p>The interesting question isn't whether AI tools can help make games faster. That's been answered. The interesting question is what happens to ideas that used to be too risky to try.</p>
<p>&quot;Snake but you're the paddle in an Arkanoid arena&quot; is not something a producer greenlights. A few indie devs have tried variations on itch.io (<a href="https://newdron.itch.io/breaksnake" target="_blank" rel="noreferrer">BreakSnake</a>, <a href="https://neop87.itch.io/snakeout" target="_blank" rel="noreferrer">SnakeOut</a>, <a href="https://merrak.itch.io/snake-break" target="_blank" rel="noreferrer">Snake Break</a>), but they're all small game jam experiments. The genre fusion has never been given a real production pass with reactive music, AI narration, dozens of brick types, and 50 waves of escalating design. In traditional development, that kind of polish on a risky concept gets killed in a meeting before anyone writes a line of code.</p>
<p>When trying something weird costs an afternoon instead of a quarter's budget, the strange ideas get built. Some of them turn out to be genuinely good. A Breaker Belt is one of those.</p>
<h2 id="play-it-or-make-your-own" tabindex="-1">Play it. Or make your own. <a class="header-anchor" href="#play-it-or-make-your-own" aria-label="Permalink to &quot;Play it. Or make your own.&quot;"></a></h2>
<p><a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">A Breaker Belt</a> is playable right now on web, mobile, and PC. If it makes you want to build something, the <a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">Cinevva Engine</a> is free to use. Describe what you want, iterate on what comes back, ship when it's ready. The <a href="https://app.cinevva.com/tools/music" target="_blank" rel="noreferrer">music</a>, <a href="https://app.cinevva.com/tools/sfx" target="_blank" rel="noreferrer">sound effects</a>, <a href="https://app.cinevva.com/tools/flux" target="_blank" rel="noreferrer">art</a>, and <a href="https://app.cinevva.com/tools/hunyuan3d" target="_blank" rel="noreferrer">3D models</a> are all built in.</p>
<p>Your weird game idea might be three days away from existing.</p>
<hr>
<p><em><a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">Play A Breaker Belt</a> | <a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">Build your own game</a> | <a href="https://cinevva.com/charts" target="_blank" rel="noreferrer">Browse community games</a></em></p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What It Actually Takes to Build an Open World in a Browser]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-series-medium</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-series-medium</guid>
            <pubDate>Sun, 12 Jul 2026 20:16:35 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="what-it-actually-takes-to-build-an-open-world-in-a-browser" tabindex="-1">What It Actually Takes to Build an Open World in a Browser <a class="header-anchor" href="#what-it-actually-takes-to-build-an-open-world-in-a-browser" aria-label="Permalink to &quot;What It Actually Takes to Build an Open World in a Browser&quot;"></a></h1>
<p>A small team at Cinevva just published one of the most transparent engineering journals in recent game-dev memory: a 12-part series documenting their attempt to build a multiplayer open world that runs entirely in the browser. No download. No app store. Just a URL.</p>
<p>The project spanned 24 technical experiments they call &quot;spikes&quot; -- short, focused prototypes designed to answer one risky question each. Every spike shipped with live source code you can open and run in your browser right now. The series was written by Oleg Sidorkin, CTO and co-founder of Cinevva, and it reads less like marketing and more like a field journal from the frontlines of what browsers can actually do in 2026.</p>
<p>What makes the series worth reading -- even if you never plan to build terrain systems -- is the method underneath. It's a case study in how to de-risk an ambitious project before you've committed to anything expensive.</p>
<h2 id="starting-with-the-hardest-question-first" tabindex="-1">Starting with the hardest question first <a class="header-anchor" href="#starting-with-the-hardest-question-first" aria-label="Permalink to &quot;Starting with the hardest question first&quot;"></a></h2>
<p>Most open-world projects die in a predictable sequence. First you get a beautiful concept. Then a pretty scene. Then you discover that your frame budget was already spent before gameplay existed.</p>
<p>Cinevva's team inverted the order. Their first spike was deliberately ugly: a 512-meter terrain mesh, 500 instanced objects, procedural height noise, a water plane, and fog. No shadows, no beauty pass. The only question was whether a browser could hold a stable frame rate while the camera moved through it.</p>
<p>It could. And that &quot;yes&quot; established something Sidorkin calls a &quot;baseline contract&quot; -- a measured reference cost for a minimal scene that every subsequent feature had to justify itself against. If a new effect looked great but blew the frame budget, it didn't ship. Not yet, anyway.</p>
<p>That kind of discipline sounds obvious. In practice, it's rare in fast-moving prototype environments where everyone is excited about the next visual win.</p>
<h2 id="the-physics-gamble" tabindex="-1">The physics gamble <a class="header-anchor" href="#the-physics-gamble" aria-label="Permalink to &quot;The physics gamble&quot;"></a></h2>
<p>The second experiment tackled an architectural debate that divides browser game developers: should physics run on the main thread, where it's simpler, or in a Web Worker, where it can't block rendering?</p>
<p>Worker-based physics is cleaner on paper. In practice, the fear is latency. Every input event has to cross a message boundary twice: once to reach the worker, once to bring back the result. If that round trip is too slow, pressing a key and seeing your character move will feel sluggish.</p>
<p>Sidorkin's team integrated the Rapier physics engine (compiled from Rust to WebAssembly) in a dedicated worker, wired up the message pipeline, and measured. The overhead was negligible. Controls still felt immediate. But the team was careful to note that they'd validated one specific scenario, not a universal rule. When GPU pressure and streaming complexity changed later, assumptions would need rechecking.</p>
<h2 id="the-boring-spikes-that-saved-the-project" tabindex="-1">The boring spikes that saved the project <a class="header-anchor" href="#the-boring-spikes-that-saved-the-project" aria-label="Permalink to &quot;The boring spikes that saved the project&quot;"></a></h2>
<p>Part three of the series has no screenshots. It covers three experiments that looked unglamorous but carried product-level consequences.</p>
<p>The first tested whether Cloudflare Durable Objects could handle real-time position broadcasts at game-like tick rates -- the multiplayer backbone. If this had failed, the entire network architecture would have needed early sharding rather than single-island ownership.</p>
<p>The second validated a mobile quality profile: not a desktop preset renamed, but an explicit low-cost rendering path from the same terrain baseline. The question was whether the world could remain readable and responsive under mobile GPU constraints without rewriting the renderer.</p>
<p>The third evaluated whether AI-generated behavior scripts for creator workflows would be reliable enough for production use.</p>
<p>None of these produced demo reels. All three set hard boundaries that shaped every architectural decision afterward. Sidorkin writes that these &quot;unflashy spikes changed architecture faster than visual spikes did.&quot;</p>
<h2 id="streaming-where-pretty-projects-fall-apart" tabindex="-1">Streaming: where pretty projects fall apart <a class="header-anchor" href="#streaming-where-pretty-projects-fall-apart" aria-label="Permalink to &quot;Streaming: where pretty projects fall apart&quot;"></a></h2>
<p><img src="./spike-6-chunk-streaming.png" alt="Spike 6: Chunk streaming in action. Each colored area is a terrain chunk that loads and unloads dynamically as the camera moves. The HUD shows 120 FPS with 29 active chunks and sub-millisecond unload times."></p>
<p>You can hide a lot in a still frame. You can't hide a 40-millisecond stutter when crossing a chunk boundary while running.</p>
<p>The team tested streaming before building advanced terrain, deliberately separating concerns. Spike 6 validated neighborhood chunk loading with simple content. Only after that clean signal did Spike 11 introduce compressed heightmap streaming with progressive refinement -- loading terrain at 17-sample resolution first, then 33, then the full 65-sample grid.</p>
<p>The sequencing mattered more than they expected. Had they started directly with compressed height chunks, every hitch would have been ambiguous. Was it a decode issue, a texture upload stall, or a geometry update problem? Testing simple streaming first eliminated one entire category of uncertainty.</p>
<p>A practical lesson emerged: measure upload stalls directly, not through average FPS. Averages hide frame spikes, and frame spikes are what players actually feel.</p>
<h2 id="the-visual-budget-wars" tabindex="-1">The visual budget wars <a class="header-anchor" href="#the-visual-budget-wars" aria-label="Permalink to &quot;The visual budget wars&quot;"></a></h2>
<p>Three separate experiments attacked rendering costs in isolation rather than bundling them. Vegetation density and wind animation. Multi-layer terrain materials with triplanar mapping for cliff faces. Cascaded shadow maps under realistic terrain load.</p>
<p>The vegetation spike revealed that batching instances into fewer meshes mattered more than reducing per-blade polygon count. The material spike found that triplanar projection on vertical surfaces was worth the GPU cost, but adding a fifth texture splat layer wasn't. The shadow spike determined that three cascades at 1024 resolution delivered acceptable contact shadows without exceeding 2 milliseconds of GPU time.</p>
<p>The team adopted a blunt rule: a feature moves forward only if it can explain its cost with measured frame-time data. That constraint, set early, made later architectural decisions around volumetric terrain and clipmaps significantly cleaner.</p>
<h2 id="the-pivot-that-changed-the-project-s-trajectory" tabindex="-1">The pivot that changed the project's trajectory <a class="header-anchor" href="#the-pivot-that-changed-the-project-s-trajectory" aria-label="Permalink to &quot;The pivot that changed the project's trajectory&quot;"></a></h2>
<p>Before Spike 10, the team's mental model was &quot;bigger world means more geometry.&quot; After Spike 10, it became &quot;constant geometry budget, camera-centered ring updates.&quot;</p>
<p>Geometry clipmaps -- concentric rings of terrain centered on the camera, each ring progressively coarser -- meant the triangle count stayed roughly constant regardless of draw distance. The practical trick was geomorphing at ring boundaries: smoothly blending vertex heights in the shader so that the shift between resolution levels is invisible in motion.</p>
<p>A subtle lesson came from testing methodology. Clipmaps look fine in screenshots. They reveal their artifacts only under sustained camera movement through ring boundaries. The team spent time running constant-speed traversals and watching for temporal noise. &quot;Screenshots lied,&quot; Sidorkin writes. &quot;Motion told the truth.&quot;</p>
<h2 id="going-underground" tabindex="-1">Going underground <a class="header-anchor" href="#going-underground" aria-label="Permalink to &quot;Going underground&quot;"></a></h2>
<p>Heightmaps can't represent caves. They store one elevation value per point on a grid. The moment you need tunnels, overhangs, or carved rock faces, you need volumetric terrain.</p>
<p>Spike 12 implemented marching cubes on the GPU using WebGPU compute shaders, extracting triangle meshes from a 3D signed distance field. Four 64-cubed chunks ran simultaneously with per-frame mesh updates from animated SDF edits. The compute shader handled everything -- evaluating the field, classifying cells, emitting vertices -- without any CPU readback.</p>
<p>The challenge wasn't making it work. It was making it work alongside everything else. Integration with Three.js's scene graph, buffer lifecycle management (WebGPU buffers can't be resized), fence handling to avoid destroying GPU resources still in flight -- the series devotes two full parts to what they call &quot;incremental hardening,&quot; the unglamorous process of adding one capability at a time and verifying the previous layer still functions after each addition.</p>
<h2 id="the-seam-nightmare" tabindex="-1">The seam nightmare <a class="header-anchor" href="#the-seam-nightmare" aria-label="Permalink to &quot;The seam nightmare&quot;"></a></h2>
<p>The most technically harrowing section of the series spans Parts 9 through 11, covering what happens when terrain chunks at different resolutions meet.</p>
<p>When a high-detail chunk sits next to a low-detail chunk, their independently generated meshes don't align at the boundary. The result is visible cracks, flickering edges, and T-junctions where light bleeds through. The Transvoxel algorithm solves this with special transition cells that bridge resolution differences -- but implementing it correctly across all chunk configurations, with consistent winding order, proper buffer management, and accurate draw ranges, consumed six separate experiments.</p>
<p>The team's most memorable debugging story: two days chasing a seam artifact they blamed on transition logic. The actual culprit was stale data. The GPU compute shader wrote N vertices into a buffer, but the draw call was still configured to render N+M vertices from the previous frame. Those extra vertices contained garbage that produced flickering razor-thin triangles. One line fix: clip the draw range to the atomic counter's active vertex count.</p>
<p>&quot;Rendering bugs often masquerade as meshing bugs,&quot; Sidorkin observes. &quot;The geometry was correct the whole time.&quot;</p>
<h2 id="from-chaos-to-governance" tabindex="-1">From chaos to governance <a class="header-anchor" href="#from-chaos-to-governance" aria-label="Permalink to &quot;From chaos to governance&quot;"></a></h2>
<p>After the seam battle, the team replaced ad-hoc chunk behavior with an explicit policy system. A central function now decided each chunk's LOD level, rendering mode (heightmap or marching cubes), and which faces needed transition cells. Distance rings determined the base LOD. An adjacency constraint ensured no two neighboring chunks differed by more than one resolution level. An edit bitmap kept volumetric chunks in marching-cubes mode regardless of distance if they contained creator modifications.</p>
<p>Color-coded debug overlays -- green for heightmap chunks, blue for marching cubes, orange for transition faces -- turned &quot;I saw a bug somewhere near that ridge&quot; into &quot;the bug appears at position (142, 12, -67) facing northwest.&quot;</p>
<p>&quot;Policy didn't reduce complexity,&quot; Sidorkin writes. &quot;It organized complexity.&quot;</p>
<h2 id="what-it-adds-up-to" tabindex="-1">What it adds up to <a class="header-anchor" href="#what-it-adds-up-to" aria-label="Permalink to &quot;What it adds up to&quot;"></a></h2>
<p><img src="./open-world-browser-120fps.png" alt="The open world running in a browser at 120 FPS -- terrain, trees, physics, and a player capsule, all rendered in a single tab with no install required."></p>
<p>The final spike combined clipmap rings, per-fragment sky fog (sampling the actual skybox color in the direction of each terrain fragment), and Three.js module wiring into a unified demonstration. The result is a terrain system that layers near-field volumetric editing, mid-field heightmap chunks, and far-field clipmap rings under a policy layer that governs mode, LOD, and transitions.</p>
<p>The series closes with lessons Sidorkin says he'd repeat on any future project:</p>
<ul>
<li><strong>Start with risk spikes before feature work.</strong> Kill the &quot;can we even do this?&quot; questions before investing in content pipelines.</li>
<li><strong>Freeze known-good baselines before integration jumps.</strong> The day spent establishing a clean checkpoint saves multiple days bisecting regressions later.</li>
<li><strong>Force policy and observability before optimization marathons.</strong> Named conditions with trigger rules beat mystery bugs every time.</li>
<li><strong>Test under motion, not screenshots.</strong> Pops, flicker, and streaming hitches all hide in still frames.</li>
<li><strong>Measure per-feature frame time, not average FPS.</strong> Averages hide the spikes that users actually feel.</li>
<li><strong>Publish the messy parts.</strong> The wrong turns, the ghost hunts, the two days blaming the wrong system. Those are the parts people can actually learn from.</li>
</ul>
<h2 id="why-this-matters-beyond-cinevva" tabindex="-1">Why this matters beyond Cinevva <a class="header-anchor" href="#why-this-matters-beyond-cinevva" aria-label="Permalink to &quot;Why this matters beyond Cinevva&quot;"></a></h2>
<p>The series is significant for three reasons that extend past one company's terrain pipeline.</p>
<p>First, it demonstrates that WebGPU compute shaders, WebAssembly physics, and edge-deployed Durable Objects have crossed a threshold. A multiplayer open world with volumetric terrain, real-time editing, and streaming LOD is architecturally viable in a browser tab in 2026. That was not true two years ago.</p>
<p>Second, the spike methodology -- small, focused experiments that each answer one risky question with live, measurable results -- offers a template for any team attempting something that might not work. The discipline of measuring before committing, of establishing baselines before integrating, of naming edge cases before optimizing, applies far beyond terrain systems.</p>
<p>Third, the radical transparency is the point. Publishing source code for all 24 experiments, including the dead ends and the two-day debugging detours, makes this more than a technical blog. It's a public engineering notebook that treats the reader as a colleague rather than a customer.</p>
<p>The full series is available at <a href="https://app.cinevva.com/blog/2026-02-25-open-world-browser-series-guide" target="_blank" rel="noreferrer">Cinevva's blog</a>, with every spike running live in the browser.</p>
<hr>
<p><em>Cinevva is a game creation platform where anyone can build, publish, and share games using AI-powered tools. The open-world browser series was written by <a href="https://app.cinevva.com/about" target="_blank" rel="noreferrer">Oleg Sidorkin</a>, CTO and co-founder.</em></p>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/spike-6-chunk-streaming.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Social Media Posts — Open World Browser Series]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-social-posts</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-social-posts</guid>
            <pubDate>Sun, 12 Jul 2026 20:16:35 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="social-media-posts-—-open-world-browser-series" tabindex="-1">Social Media Posts — Open World Browser Series <a class="header-anchor" href="#social-media-posts-—-open-world-browser-series" aria-label="Permalink to &quot;Social Media Posts — Open World Browser Series&quot;"></a></h1>
<p><strong>Article</strong>: <a href="https://vio-202020.medium.com/de-risking-an-ambitious-project-before-committing-to-anything-big-like-an-online-open-world-c94a7c05ee06" target="_blank" rel="noreferrer">https://vio-202020.medium.com/de-risking-an-ambitious-project-before-committing-to-anything-big-like-an-online-open-world-c94a7c05ee06</a>
<strong>Blog Series</strong>: <a href="https://app.cinevva.com/blog/2026-02-25-open-world-browser-series-guide" target="_blank" rel="noreferrer">https://app.cinevva.com/blog/2026-02-25-open-world-browser-series-guide</a></p>
<hr>
<h2 id="linkedin-cinevva-company-page" tabindex="-1">LinkedIn (Cinevva Company Page) <a class="header-anchor" href="#linkedin-cinevva-company-page" aria-label="Permalink to &quot;LinkedIn (Cinevva Company Page)&quot;"></a></h2>
<p><strong>Schedule: Tuesday March 10, 10:00 AM ET</strong>
<strong>Image</strong>: <a href="https://app.cinevva.com/img/blog/open-world-browser-120fps.png" target="_blank" rel="noreferrer">https://app.cinevva.com/img/blog/open-world-browser-120fps.png</a></p>
<p>Our CTO Oleg Sidorkin just published the full engineering journal behind building a multiplayer open world that runs entirely in a browser. No download. No app store. Just a URL.</p>
<p>24 experiments. 12 parts. The series documents the full sequence -- wins, dead ends, and recovery patterns -- with live source code for every experiment.</p>
<ul>
<li>The first experiment was deliberately ugly -- measuring whether a browser could even hold a stable frame rate before investing in anything pretty</li>
<li>Physics in a Web Worker with Rapier compiled to WebAssembly -- and whether the input lag fear was justified (it wasn't)</li>
<li>The &quot;boring&quot; spikes that set hard boundaries on network architecture, mobile constraints, and AI behavior reliability</li>
<li>Streaming chunk transitions that expose 40ms hitches no screenshot can hide</li>
<li>GPU-driven marching cubes via WebGPU compute shaders for caves and overhangs</li>
<li>A six-experiment seam nightmare debugging Transvoxel transitions across mixed LOD chunks</li>
<li>The two-day bug hunt where the geometry was correct the whole time -- the draw call was reading past the buffer</li>
</ul>
<p>The methodology matters as much as the tech. Start with risk. Freeze baselines before integration jumps. Measure per-feature frame time, not averages. Test under motion, not screenshots.</p>
<p>Whether you're building games, tools, or anything ambitious in the browser -- the spike-based de-risking approach transfers.</p>
<p>#gamedev #WebGPU #webdev #softwareengineering</p>
<hr>
<h2 id="x-twitter-thread-—-cinevvagames" tabindex="-1">X (Twitter) Thread — @CINEVVAGames <a class="header-anchor" href="#x-twitter-thread-—-cinevvagames" aria-label="Permalink to &quot;X (Twitter) Thread — @CINEVVAGames&quot;"></a></h2>
<p><strong>Schedule: Tuesday March 10, 9:00 AM ET</strong>
<strong>Note</strong>: X/Zapier integration is text-only (no image field). Post images manually via X app after thread is live.</p>
<h3 id="tweet-1-main" tabindex="-1">Tweet 1 (main) <a class="header-anchor" href="#tweet-1-main" aria-label="Permalink to &quot;Tweet 1 (main)&quot;"></a></h3>
<p>Our CTO Oleg Sidorkin (linkedin.com/in/osidorkin) just published something unusual -- the full engineering journal behind building a multiplayer open world that runs entirely in a browser tab.</p>
<p>24 experiments. 12 parts. Every spike ships with live source code.</p>
<p><a href="https://vio-202020.medium.com/de-risking-an-ambitious-project-before-committing-to-anything-big-like-an-online-open-world-c94a7c05ee06" target="_blank" rel="noreferrer">https://vio-202020.medium.com/de-risking-an-ambitious-project-before-committing-to-anything-big-like-an-online-open-world-c94a7c05ee06</a></p>
<p>🧵</p>
<h3 id="tweet-2" tabindex="-1">Tweet 2 <a class="header-anchor" href="#tweet-2" aria-label="Permalink to &quot;Tweet 2&quot;"></a></h3>
<p>The methodology matters as much as the tech:</p>
<ul>
<li>First experiment was deliberately ugly -- just measuring if a browser can hold a stable frame rate</li>
<li>Physics in a Web Worker with Rapier compiled to Wasm</li>
<li>GPU marching cubes via WebGPU compute shaders for caves</li>
<li>A two-day bug where the geometry was correct the whole time</li>
</ul>
<p>No install. No app store. Just a URL.</p>
<h3 id="tweet-3" tabindex="-1">Tweet 3 <a class="header-anchor" href="#tweet-3" aria-label="Permalink to &quot;Tweet 3&quot;"></a></h3>
<p>Whether you're building games, tools, or anything ambitious in the browser -- the spike-based de-risking approach transfers.</p>
<p>Full series with all 24 live experiments: <a href="https://app.cinevva.com/blog/2026-02-25-open-world-browser-series-guide" target="_blank" rel="noreferrer">https://app.cinevva.com/blog/2026-02-25-open-world-browser-series-guide</a></p>
<p>#gamedev #WebGPU #webdev</p>
<hr>
<h2 id="instagram-cinevva" tabindex="-1">Instagram (Cinevva) <a class="header-anchor" href="#instagram-cinevva" aria-label="Permalink to &quot;Instagram (Cinevva)&quot;"></a></h2>
<p><strong>Schedule: Tuesday March 10 (or post anytime — Instagram is less time-sensitive)</strong>
<strong>Images (carousel)</strong>:</p>
<ol>
<li><a href="https://app.cinevva.com/img/blog/open-world-browser-120fps.png" target="_blank" rel="noreferrer">https://app.cinevva.com/img/blog/open-world-browser-120fps.png</a></li>
<li><a href="https://app.cinevva.com/img/blog/open-world-chunk-streaming.png" target="_blank" rel="noreferrer">https://app.cinevva.com/img/blog/open-world-chunk-streaming.png</a></li>
</ol>
<p>No download. No app store. Just a URL.</p>
<p>Our CTO Oleg Sidorkin published the full engineering journal behind building a multiplayer open world that runs entirely in a browser.</p>
<p>24 experiments called &quot;spikes&quot; -- each one designed to answer a single risky question before committing to anything expensive. Every experiment ships with live source code.</p>
<p>What stood out:</p>
<p>→ The first experiment was deliberately ugly. No shadows. No beauty pass. Just: can a browser hold 60fps while a camera moves through 500 objects?
→ Physics running in a Web Worker with Rust compiled to WebAssembly -- and the input lag everyone feared didn't show up
→ GPU-driven marching cubes via WebGPU compute shaders for caves and overhangs
→ A six-experiment nightmare debugging seams between terrain chunks at different resolutions
→ The two-day bug hunt where the geometry was correct the whole time -- the draw call was reading past the buffer</p>
<p>&quot;Screenshots lied. Motion told the truth.&quot;</p>
<p>The spike methodology applies far beyond games. Start with risk. Measure before committing. Publish the messy parts.</p>
<p>Full series link in bio.</p>
<p>.
.
.
#gamedev #gamedevelopment #webdev #webgpu #indiedev #indiegames #browsergames #openworld #javascript #threejs #webassembly #softwareengineering #techblog #buildinpublic #cinevva</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Fact-check note: "From sketch to video and playable 3D"]]></title>
            <link>https://app.cinevva.com/blog/2026-03-04-sketch-to-animated-3d-character-factcheck-note</link>
            <guid>https://app.cinevva.com/blog/2026-03-04-sketch-to-animated-3d-character-factcheck-note</guid>
            <pubDate>Sun, 12 Jul 2026 20:16:35 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="fact-check-note-from-sketch-to-video-and-playable-3d" tabindex="-1">Fact-check note: &quot;From sketch to video and playable 3D&quot; <a class="header-anchor" href="#fact-check-note-from-sketch-to-video-and-playable-3d" aria-label="Permalink to &quot;Fact-check note: &quot;From sketch to video and playable 3D&quot;&quot;"></a></h1>
<p><strong>For:</strong> Oleg Sidorkin<br>
<strong>Re:</strong> <a href="https://app.cinevva.com/blog/2026-03-04-sketch-to-animated-3d-character" target="_blank" rel="noreferrer">app.cinevva.com/blog/2026-03-04-sketch-to-animated-3d-character</a><br>
<strong>Checked against:</strong> cinevva-site repo (marketing site + worker)</p>
<hr>
<h2 id="verified-✓" tabindex="-1">Verified ✓ <a class="header-anchor" href="#verified-✓" aria-label="Permalink to &quot;Verified ✓&quot;"></a></h2>
<ul>
<li><strong>Tool names</strong> (Image Generator, Video Generator, 3D Model Generator) and <strong>link</strong> <code>/tools/hunyuan3d</code> match the codebase.</li>
<li><strong>Author</strong> (Oleg Sidorkin, CTO) is correct.</li>
<li><strong>Video workflow:</strong> Start + end frame interpolation, 9:16, 3–15 s duration, and storyboard mode are all supported.</li>
<li><strong>3D pipeline:</strong> Hunyuan3D and &quot;3D Model Generator&quot; are correct; auto-rigging (Tripo) and &quot;upload GLB → rigged model + animation library&quot; match the implementation.</li>
<li><strong>Cinevva Engine</strong> and &quot;Reflects early 2026&quot; are accurate.</li>
</ul>
<hr>
<h2 id="correction-needed" tabindex="-1">Correction needed <a class="header-anchor" href="#correction-needed" aria-label="Permalink to &quot;Correction needed&quot;"></a></h2>
<p><strong>&quot;Seedance 1.5 Pro&quot;</strong> — In this repo, video is implemented with <strong>Kling 3.0 Pro</strong> (fal.ai), not Seedance 1.5 Pro:</p>
<ul>
<li><code>worker/src/genai/fal-video.ts</code> uses <code>fal-ai/kling-video/v3/pro/...</code></li>
<li><code>worker/src/index.ts</code> tracks cost as <code>model: 'kling-3.0-pro'</code></li>
<li><code>tools/video.md</code> says: &quot;Generate short AI videos from text or images using <strong>Kling 3.0 Pro</strong>&quot;</li>
</ul>
<p>The API/product name is &quot;seedance&quot; (e.g. <code>/genai/seedance/generate</code>), but the underlying model is Kling 3.0 Pro. Seedance 1.5 Pro is a different model (ByteDance). Public sources often cap Seedance 1.5 Pro at ~10 s; 15 s is supported for Kling here.</p>
<p><strong>Recommendation:</strong> In the article, name the video model <strong>&quot;Kling 3.0 Pro&quot;</strong> (or e.g. &quot;Video Generator (Kling 3.0 Pro)&quot;) so it matches the marketing site and docs. If the app backend actually uses Seedance 1.5 Pro, align the app copy and <code>tools/video.md</code> so they describe the same model.</p>
<hr>
<h2 id="not-verifiable-in-repo" tabindex="-1">Not verifiable in repo <a class="header-anchor" href="#not-verifiable-in-repo" aria-label="Permalink to &quot;Not verifiable in repo&quot;"></a></h2>
<p>Cubcoats, Kali the Kitty, Mimi Chao, and related business claims are external; not checked.</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Spike source viewer]]></title>
            <link>https://app.cinevva.com/blog/spike-source</link>
            <guid>https://app.cinevva.com/blog/spike-source</guid>
            <pubDate>Sun, 12 Jul 2026 20:16:35 GMT</pubDate>
            <description><![CDATA[View the source code of any spike from the open world browser series.]]></description>
            <content:encoded><![CDATA[<h1 id="opening-in-the-builder" tabindex="-1">Opening in the builder… <a class="header-anchor" href="#opening-in-the-builder" aria-label="Permalink to &quot;Opening in the builder…&quot;"></a></h1>
<p v-if="spikeId">
  Taking you to <strong>spike {{ spikeId }}</strong> in the Game Creator.
  If you're not redirected, <a :href="`/create#spike-${spikeId}`">open it here</a>.
</p>
<p v-else>
  <a href="/create">Open the Game Creator</a>.
</p>
<p style="margin-top:1rem">
  <a href="/blog/2026-02-25-open-world-browser-series-guide">← Back to series guide</a>
</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Record profits, record layoffs: how game devs are actually getting rehired in 2026]]></title>
            <link>https://app.cinevva.com/blog/2026-07-02-game-industry-layoffs-getting-rehired</link>
            <guid>https://app.cinevva.com/blog/2026-07-02-game-industry-layoffs-getting-rehired</guid>
            <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The games industry made $195.6B last year and still cut a third of its people. Here's the honest data on the layoffs, why the job market got so brutal, and the one thing that's actually pulling people back in.]]></description>
            <content:encoded><![CDATA[<h1 id="record-profits-record-layoffs-how-game-devs-are-actually-getting-rehired-in-2026" tabindex="-1">Record profits, record layoffs: how game devs are actually getting rehired in 2026 <a class="header-anchor" href="#record-profits-record-layoffs-how-game-devs-are-actually-getting-rehired-in-2026" aria-label="Permalink to &quot;Record profits, record layoffs: how game devs are actually getting rehired in 2026&quot;"></a></h1>
<p><em>By <a href="/about.html">Mariana Muntean</a>, CEO of Cinevva</em></p>
<p>The games industry made about <strong>$195.6 billion</strong> in 2025. Its best year ever. In the same stretch, roughly <strong>one in three developers lost their job</strong>.</p>
<p>Read those two sentences again. They shouldn't be able to coexist, and yet here we are. If you got cut in the last two years, you already know the math doesn't feel like a downturn. It feels like being told the boat is fine while you're the one going overboard.</p>
<p>I've been an indie dev. I've watched friends post the &quot;open to work&quot; banner three times in eighteen months. So this isn't a hot take from the sidelines. Let's look at what actually happened, and then the part that matters most if you're job hunting right now: what's genuinely getting people rehired.</p>
<h2 id="the-numbers-are-worse-than-the-vibe" tabindex="-1">The numbers are worse than the vibe <a class="header-anchor" href="#the-numbers-are-worse-than-the-vibe" aria-label="Permalink to &quot;The numbers are worse than the vibe&quot;"></a></h2>
<p>The layoff wave started in 2022 and never really stopped. It's not one bad quarter. It's a four-year structural reset.</p>
<table tabindex="0">
<thead>
<tr>
<th>Year</th>
<th>Games jobs cut</th>
</tr>
</thead>
<tbody>
<tr>
<td>2022</td>
<td>~8,500</td>
</tr>
<tr>
<td>2023</td>
<td>~10,500</td>
</tr>
<tr>
<td>2024</td>
<td>~14,600 (the peak)</td>
</tr>
<tr>
<td>2025</td>
<td>~9,200</td>
</tr>
<tr>
<td>2026</td>
<td>tracking above 2025, projected ~11,580</td>
</tr>
</tbody>
</table>
<p>That's roughly <strong>44,000 to 55,000 jobs gone</strong> over the period, depending on whose tracker you trust (<a href="https://en.wikipedia.org/wiki/2022%E2%80%932026_video_game_industry_layoffs" target="_blank" rel="noreferrer">Wikipedia</a>, <a href="https://gamesbeat.com/global-gamings-workforce-grew-0-6-in-4-years-but-north-americas-shrank-11-5-amir-satvat/" target="_blank" rel="noreferrer">GamesBeat</a>). The 2026 State of the Game Industry report from GDC, which surveyed more than 2,300 professionals, found that <strong>28% of developers globally lost their job in the past two years, and 33% in the US</strong> (<a href="https://www.gamesindustry.biz/gdc-survey-reveals-layoffs-up-6-36-of-industry-using-ai-and-overwhelming-support-for-unionisation-in-the-us" target="_blank" rel="noreferrer">GamesIndustry.biz</a>). Half said their current or most recent employer ran layoffs in the last twelve months.</p>
<p>North America took the worst of it. Amir Satvat's workforce tracking, which is the closest thing this industry has to a running census, shows the regional workforce shrank while the global headcount barely moved. About <strong>19% of North American games workers were affected</strong> (<a href="https://gamesbeat.com/global-gamings-workforce-grew-0-6-in-4-years-but-north-americas-shrank-11-5-amir-satvat/" target="_blank" rel="noreferrer">GamesBeat</a>).</p>
<p>Here's the stat that explains why the job search feels impossible even after hiring &quot;recovered.&quot; Satvat estimates around <strong>288,000 people were looking for games work</strong> over this window. Recent grads, laid-off veterans, and career changers, all applying into the same narrow set of openings. When you send a resume into a job post now, you're not competing with a dozen people. You're competing with a stadium.</p>
<h2 id="why-a-record-year-still-meant-pink-slips" tabindex="-1">Why a record year still meant pink slips <a class="header-anchor" href="#why-a-record-year-still-meant-pink-slips" aria-label="Permalink to &quot;Why a record year still meant pink slips&quot;"></a></h2>
<p>The revenue didn't disappear. It concentrated. A shrinking number of live-service giants pull an outsized share, which means fewer new bets, fewer new teams, and fewer of the mid-size projects that used to absorb talent. Private investment in games dropped by more than half in 2025 (<a href="https://respawn.outlookindia.com/gaming/gaming-news/record-profits-record-layoffs-inside-gamings-2026-paradox" target="_blank" rel="noreferrer">Outlook Respawn</a>). Studios responded the way studios do: cancel projects, trim budgets, move work to contractors.</p>
<p>So the profit is real, and the pain is real, and they're pointing at the same cause. That's cold comfort when you're the one refreshing your inbox, but it tells you something useful. This isn't a market waiting to snap back to 2021. The people getting rehired aren't waiting for that either. They're adapting to how hiring actually works now.</p>
<h2 id="the-job-market-has-an-authenticity-problem" tabindex="-1">The job market has an authenticity problem <a class="header-anchor" href="#the-job-market-has-an-authenticity-problem" aria-label="Permalink to &quot;The job market has an authenticity problem&quot;"></a></h2>
<p>Talk to anyone who screens candidates today and you'll hear the same complaint. They're drowning. Job posts pull hundreds of applications in hours, and most of them read like the same person wrote them, because in a sense the same tool did.</p>
<p>Recruiters told TechRound the hard part isn't spotting AI use, it's that the flood of &quot;well-optimized, low-context content&quot; makes it nearly impossible to find the person who actually gets the role (<a href="https://techround.co.uk/news/linkedin-ai-slop-content-recruitment-process/" target="_blank" rel="noreferrer">TechRound</a>). LinkedIn itself now says it detects generic AI-written content around 94% of the time and quietly limits how far it spreads (<a href="https://techround.co.uk/news/linkedin-ai-slop-content-recruitment-process/" target="_blank" rel="noreferrer">TechRound</a>).</p>
<p>Flip that around and it becomes the best news in this whole article. When everything sounds the same, being specific is a superpower. In a feed of identical polish, the human who shows real work and real thinking doesn't just stand out. They're the only signal in the noise.</p>
<h2 id="proof-beats-polish-every-single-time" tabindex="-1">Proof beats polish, every single time <a class="header-anchor" href="#proof-beats-polish-every-single-time" aria-label="Permalink to &quot;Proof beats polish, every single time&quot;"></a></h2>
<p>For game roles specifically, the thing that moves the needle isn't a prettier PDF. It's evidence someone can experience. A 12-year recruiter put it bluntly: a playable build or a demo reel does more for you than almost anything else on your application.</p>
<p>That's the shift. A resume tells a recruiter you understand design. A playable slice lets them feel it in 30 to 90 seconds. You can't fake a working mechanic the way you can fake a bullet point. You can't fake a walkable level, a live shader, a rig that actually moves, or an interactive audio cue mapped to a real scene. Interactivity is proof that survives scrutiny, because behind it is a person who can defend every decision.</p>
<p>And that last part is what recruiters are really testing. The tell they use to catch AI-padded portfolios is simple: ask about process, not output. Someone who generated a case study can describe what it says, but not the decisions behind it. So the strongest portfolios in 2026 all carry the same four things:</p>
<table tabindex="0">
<thead>
<tr>
<th>Layer</th>
<th>What it answers</th>
</tr>
</thead>
<tbody>
<tr>
<td>Context</td>
<td>The brief, the platform, the real constraints (budget, deadline, tech limits)</td>
</tr>
<tr>
<td>Contribution</td>
<td>What you personally owned versus what the team did</td>
</tr>
<tr>
<td>Decisions</td>
<td>The trade-offs you made, and what you kept, changed, and rejected</td>
</tr>
<tr>
<td>Outcome</td>
<td>Measurable, verifiable results a stranger can check</td>
</tr>
</tbody>
</table>
<p>That &quot;kept, changed, rejected&quot; line is the one most people skip, and it's the one that proves taste. Taste is the hardest thing to fake, and it's exactly what a studio is buying: someone who can pick the right idea out of ten plausible ones. Skip the &quot;I learned so much&quot; wrap-up. Show the call you made and why it served the player.</p>
<h2 id="a-word-about-ai-because-this-industry-has-earned-the-right-to-be-angry" tabindex="-1">A word about AI, because this industry has earned the right to be angry <a class="header-anchor" href="#a-word-about-ai-because-this-industry-has-earned-the-right-to-be-angry" aria-label="Permalink to &quot;A word about AI, because this industry has earned the right to be angry&quot;"></a></h2>
<p>I run a platform that uses AI tools, and I'm not going to pretend the tension isn't there. The same GDC survey found that <strong>52% of developers now think generative AI is having a negative impact on the industry, up from 18% two years ago</strong>, and the strongest opposition comes from artists, writers, and programmers (<a href="https://www.gamesindustry.biz/gdc-survey-reveals-layoffs-up-6-36-of-industry-using-ai-and-overwhelming-support-for-unionisation-in-the-us" target="_blank" rel="noreferrer">GamesIndustry.biz</a>). When you've just been laid off and told a model might do a slice of your old job, &quot;just use AI for your portfolio&quot; is a slap, not advice.</p>
<p>So I'll say it plainly. Nobody gets hired by handing a recruiter something a machine generated and they can't explain. The work has to be yours. Your decisions, your craft, your ownership. If you did use a tool somewhere in a project, say so, and show your judgment on top of it: what you kept, what you threw out, why your call was better for the player. Hiring managers trust the person who treats a tool like an intern that needs direction, not the person hiding behind it. Honesty about your process reads as professionalism, and professionalism is often just &quot;I thought about the consequences before I published.&quot;</p>
<h2 id="where-to-put-the-proof" tabindex="-1">Where to put the proof <a class="header-anchor" href="#where-to-put-the-proof" aria-label="Permalink to &quot;Where to put the proof&quot;"></a></h2>
<p>One practical thing, since so many of these conversations start on LinkedIn. The feed punishes outside links hard, but your profile's Featured section doesn't. That's the one spot that turns a link into a clean preview card with no penalty, and it sits right at the top of your profile where recruiters look first. Pin three to five things, best work first, and remember most people are viewing on a phone. Lead with the item someone can open and experience in seconds.</p>
<p>Keep it fast and keep it honest. A demo that loads in under two seconds and shows one clean idea beats an ambitious project that stutters or 404s. Recruiters reviewing thirty portfolios in an afternoon do not wait, and a broken link reads as a broken habit.</p>
<h2 id="why-we-care-about-this" tabindex="-1">Why we care about this <a class="header-anchor" href="#why-we-care-about-this" aria-label="Permalink to &quot;Why we care about this&quot;"></a></h2>
<p>We build browser-based, instantly playable games at Cinevva, which means a lot of what we make happens to be exactly what a modern portfolio needs: real work that a recruiter can open and play from a single link, no download, no install. We're paying close attention to this problem, because the people it's hitting are our people. If you got cut, your skills didn't evaporate. The market got louder, and the way you prove yourself changed.</p>
<p>The old question was &quot;where did you work?&quot; The new one is &quot;show me something I can play, and tell me why you built it that way.&quot; That second question is harder to game and better for the people who can actually do the work. If that's you, this market is brutal, but it isn't closed. It's waiting for proof it can trust.</p>
<hr>
<h2 id="sources" tabindex="-1">Sources <a class="header-anchor" href="#sources" aria-label="Permalink to &quot;Sources&quot;"></a></h2>
<ul>
<li><a href="https://www.gamesindustry.biz/gdc-survey-reveals-layoffs-up-6-36-of-industry-using-ai-and-overwhelming-support-for-unionisation-in-the-us" target="_blank" rel="noreferrer">GDC 2026 State of the Game Industry: layoffs, AI, and unionization</a></li>
<li><a href="https://gamesbeat.com/global-gamings-workforce-grew-0-6-in-4-years-but-north-americas-shrank-11-5-amir-satvat/" target="_blank" rel="noreferrer">GamesBeat: Global games workforce grew 0.6% in 4 years, North America shrank</a></li>
<li><a href="https://en.wikipedia.org/wiki/2022%E2%80%932026_video_game_industry_layoffs" target="_blank" rel="noreferrer">Wikipedia: 2022–2026 video game industry layoffs</a></li>
<li><a href="https://respawn.outlookindia.com/gaming/gaming-news/record-profits-record-layoffs-inside-gamings-2026-paradox" target="_blank" rel="noreferrer">Outlook Respawn: Record profits, record layoffs</a></li>
<li><a href="https://www.gamedeveloper.com/business/industry-layoffs-are-seemingly-slowing-but-the-damage-has-already-been-done" target="_blank" rel="noreferrer">Game Developer: Layoffs are slowing, but the damage is done</a></li>
<li><a href="https://techround.co.uk/news/linkedin-ai-slop-content-recruitment-process/" target="_blank" rel="noreferrer">TechRound: LinkedIn faces a surge in AI slop content</a></li>
</ul>
<hr>
<p><strong>Related:</strong></p>
<ul>
<li><a href="/blog/2026-01-18-skills-over-degrees.html">The job market is transforming: from credentials to skills</a></li>
<li><a href="/blog/2026-01-18-ai-controversy-and-post-ai-economy.html">AI controversy, trust, and the post-AI economy for games</a></li>
<li><a href="/blog/2026-02-10-the-intuitive-mind.html">The intuitive mind in an age of AI</a></li>
</ul>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/blog/skills-over-degrees.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Best Free Browser Games to Play Now (No Download)]]></title>
            <link>https://app.cinevva.com/blog/2026-06-30-best-free-browser-games-no-download</link>
            <guid>https://app.cinevva.com/blog/2026-06-30-best-free-browser-games-no-download</guid>
            <pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A hand-picked list of the best browser games you can play instantly with no download or install. Top-rated indie games across puzzle, roguelike, story, and rhythm.]]></description>
            <content:encoded><![CDATA[<h1 id="best-free-browser-games-to-play-now-no-download" tabindex="-1">Best Free Browser Games to Play Now (No Download) <a class="header-anchor" href="#best-free-browser-games-to-play-now-no-download" aria-label="Permalink to &quot;Best Free Browser Games to Play Now (No Download)&quot;"></a></h1>
<p><em>By <a href="/about.html">Mariana Muntean</a>, CEO of Cinevva</em></p>
<p>Okay so we absolutely did not intend to burn an entire week on itch.io. What started as quick research turned into... well, somewhere around hour twelve we stopped counting. The whole point was finding games you can play immediately — no downloads, no installers chugging away in the background. You click, you're in. Done.</p>
<p>These are the ones that made the cut.</p>
<h2 id="top-rated-interactive-experiences" tabindex="-1">Top-Rated Interactive Experiences <a class="header-anchor" href="#top-rated-interactive-experiences" aria-label="Permalink to &quot;Top-Rated Interactive Experiences&quot;"></a></h2>
<div class="showcase-card">
<img src="/img/showcases/anxiety.png" alt="Adventures With Anxiety screenshot" loading="lazy">
<p><strong>Adventures With Anxiety!</strong> — Interactive Story
Over 6,500 ratings with a 4.9 average. Wild that something forcing you to stare down anxiety has this many fans, right? Gets uncomfortable in the best way — the kind that sticks with you. <a href="https://ncase.itch.io/anxiety" target="_blank" rel="noreferrer">Play it here</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/wbwwb.png" alt="We Become What We Behold screenshot" loading="lazy">
<p><strong>We Become What We Behold</strong> — Social Commentary
7,600 ratings at 4.8/5. Ten minutes to finish. Days to stop thinking about it. <a href="https://ncase.itch.io/wbwwb" target="_blank" rel="noreferrer">Play it here</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/six-cats-under.png" alt="Six Cats Under screenshot" loading="lazy">
<p><strong>Six Cats Under</strong> — Puzzle Adventure
Cozy and clever. Visuals do serious heavy lifting here, which works in its favor. 4.8/5 from 6,400+ people. <a href="https://teambeanloop.itch.io/six-cats-under" target="_blank" rel="noreferrer">Play it here</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/friday-night-funkin.gif" alt="Friday Night Funkin screenshot" loading="lazy">
<p><strong>Friday Night Funkin'</strong> — Rhythm Game
This one's everywhere by now. 11,700+ ratings, 4.7/5. <a href="https://ninja-muffin24.itch.io/funkin" target="_blank" rel="noreferrer">Play it here</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/sort-the-court.png" alt="Sort the Court screenshot" loading="lazy">
<p><strong>Sort the Court!</strong> — Kingdom Sim
Your only job: say yes or no. Doesn't sound like much. Then two hours vanish while you're still running a tiny kingdom. <a href="https://graebor.itch.io/sort-the-court" target="_blank" rel="noreferrer">Play it here</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/dragonsweeper.png" alt="Dragonsweeper screenshot" loading="lazy">
<p><strong>Dragonsweeper</strong> — Puzzle Roguelike
Take Minesweeper, mash it together with RPG systems. 4.9/5 — this might actually be the best thing on the whole list. <a href="https://danielben.itch.io/dragonsweeper" target="_blank" rel="noreferrer">Play it here</a></p>
</div>
<h2 id="roguelike-strategy-browser-playable" tabindex="-1">Roguelike &amp; Strategy (Browser-Playable) <a class="header-anchor" href="#roguelike-strategy-browser-playable" aria-label="Permalink to &quot;Roguelike &amp; Strategy (Browser-Playable)&quot;"></a></h2>
<div class="showcase-card">
<img src="/img/showcases/backpack-hero.png" alt="Backpack Hero screenshot" loading="lazy">
<p><strong>Backpack Hero</strong> — Inventory Roguelike
Browser version costs nothing. Paid version adds more stuff, but the free web build stands on its own. <a href="https://thejaspel.itch.io/backpack-hero" target="_blank" rel="noreferrer">Play it here</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/vampire-survivors.png" alt="Vampire Survivors screenshot" loading="lazy">
<p><strong>Vampire Survivors</strong> — Bullet Heaven
Demo only — single stage, seven characters. Still enough to see why this thing consumed everyone's free time. <a href="https://poncle.itch.io/vampire-survivors" target="_blank" rel="noreferrer">Play it here</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/dome-romantik.gif" alt="Dome Romantik screenshot" loading="lazy">
<p><strong>Dome Romantik</strong> — Mining Defense
Started as a game jam thing. Became Dome Keeper. Became huge. Original prototype still holds up. <a href="https://bippinbits.itch.io/dome-romantik" target="_blank" rel="noreferrer">Play it here</a></p>
</div>
<h2 id="why-these-specifically" tabindex="-1">Why These Specifically? <a class="header-anchor" href="#why-these-specifically" aria-label="Permalink to &quot;Why These Specifically?&quot;"></a></h2>
<p>Every single link got tested. Not a quick skim — we actually sat down and played these. They load without drama. No browser weirdness. Thousands of ratings came from folks who never touched a download button; they clicked and jumped straight in. That zero-friction path from &quot;huh, interesting&quot; to actually playing? Most people underestimate how much that changes things.</p>
<h2 id="browse-more-web-games" tabindex="-1">Browse More Web Games <a class="header-anchor" href="#browse-more-web-games" aria-label="Permalink to &quot;Browse More Web Games&quot;"></a></h2>
<p>Still hungry for more?</p>
<ul>
<li><a href="https://itch.io/games/top-rated/html5" target="_blank" rel="noreferrer">Top rated HTML5 games on itch.io</a></li>
<li><a href="https://itch.io/games/top-rated/genre-platformer/html5" target="_blank" rel="noreferrer">Best HTML5 platformers</a></li>
<li><a href="https://itch.io/games/top-rated/html5/year-2024" target="_blank" rel="noreferrer">Games from 2024-2025</a></li>
</ul>
<h2 id="notable-indie-games-download-required" tabindex="-1">Notable Indie Games (Download Required) <a class="header-anchor" href="#notable-indie-games-download-required" aria-label="Permalink to &quot;Notable Indie Games (Download Required)&quot;"></a></h2>
<p>Some great stuff won't run in your browser. These earn the extra step:</p>
<div class="showcase-card">
<img src="/img/showcases/slice-dice.png" alt="Slice & Dice screenshot" loading="lazy">
<p><strong>Slice &amp; Dice</strong> — Dice Roguelike
Runs on Windows, Mac, Android. <a href="https://tann.itch.io/slice-dice" target="_blank" rel="noreferrer">Grab it on itch.io</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/stacklands.png" alt="Stacklands screenshot" loading="lazy">
<p><strong>Stacklands</strong> — Card Village Builder
Windows and Mac. <a href="https://sokpop.itch.io/stacklands" target="_blank" rel="noreferrer">Get it on itch.io</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/luck-be-a-landlord.png" alt="Luck be a Landlord screenshot" loading="lazy">
<p><strong>Luck be a Landlord</strong> — Slot Machine Roguelike
Sounds like a gimmick. It's not. Actual strategy underneath that slot machine surface. <a href="https://trampolinetales.itch.io/luck-be-a-landlord" target="_blank" rel="noreferrer">itch.io</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/patricks-parabox.png" alt="Patrick's Parabox screenshot" loading="lazy">
<p><strong>Patrick's Parabox</strong> — Recursive Puzzles
Expect your brain to ache. Expect to like it anyway. <a href="https://patricktraynor.itch.io/patricks-parabox" target="_blank" rel="noreferrer">itch.io</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/dome-keeper.jpg" alt="Dome Keeper screenshot" loading="lazy">
<p><strong>Dome Keeper</strong> — Mining Defense
Full release of that jam prototype we mentioned. <a href="https://store.steampowered.com/app/1637320/Dome_Keeper/" target="_blank" rel="noreferrer">Find it on Steam</a></p>
</div>
<div class="showcase-card">
<img src="/img/showcases/brotato.jpg" alt="Brotato screenshot" loading="lazy">
<p><strong>Brotato</strong> — Arena Survivor
<a href="https://store.steampowered.com/app/1942280/Brotato/" target="_blank" rel="noreferrer">Available on Steam</a></p>
</div>
<hr>
<p>Here's something to sit with if you're making games: <strong>browser builds put nothing between players and your work</strong>. No download sitting in a queue they'll forget about. No install screen to click through. One click and they're already forming opinions about what you built. That kind of immediacy doesn't get talked about enough — but it shifts everything.</p>
<p>Want that for your own game? You can build one right in the browser with the <a href="/create.html">Cinevva game creator</a>, and the ones we feature land in our <a href="/arcade.html">community Arcade</a>.</p>
<ul>
<li><a href="/creators.html">For game creators</a></li>
<li><a href="/tutorials/ship-web-game-fast.html">Build a web game that loads fast</a></li>
</ul>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/showcases/anxiety.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Web game engines in 2026: PlayCanvas vs Three.js vs Babylon.js vs Unity WebGL]]></title>
            <link>https://app.cinevva.com/blog/2026-06-09-web-game-engines-2026-comparison</link>
            <guid>https://app.cinevva.com/blog/2026-06-09-web-game-engines-2026-comparison</guid>
            <pubDate>Tue, 09 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A current, honest comparison of the main ways to build a 3D game in the browser in 2026: PlayCanvas (v2.20), Three.js, Babylon.js, and Unity WebGL. What each is, who it's for, WebGL2 vs WebGPU support, licensing, and how to choose.]]></description>
            <content:encoded><![CDATA[<h1 id="web-game-engines-in-2026-playcanvas-vs-three-js-vs-babylon-js-vs-unity-webgl" tabindex="-1">Web game engines in 2026: PlayCanvas vs Three.js vs Babylon.js vs Unity WebGL <a class="header-anchor" href="#web-game-engines-in-2026-playcanvas-vs-three-js-vs-babylon-js-vs-unity-webgl" aria-label="Permalink to &quot;Web game engines in 2026: PlayCanvas vs Three.js vs Babylon.js vs Unity WebGL&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<img src="https://cdn.cinevva.com/blog/web-engines-2026-hero.jpg" alt="A browser window showing a 3D scene transitioning from a white wireframe mesh on the left to a rendered colorful low-poly landscape on the right" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>If you want to ship a 3D game that runs in a browser in 2026, you have four mainstream choices and a few newer ones. They get lumped together as &quot;web game engines,&quot; but they're genuinely different tools that solve different problems. Picking the wrong one costs you months. This is a current, fact-checked comparison, written by people who build a web 3D engine for a living and evaluated all of these before building our own.</p>
<p>We'll go through each option, what it actually is, who it's for, and where it falls down, then give you a short decision guide at the end. Where we mention version numbers, they're accurate as of June 2026.</p>
<blockquote>
<p>Looking for the full field? This post focuses on the four mainstream 3D choices. For all 11 web engines ranked and compared (including 2D options like Phaser, Defold, and Construct) see our <a href="/guides/web-game-engines-comparison.html">best web game engines for 2026</a> guide.</p>
</blockquote>
<h2 id="the-quick-comparison" tabindex="-1">The quick comparison <a class="header-anchor" href="#the-quick-comparison" aria-label="Permalink to &quot;The quick comparison&quot;"></a></h2>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th>PlayCanvas</th>
<th>Three.js</th>
<th>Babylon.js</th>
<th>Unity WebGL</th>
<th>Cinevva</th>
</tr>
</thead>
<tbody>
<tr>
<td>What it is</td>
<td>Full engine + hosted editor</td>
<td>Rendering library</td>
<td>Full engine + editor</td>
<td>Desktop engine, web export</td>
<td>AI-native world platform</td>
</tr>
<tr>
<td>Architecture</td>
<td>Entity-component (ECS)</td>
<td>Scene graph</td>
<td>Scene graph + components</td>
<td>GameObject/component</td>
<td>Scene graph + AI builder</td>
</tr>
<tr>
<td>Scripting</td>
<td>TypeScript / JavaScript</td>
<td>JavaScript (you build the rest)</td>
<td>TypeScript / JavaScript</td>
<td>C# (compiled to Wasm)</td>
<td>Natural language + code</td>
</tr>
<tr>
<td>Renderer</td>
<td>WebGL2, WebGPU maturing</td>
<td>WebGL2, WebGPU growing</td>
<td>WebGL2, WebGPU advancing</td>
<td>WebGL2 (export)</td>
<td>WebGPU only</td>
</tr>
<tr>
<td>Editor</td>
<td>Hosted, commercial</td>
<td>None</td>
<td>Web-based, free</td>
<td>Desktop, commercial</td>
<td>In-world, immersive</td>
</tr>
<tr>
<td>Physics</td>
<td>Ammo / integrations</td>
<td>Bring your own</td>
<td>Havok built in</td>
<td>Built in (PhysX)</td>
<td>Custom character solver</td>
</tr>
<tr>
<td>License</td>
<td>Engine MIT, editor proprietary</td>
<td>MIT</td>
<td>MIT</td>
<td>Proprietary</td>
<td>Proprietary</td>
</tr>
<tr>
<td>Best for</td>
<td>Studio-style browser games</td>
<td>Custom 3D, full control</td>
<td>Batteries-included games</td>
<td>Porting existing Unity games</td>
<td>In-world AI game creation</td>
</tr>
</tbody>
</table>
<p>The rest of this post explains the table.</p>
<h2 id="playcanvas" tabindex="-1">PlayCanvas <a class="header-anchor" href="#playcanvas" aria-label="Permalink to &quot;PlayCanvas&quot;"></a></h2>
<p>PlayCanvas is the closest thing the web has to a Unity-style workflow. The engine is open source under the MIT license and sits at v2.19.6 as of June 5, 2026. It uses an entity-component system, you write game logic in TypeScript or JavaScript, and assets move through a server-side pipeline that produces GLB. Real commercial games run on it, and large companies use it in production, Snap being a public example.</p>
<p>Two things people get wrong about PlayCanvas are worth correcting. First, while the engine is MIT-licensed, the hosted visual editor that most teams actually use is a commercial product, not open source. You can use the engine freely without it, but the editor-plus-cloud workflow is the paid part. Second, PlayCanvas is WebGL2-first. It has a WebGPU path, but that path is still maturing rather than being the default renderer, so don't pick PlayCanvas today expecting production WebGPU compute shaders.</p>
<p>Where PlayCanvas genuinely leads is 3D Gaussian splatting. Its SuperSplat editor and viewer, which the team open-sourced, are among the best tooling anywhere for capturing and shipping splat scenes on the web, with WebGPU-based streaming for large captures. If photoreal scanned environments are your thing, that's a real reason to start here.</p>
<p><strong>Choose PlayCanvas if</strong> you want a Unity-like editor and a managed asset pipeline for the browser, and you're shipping the kind of game a studio would build. <strong>Look elsewhere if</strong> you need WebGPU compute today, or you want to avoid a hosted commercial editor.</p>
<h2 id="three-js" tabindex="-1">Three.js <a class="header-anchor" href="#three-js" aria-label="Permalink to &quot;Three.js&quot;"></a></h2>
<p>Three.js is not a game engine. It's a rendering library, and it's the most widely used one in the space by a wide margin. It gives you a scene graph, cameras, lights, materials, geometry, loaders, and a renderer, and then it stops. There's no editor, no physics, no entity system, and no opinion about how to structure a game. You add those yourself or pull them from the ecosystem.</p>
<p>That trade is the whole story. You get maximum control and the largest community in web 3D, at the cost of building or assembling everything above the renderer. Its WebGPU renderer has been growing steadily and is usable today, though, as with the others, the WebGL2 path is still the mature default. Three.js is MIT-licensed.</p>
<p><strong>Choose Three.js if</strong> you want full control, a minimal base, and you have the engineering to build your game systems on top. <strong>Look elsewhere if</strong> you want an editor and game systems handed to you.</p>
<h2 id="babylon-js" tabindex="-1">Babylon.js <a class="header-anchor" href="#babylon-js" aria-label="Permalink to &quot;Babylon.js&quot;"></a></h2>
<p>Babylon.js is a full engine, MIT-licensed and backed by a team at Microsoft. Unlike Three.js, it ships with the parts you'd otherwise assemble: a component model, the Havok physics engine integrated, a free web-based editor, and an asset pipeline. Its WebGPU work has moved quickly and is among the more advanced of the general-purpose engines, though it still maintains WebGL2 as the broad-compatibility path.</p>
<p>If your mental model is &quot;I want a complete engine, included physics, and I'm happy working inside its conventions,&quot; Babylon is a strong default and arguably the most feature-complete free option in the space.</p>
<p><strong>Choose Babylon.js if</strong> you want batteries included, built-in physics, and a free editor. <strong>Look elsewhere if</strong> you want a tiny dependency footprint or you specifically want a Unity-style hosted workflow.</p>
<h2 id="unity-webgl" tabindex="-1">Unity WebGL <a class="header-anchor" href="#unity-webgl" aria-label="Permalink to &quot;Unity WebGL&quot;"></a></h2>
<p>Unity WebGL isn't a web engine, it's an export target. You build in the Unity desktop editor, in C#, and compile to a WebGL bundle that runs in the browser via WebAssembly. That makes it the obvious answer for one specific case: you already have a Unity game and you want a browser version of it.</p>
<p>For a web-first project it carries real costs. The runtime and download weight are significant, startup is slower than a native web engine, and mobile browser performance is a known pain point. Unity is proprietary and its WebGL output targets WebGL2.</p>
<p><strong>Choose Unity WebGL if</strong> you have an existing Unity project to bring to the browser, or your team lives in Unity already. <strong>Look elsewhere if</strong> instant load on mid-range phones is a hard requirement, or you're starting fresh and web-first.</p>
<h2 id="where-ai-native-and-in-world-creation-fits" tabindex="-1">Where AI-native and in-world creation fits <a class="header-anchor" href="#where-ai-native-and-in-world-creation-fits" aria-label="Permalink to &quot;Where AI-native and in-world creation fits&quot;"></a></h2>
<p>Everything above shares one assumption: a developer builds the game at a desk, in an editor, and ships a runtime. That assumption is correct for most projects, and if it describes yours, pick from the four above.</p>
<p>It's worth knowing the assumption is no longer the only option, because a different category is emerging. We build Cinevva, which is a WebGPU-only world platform where games are created from inside the world. Instead of opening an editor, you're an avatar standing in the space, and you describe what you want, and an AI builder turns it into terrain, objects, and behavior while you stand there. Creation and play are the same session. Under the hood that meant going WebGPU-only with compute-shader terrain, writing a custom character solver instead of a general physics engine, and building an animation and retargeting system, which we wrote about in <a href="/blog/2026-06-08-why-we-built-our-own-webgpu-engine.html">why we built our own WebGPU engine</a>.</p>
<p>This isn't a replacement for PlayCanvas or Babylon. If you're a developer building a specific game, those are the right tools. Cinevva is for the different goal of letting people who aren't engine developers make and share playable spaces by describing them. We mention it here because &quot;which web game engine should I use&quot; increasingly has a fifth answer that isn't an engine at all.</p>
<h2 id="the-full-feature-matrix" tabindex="-1">The full feature matrix <a class="header-anchor" href="#the-full-feature-matrix" aria-label="Permalink to &quot;The full feature matrix&quot;"></a></h2>
<p>The quick table up top is the headline. This is the detailed version, grouped by subsystem. A few honest notes on how to read it. &quot;BYO&quot; means bring your own, which is to say the engine doesn't ship it but the ecosystem or your own code can add it. This matters most for Three.js, which is a rendering library by design, so &quot;BYO&quot; there is a feature of the philosophy, not a gap. &quot;Export-only&quot; for Unity means the capability exists in the desktop editor and rides along into the WebGL build rather than being web-native. Competitor cells reflect out-of-the-box capability and well-documented behavior as of mid-2026. Cinevva cells reflect what runs in our shipped build, with &quot;planned&quot; marking what's designed but not yet built.</p>
<h3 id="rendering" tabindex="-1">Rendering <a class="header-anchor" href="#rendering" aria-label="Permalink to &quot;Rendering&quot;"></a></h3>
<table tabindex="0">
<thead>
<tr>
<th>Feature</th>
<th>Cinevva</th>
<th>PlayCanvas</th>
<th>Three.js</th>
<th>Babylon.js</th>
<th>Unity WebGL</th>
</tr>
</thead>
<tbody>
<tr>
<td>Primary renderer</td>
<td>WebGPU only</td>
<td>WebGL2 (WebGPU beta)</td>
<td>WebGL2 (WebGPU growing)</td>
<td>WebGL2 (WebGPU advanced)</td>
<td>WebGL2 (export)</td>
</tr>
<tr>
<td>Compute shaders in production</td>
<td>Yes (core dependency)</td>
<td>Beta</td>
<td>Via WebGPU</td>
<td>Yes (WebGPU)</td>
<td>Export-only, limited</td>
</tr>
<tr>
<td>Shader authoring</td>
<td>TSL nodes + compute</td>
<td>Shader chunks / GLSL</td>
<td>GLSL + node (TSL)</td>
<td>Node material / GLSL / WGSL</td>
<td>ShaderLab / HLSL</td>
</tr>
<tr>
<td>Clustered / forward+ lighting</td>
<td>Yes (froxel)</td>
<td>Yes</td>
<td>BYO</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Volumetric clouds and weather</td>
<td>Yes</td>
<td>BYO</td>
<td>BYO</td>
<td>Partial</td>
<td>BYO</td>
</tr>
<tr>
<td>3D Gaussian splatting tooling</td>
<td>Planned</td>
<td>Yes (SuperSplat, leading)</td>
<td>Community</td>
<td>Yes</td>
<td>Plugins</td>
</tr>
</tbody>
</table>
<h3 id="world-and-terrain" tabindex="-1">World and terrain <a class="header-anchor" href="#world-and-terrain" aria-label="Permalink to &quot;World and terrain&quot;"></a></h3>
<table tabindex="0">
<thead>
<tr>
<th>Feature</th>
<th>Cinevva</th>
<th>PlayCanvas</th>
<th>Three.js</th>
<th>Babylon.js</th>
<th>Unity WebGL</th>
</tr>
</thead>
<tbody>
<tr>
<td>Built-in large-world streaming</td>
<td>Yes (64m chunks)</td>
<td>BYO</td>
<td>BYO</td>
<td>BYO</td>
<td>Export-only</td>
</tr>
<tr>
<td>Terrain system</td>
<td>Hybrid heightmap + marching-cubes/SDF</td>
<td>BYO</td>
<td>BYO</td>
<td>Extension</td>
<td>Built-in (desktop)</td>
</tr>
<tr>
<td>Runtime terrain sculpting</td>
<td>Yes (GPU)</td>
<td>BYO</td>
<td>BYO</td>
<td>BYO</td>
<td>No (edit-time)</td>
</tr>
<tr>
<td>Caves and overhangs (true 3D topology)</td>
<td>Yes (marching cubes)</td>
<td>BYO</td>
<td>BYO</td>
<td>BYO</td>
<td>BYO</td>
</tr>
<tr>
<td>GPU-instanced foliage and grass</td>
<td>Yes</td>
<td>Yes</td>
<td>BYO</td>
<td>Yes</td>
<td>Yes</td>
</tr>
</tbody>
</table>
<h3 id="physics-and-character" tabindex="-1">Physics and character <a class="header-anchor" href="#physics-and-character" aria-label="Permalink to &quot;Physics and character&quot;"></a></h3>
<table tabindex="0">
<thead>
<tr>
<th>Feature</th>
<th>Cinevva</th>
<th>PlayCanvas</th>
<th>Three.js</th>
<th>Babylon.js</th>
<th>Unity WebGL</th>
</tr>
</thead>
<tbody>
<tr>
<td>Physics engine</td>
<td>Custom kinematic solver</td>
<td>Ammo integration</td>
<td>BYO (Rapier/Cannon/Ammo)</td>
<td>Havok built-in</td>
<td>PhysX built-in</td>
</tr>
<tr>
<td>Rigid-body dynamics</td>
<td>No (by design)</td>
<td>Yes</td>
<td>BYO</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Character controller</td>
<td>Yes (multi-mode FSM)</td>
<td>Templates / Ammo</td>
<td>BYO</td>
<td>Yes</td>
<td>Built-in</td>
</tr>
<tr>
<td>Terrain-integrated collision (SDF)</td>
<td>Yes</td>
<td>No</td>
<td>BYO</td>
<td>No</td>
<td>No</td>
</tr>
</tbody>
</table>
<h3 id="animation" tabindex="-1">Animation <a class="header-anchor" href="#animation" aria-label="Permalink to &quot;Animation&quot;"></a></h3>
<table tabindex="0">
<thead>
<tr>
<th>Feature</th>
<th>Cinevva</th>
<th>PlayCanvas</th>
<th>Three.js</th>
<th>Babylon.js</th>
<th>Unity WebGL</th>
</tr>
</thead>
<tbody>
<tr>
<td>Skeletal animation</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Blend / state machine</td>
<td>Yes (resolver FSM)</td>
<td>Yes (anim state graph)</td>
<td>Mixer (BYO blending)</td>
<td>Yes</td>
<td>Yes (Mecanim)</td>
</tr>
<tr>
<td>Skeleton retargeting</td>
<td>Yes (pipeline)</td>
<td>Limited</td>
<td>Community</td>
<td>Partial</td>
<td>Yes (humanoid)</td>
</tr>
<tr>
<td>Inverse kinematics</td>
<td>Planned</td>
<td>Limited</td>
<td>Community</td>
<td>Yes</td>
<td>Yes</td>
</tr>
</tbody>
</table>
<h3 id="multiplayer-and-backend" tabindex="-1">Multiplayer and backend <a class="header-anchor" href="#multiplayer-and-backend" aria-label="Permalink to &quot;Multiplayer and backend&quot;"></a></h3>
<table tabindex="0">
<thead>
<tr>
<th>Feature</th>
<th>Cinevva</th>
<th>PlayCanvas</th>
<th>Three.js</th>
<th>Babylon.js</th>
<th>Unity WebGL</th>
</tr>
</thead>
<tbody>
<tr>
<td>Built-in multiplayer</td>
<td>Yes (edge-authoritative)</td>
<td>BYO (Photon/Colyseus)</td>
<td>BYO</td>
<td>BYO</td>
<td>BYO (Netcode, not web-native)</td>
</tr>
<tr>
<td>Persistent shared world</td>
<td>Yes (Durable Objects per chunk)</td>
<td>BYO</td>
<td>BYO</td>
<td>BYO</td>
<td>BYO</td>
</tr>
<tr>
<td>Spatial voice chat</td>
<td>Yes (WebRTC + HRTF)</td>
<td>BYO</td>
<td>BYO</td>
<td>BYO</td>
<td>BYO</td>
</tr>
</tbody>
</table>
<h3 id="creation-and-authoring" tabindex="-1">Creation and authoring <a class="header-anchor" href="#creation-and-authoring" aria-label="Permalink to &quot;Creation and authoring&quot;"></a></h3>
<table tabindex="0">
<thead>
<tr>
<th>Feature</th>
<th>Cinevva</th>
<th>PlayCanvas</th>
<th>Three.js</th>
<th>Babylon.js</th>
<th>Unity WebGL</th>
</tr>
</thead>
<tbody>
<tr>
<td>Editor</td>
<td>In-world, immersive</td>
<td>Hosted desktop-style (commercial)</td>
<td>None (minimal)</td>
<td>Web-based (free)</td>
<td>Desktop (commercial)</td>
</tr>
<tr>
<td>In-world embodied creation</td>
<td>Yes</td>
<td>No</td>
<td>No</td>
<td>No</td>
<td>No</td>
</tr>
<tr>
<td>Natural-language / AI creation</td>
<td>Yes (AI builder)</td>
<td>No</td>
<td>No</td>
<td>No</td>
<td>No</td>
</tr>
<tr>
<td>Scripting</td>
<td>Natural language + JS</td>
<td>TypeScript / JS</td>
<td>JavaScript</td>
<td>TypeScript / JS</td>
<td>C#</td>
</tr>
</tbody>
</table>
<h3 id="assets-and-distribution" tabindex="-1">Assets and distribution <a class="header-anchor" href="#assets-and-distribution" aria-label="Permalink to &quot;Assets and distribution&quot;"></a></h3>
<table tabindex="0">
<thead>
<tr>
<th>Feature</th>
<th>Cinevva</th>
<th>PlayCanvas</th>
<th>Three.js</th>
<th>Babylon.js</th>
<th>Unity WebGL</th>
</tr>
</thead>
<tbody>
<tr>
<td>Server-side asset pipeline</td>
<td>Yes (GLB + LOD + KTX2 + Draco)</td>
<td>Yes (GLB)</td>
<td>Loaders only</td>
<td>Import tools</td>
<td>Yes</td>
</tr>
<tr>
<td>Built-in AI asset generation</td>
<td>Yes (3D, image, audio, music)</td>
<td>No</td>
<td>No</td>
<td>No</td>
<td>No</td>
</tr>
<tr>
<td>Federated asset search</td>
<td>Yes (eleven providers)</td>
<td>Asset store</td>
<td>No</td>
<td>No</td>
<td>Asset store</td>
</tr>
<tr>
<td>Runs in browser, no install</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes (heavy)</td>
</tr>
<tr>
<td>Engine license</td>
<td>Proprietary platform</td>
<td>Engine MIT, editor proprietary</td>
<td>MIT</td>
<td>MIT</td>
<td>Proprietary</td>
</tr>
</tbody>
</table>
<p>The pattern in the matrix is the real story. The general-purpose engines spread their strength horizontally, they each do most things competently and leave the world, the backend, and the creation flow to you. Cinevva concentrates vertically, it does fewer things but owns the whole path from the renderer to a shared world you create from inside. Neither shape is better in the abstract. They answer different questions.</p>
<h2 id="how-to-choose" tabindex="-1">How to choose <a class="header-anchor" href="#how-to-choose" aria-label="Permalink to &quot;How to choose&quot;"></a></h2>
<p>Match the tool to the situation rather than to a feature checklist.</p>
<p>If you already have a Unity game, export it with Unity WebGL and accept the weight. If you're a studio that wants an editor-driven workflow for browser games, use PlayCanvas. If you want a complete free engine with physics included, use Babylon.js. If you want total control and you have the team to build on a bare renderer, use Three.js. And if your goal isn't to build one game but to let people create and play inside a shared world by describing things, that's the category we work in, and you can <a href="/">try Cinevva</a>.</p>
<p>Whatever you pick, do the evaluation with real spikes before you commit. Each of these can carry a real game, and the cost of switching halfway is the months you didn't spend shipping.</p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/web-engines-2026-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Why we built our own WebGPU engine instead of forking PlayCanvas]]></title>
            <link>https://app.cinevva.com/blog/2026-06-08-why-we-built-our-own-webgpu-engine</link>
            <guid>https://app.cinevva.com/blog/2026-06-08-why-we-built-our-own-webgpu-engine</guid>
            <pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Three.js, Babylon.js, PlayCanvas, Unity WebGL: we evaluated all of them before writing our own renderer. Here's what each one is good at, the four decisions that pushed us off the shelf, and the cases where you should absolutely not do what we did.]]></description>
            <content:encoded><![CDATA[<h1 id="why-we-built-our-own-webgpu-engine-instead-of-forking-playcanvas" tabindex="-1">Why we built our own WebGPU engine instead of forking PlayCanvas <a class="header-anchor" href="#why-we-built-our-own-webgpu-engine-instead-of-forking-playcanvas" aria-label="Permalink to &quot;Why we built our own WebGPU engine instead of forking PlayCanvas&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<img src="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" alt="A stylized low-poly world rendered by Cinevva's WebGPU engine: rolling hills, a cave carved into a cliff, a winding river, and an avatar mid-stride on a path at golden hour" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Every technical person who looks at Cinevva World asks the same thing within five minutes. You're a small team. Mature web 3D engines exist. Why did you write your own renderer, your own character physics, your own animation system, instead of standing on PlayCanvas or Babylon and shipping faster?</p>
<p>It's a fair question, and &quot;not invented here&quot; is the wrong answer to it. We did the homework. We ran the existing engines, read their source, and shipped spikes on top of two of them before deciding. This post is the honest version of that decision: what the off-the-shelf options are genuinely good at, the four places our needs diverged hard enough to justify building, and the situations where you should pick one of them over copying us. We documented the build itself in a long-running <a href="/blog/2026-02-25-open-world-browser-series-guide.html">open-world-in-the-browser engineering series</a>, so where a claim below has a working log behind it, I'll link to it.</p>
<h2 id="the-options-we-actually-evaluated" tabindex="-1">The options we actually evaluated <a class="header-anchor" href="#the-options-we-actually-evaluated" aria-label="Permalink to &quot;The options we actually evaluated&quot;"></a></h2>
<p>Four things get called &quot;web game engines,&quot; and they aren't the same kind of thing.</p>
<p><strong>Three.js</strong> is a rendering library, not a game engine. It gives you a scene graph, materials, loaders, and a renderer, and then it gets out of your way. There's no editor, no physics, no entity system, no opinion about how your game is structured. That's the appeal and the cost. You build everything above the renderer yourself, but nothing fights you. It's MIT-licensed, it has the largest ecosystem in the space, and when you hit an obscure shader bug at 2am there's a forum answer waiting.</p>
<p><strong>Babylon.js</strong> is a full engine with a scene graph, a physics integration (Havok), an asset pipeline, and a web editor. It's MIT-licensed, backed by a team at Microsoft, and its WebGPU work has been moving quickly. If you want batteries included and you're happy living inside the engine's structure, it's a strong default.</p>
<p><strong>PlayCanvas</strong> is the closest thing to a &quot;Unity for the web.&quot; As of this writing the engine is at v2.19.6, released June 5, 2026, and the engine itself is open source under MIT. The thing most people actually use, though, is the hosted visual editor, and that editor is a commercial product, not open source. PlayCanvas uses an entity-component system, you script in TypeScript or JavaScript, assets flow through a server-side GLB pipeline, and real commercial games have shipped on it (Snap runs production work on PlayCanvas, for one). Its renderer runs WebGL2 with a WebGPU path that is still maturing rather than the default. That last detail matters more than it sounds, and I'll come back to it.</p>
<p><strong>Unity WebGL</strong> isn't a web engine at all. It's an export target. You build in the Unity desktop editor and compile to a WebGL bundle. It's the right tool when you already have a Unity game and want it in a browser, and the wrong tool when &quot;loads instantly in a tab on a mid-range phone&quot; is a hard requirement, because the runtime and download weight come along for the ride.</p>
<p>Any of these is a reasonable foundation for a normal web game. We didn't have a normal web game.</p>
<h2 id="decision-one-webgpu-is-our-floor-not-our-finish-line" tabindex="-1">Decision one: WebGPU is our floor, not our finish line <a class="header-anchor" href="#decision-one-webgpu-is-our-floor-not-our-finish-line" aria-label="Permalink to &quot;Decision one: WebGPU is our floor, not our finish line&quot;"></a></h2>
<p>The split that pushes us off every shelf is this. For us, WebGPU is a requirement, not a feature we'll grow into.</p>
<p>Our terrain isn't a static heightmap. It's a hybrid of a streamed heightmap field and marching-cubes chunks backed by a signed-distance field, so the world can have real caves and overhangs, and creators can sculpt it live. The sculpt brushes, the foliage scatter, and the terrain meshing all run as compute shaders. Take compute away and the world doesn't degrade, it doesn't run.</p>
<p>That's the opposite of where the general-purpose engines sit today. Their WebGPU support is designed as progressive enhancement on top of a WebGL2-first renderer, with a fallback path for browsers that lack it. PlayCanvas in particular is WebGL2-first with WebGPU still in beta. That's the correct call for them, because their job is to run the widest possible matrix of games on the widest possible matrix of devices. Our job is narrower and deeper, so we made the opposite call: we went WebGPU-only at spike 13 and never looked back. Browsers without WebGPU aren't downgraded, they're unsupported, and we track that as a reach number instead of pretending a WebGL2 fallback is one config flag away. It isn't. It would be a partial rewrite of our terrain and foliage stages.</p>
<p>Building on a WebGL2-first engine would have meant either fighting its fallback assumptions on every compute feature, or maintaining two rendering paths forever. Owning the renderer let us treat compute as the baseline.</p>
<h2 id="decision-two-a-character-solver-not-a-physics-engine" tabindex="-1">Decision two: a character solver, not a physics engine <a class="header-anchor" href="#decision-two-a-character-solver-not-a-physics-engine" aria-label="Permalink to &quot;Decision two: a character solver, not a physics engine&quot;"></a></h2>
<p>The textbook move is to drop in a physics engine. We tried it. An early spike validated Rapier running in a worker, and it felt fine.</p>
<p>We wrote our own anyway, and there's no Rapier, Cannon, or Ammo anywhere in the shipped build. The reason is scope. We don't need rigid bodies, joints, ragdolls, or a constraint solver. We need one capsule to move correctly against terrain, and we need walking, sliding, gliding, climbing, and swimming to agree on a single shared answer to &quot;am I grounded, on what surface, at what angle.&quot; A general physics engine makes that harder, not easier, because those modes end up fighting its internal springs and dampers.</p>
<p>So our character controller is a <a href="/blog/2026-05-15-open-world-browser-part-29-pluggable-character.html">pluggable multi-channel state machine</a>. Each mode is a small unit that says whether it wants control this frame and, if it wins, writes velocity and facing. They arbitrate by priority across three channels, resource then stance then locomotion, and they all read the same terrain query. Collision is a capsule probe against the heightmap or the signed-distance field depending on the chunk.</p>
<p>The detail I'm proudest of is unglamorous. Ground detection scans every surface in the vertical column beneath your feet and picks the highest one at or below the capsule, instead of trusting the SDF gradient. On the edge of an overhang the nearest surface sideways is the cliff face, so a gradient-based normal flickers between &quot;floor&quot; and &quot;wall&quot; and you get phantom sliding. The column query makes standing on a ledge boring, which is exactly what you want. An off-the-shelf heightfield collider would not have given us that for free, because it can't see our terrain in the first place. The terrain lives on the GPU in our format. No external engine can collide against it without us copying the whole field into its collider format on every edit.</p>
<h2 id="decision-three-animation-that-travels-across-rigs" tabindex="-1">Decision three: animation that travels across rigs <a class="header-anchor" href="#decision-three-animation-that-travels-across-rigs" aria-label="Permalink to &quot;Decision three: animation that travels across rigs&quot;"></a></h2>
<p>Avatars use the Synty POLYGON rig, and our motion clips come from a large open animation library. Because the rig and the clips share bone names, the common case needs no retargeting at runtime, the tracks just bind by name. We wrote up the full character pipeline in <a href="/blog/2026-05-10-open-world-browser-part-25-universal-characters.html">universal characters</a>.</p>
<p>The interesting work is the case where they don't match. We built a retargeting pass that maps one skeleton onto another, and getting it right meant solving three specific problems. You align bind poses first, because an A-pose against a T-pose silently adds about thirty degrees per joint. You scale root motion and stride with two separate ratios, hip-to-floor for the vertical and overall proportion for the horizontal. And you test retargeted clips against their source at a fixed sample rate, so regressions show up before a player does. That pipeline is what lets us add new animation sources, and eventually creator-supplied characters, without hand-fixing every clip.</p>
<p>On top of that sits an animation state machine that picks the right clip family each frame from the motion state: jump variant by speed, landing severity by impact, slope direction from the dot of velocity against the terrain gradient, foot-phase-correct stops so you don't moonwalk to a halt, and a hundred-millisecond debounce so bumping a wall doesn't strobe you between walk and fall. None of this is exotic, but it's the kind of thing you only get by owning the layer.</p>
<h2 id="decision-four-the-part-no-engine-ships" tabindex="-1">Decision four: the part no engine ships <a class="header-anchor" href="#decision-four-the-part-no-engine-ships" aria-label="Permalink to &quot;Decision four: the part no engine ships&quot;"></a></h2>
<p>The other three decisions are about how the world runs. This one is about what the product is, and it's why the comparison to PlayCanvas is ultimately a category error.</p>
<p>PlayCanvas, Babylon, Unity, and a hand-rolled Three.js app all assume the same shape: you build your game at a desk, in a 2D editor, looking at the world from outside, and then you hit play and ship a separate runtime. Even their in-engine editing is you-at-a-workstation manipulating a scene you're looking at.</p>
<p>Cinevva World inverts that. You create from inside the world, as an avatar standing in the same space your players will stand in, by describing what you want and watching it appear. Creation and play are one continuous session, not a build step bridged to a runtime. The closest reference points are Roblox, Rec Room, and Horizon Worlds, not web 3D engines, and even those keep most creation at a desk. The thing that makes it tractable without a mouse-driven editor is the AI builder, which turns &quot;put a lantern-lit dock here&quot; into geometry and placement.</p>
<p>You can't bolt that onto a general-purpose engine, because it isn't a rendering feature. It's the premise of the whole stack, from how terrain is editable at runtime to how the network treats every object as something a person standing nearby just made.</p>
<h2 id="when-you-should-not-do-what-we-did" tabindex="-1">When you should not do what we did <a class="header-anchor" href="#when-you-should-not-do-what-we-did" aria-label="Permalink to &quot;When you should not do what we did&quot;"></a></h2>
<p>Here's the part the &quot;we built our own engine&quot; genre usually skips. If you want to ship a browser game this quarter, do not copy us. Use PlayCanvas if you want a Unity-style editor and a managed pipeline. Use Babylon if you want a full engine with physics included and you're happy inside its structure. Use Three.js if you want maximum control and minimum opinion and you have the team to build above it. Port from Unity if you already have a Unity game.</p>
<p>Writing your own renderer, physics, and animation is only the right call when your product's premise is incompatible with the shelf, when the thing you're building isn't a game on an engine but a place that happens to be made of one. That was true for us. It probably isn't true for you, and that's fine. The point of doing the evaluation honestly is knowing which case you're in before you write the first line.</p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[An empty world is a sad world: what every creator platform that lived or died can teach us]]></title>
            <link>https://app.cinevva.com/blog/2026-06-06-how-creator-worlds-live-or-die</link>
            <guid>https://app.cinevva.com/blog/2026-06-06-how-creator-worlds-live-or-die</guid>
            <pubDate>Sat, 06 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We built an AI that constructs whole buildings in a shared 3D world from a sentence. Before we open it to everyone, we studied the launches of every comparable platform we could find. The graveyard is enormous, the survivors are few, and the line between them is sharper than the hype ever admits. Here are the stories, the numbers, and what we're doing differently.]]></description>
            <content:encoded><![CDATA[<h1 id="an-empty-world-is-a-sad-world-what-every-creator-platform-that-lived-or-died-can-teach-us" tabindex="-1">An empty world is a sad world: what every creator platform that lived or died can teach us <a class="header-anchor" href="#an-empty-world-is-a-sad-world-what-every-creator-platform-that-lived-or-died-can-teach-us" aria-label="Permalink to &quot;An empty world is a sad world: what every creator platform that lived or died can teach us&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<figure style="margin:1.5rem 0">
<img src="/img/blog/ai-builder-hero.webp" alt="Cinevva's AI Builder constructing a two-story house from a chat prompt" style="width:100%;border-radius:10px">
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Our AI Builder. Someone typed "build a second floor" and the AI laid the floor deck, raised the walls, and offered stairs and a roof next, all inside a world other people can walk into.</figcaption>
</figure>
<p>We can generate worlds now, and I don't mean in a demo reel. The picture above is our editor. Someone typed &quot;build a second floor&quot; into a chat box and watched the AI lay the floor deck, raise the front, side, and back walls, and offer to keep going with stairs and a roof. Not props, not textures, whole structures, dropped into a world other people can walk into. Our pipeline already makes the parts too: 3D models, music, sound, materials.</p>
<p>So we're close to opening Cinevva World to the public. And before we do, I went and read the obituaries.</p>
<p>Because here's the uncomfortable truth about what we're building. For fifteen years, very smart, very well-funded teams have tried to make shared creative worlds work. A few became some of the largest things humanity has ever built. Most are dead. The gap between the two outcomes is not talent and it is not money, because some of the dead had more of both than we will ever have. The gap is a small number of decisions that almost nobody gets right on the first try. This is what I found.</p>
<h2 id="the-graveyard" tabindex="-1">The graveyard <a class="header-anchor" href="#the-graveyard" aria-label="Permalink to &quot;The graveyard&quot;"></a></h2>
<p>Start with the failures, because they're more honest than the success stories. Winners rewrite their history into a clean narrative. Losers leak memos.</p>
<h3 id="meta-horizon-worlds-the-most-expensive-empty-room-ever-built" tabindex="-1">Meta Horizon Worlds: the most expensive empty room ever built <a class="header-anchor" href="#meta-horizon-worlds-the-most-expensive-empty-room-ever-built" aria-label="Permalink to &quot;Meta Horizon Worlds: the most expensive empty room ever built&quot;"></a></h3>
<p>Meta spent tens of billions on its metaverse bet. Reality Labs posted a <a href="https://www.cnbc.com/2026/03/18/meta-horizon-worlds-metaverse-vr.html" target="_blank" rel="noreferrer">$6.02 billion operating loss in a single quarter</a> reported in early 2026, and it had been bleeding multiple billions per quarter for years. The flagship was Horizon Worlds. Here is what that money bought.</p>
<figure style="margin:1.5rem 0">
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:10px">
<iframe src="https://www.youtube-nocookie.com/embed/mNbz7-ejnZw" title="Horizon Worlds - Official Trailer" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
</div>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Meta's Horizon Worlds, official trailer. The platform Meta spent billions on drops VR support in June 2026 to survive as a mobile app.</figcaption>
</figure>
<p>A leaked internal memo, reported by <a href="https://fortune.com/2022/10/07/metas-horizon-world-quality-poor-not-even-employees-using-says-metaverse-vp-leaked-memo/amp/" target="_blank" rel="noreferrer">Fortune</a> and <a href="https://ryanschultz.com/2022/10/07/leaked-internal-memos-from-meta-detail-problems-with-horizon-worlds-and-horizon-workrooms/" target="_blank" rel="noreferrer">Ryan Schultz</a>, revealed that only about 9 percent of the worlds creators built were ever visited by 50 or more people. The vast majority got zero visits beyond their own maker. One memo summed up the engagement problem in a sentence that should be tattooed on every founder in this space: <a href="https://kotaku.com/meta-facebook-horizon-worlds-vr-mark-zuckerberg-1849669048" target="_blank" rel="noreferrer">&quot;an empty world is a sad world.&quot;</a> Researchers wanting to survey active users could find only 514 of them, because the player base was that small.</p>
<p>The retention numbers were worse. Leaked data showed <a href="https://mixed-news.com/en/horizon-worlds-leak-only-1-in-10-users-return-web-launch-coming/" target="_blank" rel="noreferrer">only about 1 in 10 users returned</a> after their first month. Meta quietly cut its own 2022 target from 500,000 monthly users down to 200,000, and even then the platform had fewer concurrent users than VRChat and fewer than Second Life, a game from 2003. Internal dogfooding dashboards showed that Meta's own employees weren't using it, which pushed management to start holding managers accountable for weekly logins. A VP put the team into a &quot;quality lockdown&quot; for the rest of the year and admitted, in writing, that they were working on a product that had not found product-market fit.</p>
<p>In March 2026, Meta <a href="https://www.cnbc.com/2026/03/18/meta-horizon-worlds-metaverse-vr.html" target="_blank" rel="noreferrer">announced it was killing VR support entirely</a> and turning Horizon Worlds into an almost-exclusively mobile app, an entry point for people without headsets, functioning, in their own framing, like Roblox. The most expensive metaverse ever built ended its life trying to become the thing it should have been on day one.</p>
<p>The lesson isn't &quot;VR was too early,&quot; though it was. The lesson is that no amount of capital fills an empty world. Content density is not something you can buy with a marketing budget. If the worlds are empty, people leave, and when people leave, the worlds stay empty. That loop ate Meta alive.</p>
<h3 id="decentraland-and-the-sandbox-the-billion-dollar-ghost-towns" tabindex="-1">Decentraland and The Sandbox: the billion-dollar ghost towns <a class="header-anchor" href="#decentraland-and-the-sandbox-the-billion-dollar-ghost-towns" aria-label="Permalink to &quot;Decentraland and The Sandbox: the billion-dollar ghost towns&quot;"></a></h3>
<p>In October 2022, a single data point set the crypto world on fire. <a href="https://www.coindesk.com/web3/2022/10/07/its-lonely-in-the-metaverse-decentralands-38-daily-active-users-in-a-13b-ecosystem" target="_blank" rel="noreferrer">DappRadar reported</a> that Decentraland had 38 daily active users. The Sandbox had 522. Each of these projects carried a market capitalization around $1.3 billion at the time.</p>
<figure style="margin:1.5rem 0">
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:10px">
<iframe src="https://www.youtube-nocookie.com/embed/Zg5vcdEeLOA" title="The Sandbox Official Teaser" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
</div>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">The Sandbox, official teaser. It and Decentraland each carried valuations around $1.3 billion while their daily users numbered in the hundreds to low thousands.</figcaption>
</figure>
<p>Now, the projects pushed back, and the nuance actually matters more than the dunk. The 38 figure counted only unique wallets making on-chain transactions in a day, what DappRadar calls &quot;payers,&quot; not people who logged in and walked around without touching the blockchain. Decentraland countered that it had <a href="https://blockworks.com/news/metaverse-platforms-set-the-record-straight-about-daily-active-users" target="_blank" rel="noreferrer">56,697 monthly logged-in users</a> and roughly 8,000 daily, by other estimates. The Sandbox claimed 39,000 daily during an alpha season with a day-14 retention of 40 percent.</p>
<p>But sit with the gap for a second, because it's the most important number in this whole essay. Even taking the charitable figures, you have billion-dollar valuations sitting on top of, at best, a few thousand daily users. For comparison, the research turned up that non-blockchain Roblox had <a href="https://blockworks.com/news/metaverse-platforms-set-the-record-straight-about-daily-active-users" target="_blank" rel="noreferrer">52 million daily active users but only 11.3 million monthly paying users</a> in the same period. Most people in free virtual worlds transact rarely or never. That cuts both ways. It means the 38-DAU dunk was unfair, and it means the billion-dollar valuation was a fantasy. Both things measured the wrong number. The valuation measured token speculation. The dunk measured wallet transactions. Neither measured whether anyone was having fun.</p>
<p>That is the vanity-metric trap in its purest form. Registered accounts, token market cap, total addressable market, press impressions: none of them tell you if your world is alive. The only numbers that do are how many people come back, and whether the people who make things keep making things.</p>
<h3 id="dreams-the-most-beautiful-tool-nobody-could-stay-in" tabindex="-1">Dreams: the most beautiful tool nobody could stay in <a class="header-anchor" href="#dreams-the-most-beautiful-tool-nobody-could-stay-in" aria-label="Permalink to &quot;Dreams: the most beautiful tool nobody could stay in&quot;"></a></h3>
<p>Media Molecule, the studio behind LittleBigPlanet, spent the better part of a decade building Dreams, a creation tool so deep that people made playable games, animated short films, and music albums inside it. Critics adored it. And in <a href="https://www.pushsquare.com/news/2023/07/dreams-live-service-ending-as-media-molecule-couldnt-find-a-sustainable-path" target="_blank" rel="noreferrer">September 2023, the studio ended live service</a>, saying plainly that they could not find a sustainable business model.</p>
<figure style="margin:1.5rem 0">
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:10px">
<iframe src="https://www.youtube-nocookie.com/embed/2ltgkcoQzow" title="Dreams | Release Date Trailer | PS4" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
</div>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Dreams, by Media Molecule. A creation tool people adored, whose live service ended in 2023 with no way for creators to earn a cent.</figcaption>
</figure>
<p>The postmortem is a checklist of self-inflicted wounds, and I say that with enormous respect, because we could make every one of them. Dreams <a href="https://www.pushsquare.com/news/2023/07/dreams-live-service-ending-as-media-molecule-couldnt-find-a-sustainable-path" target="_blank" rel="noreferrer">never let creators monetize their work</a>. The promised in-game store never shipped. There were no cosmetics, no battle pass, nothing. Serious creators, the kind who turn a tool into a platform, had no reason to invest years when there was no upside. It launched PS4-only, with no PC version and no native PS5 build at launch, which capped its possible audience hard. And the co-founder <a href="https://icon-era.com/threads/media-molecule-co-founders-biggest-regret-with-dreams-was-not-focusing-on-multiplayer-understands-new-ip-is-more-of-a-game-than-a-creative-tool.11724/" target="_blank" rel="noreferrer">Mark Healey later said his biggest regret was not focusing on multiplayer</a>, and that the team came to understand their creation worked better as a game than as a creative tool.</p>
<p>Read that last part again. The most talented UGC studio on earth concluded, after the fact, that the thing should have been a game first and a tool second. That is the single most important sentence in this entire study, and we'll come back to it.</p>
<h3 id="the-rest-of-the-cemetery" tabindex="-1">The rest of the cemetery <a class="header-anchor" href="#the-rest-of-the-cemetery" aria-label="Permalink to &quot;The rest of the cemetery&quot;"></a></h3>
<p>The pattern repeats with grim consistency.</p>
<p><a href="https://www.gamedeveloper.com/business/crayta-to-shut-down-after-launch-platform-stadia-bites-the-dust" target="_blank" rel="noreferrer">Crayta</a>, a collaborative UGC creation platform, shut down in March 2023 and users lost everything they'd made. Its developer was acquired by Meta in 2021 and the platform still got killed. The proximate cause: it launched on Google Stadia, and when Stadia died, Crayta died with it. Platform dependency is a loaded gun pointed at your own head.</p>
<figure style="margin:1.5rem 0">
<img src="https://cdn.cinevva.com/blog/lived-or-died-crayta.webp" alt="Crayta, the collaborative game-creation platform" loading="lazy" style="width:100%;border-radius:10px">
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Crayta launched on Google Stadia. When Stadia shut down in 2023, Crayta went with it, and creators lost everything they'd made. Trailer still.</figcaption>
</figure>
<p>Project Spark, Microsoft's UGC game-maker, shut down years earlier on the same arc: a powerful tool, not enough people who stayed. Sansar, Linden Lab's VR successor to Second Life, was sold off after failing to find an audience. High Fidelity, the second metaverse company from Second Life's own founder Philip Rosedale, pivoted away from virtual worlds entirely.</p>
<figure style="margin:1.5rem 0">
<img src="https://cdn.cinevva.com/blog/lived-or-died-secondlife.webp" alt="Second Life, Linden Lab's user-created virtual world" loading="lazy" style="width:100%;border-radius:10px">
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Second Life, Linden Lab's 2003 world, is still running after more than two decades. Its own VR successors, Sansar and Philip Rosedale's High Fidelity, both failed. Trailer still.</figcaption>
</figure>
<p>And in a signal you can't ignore because it's happening right now, Rec Room <a href="https://www.uploadvr.com/vrchat-statement-after-rec-room-and-horizon-worlds-fold/" target="_blank" rel="noreferrer">announced it is shutting down effective June 1, 2026</a>, stating it never reached sustained profitability.</p>
<p>Founders of metaverse companies, building metaverses, could not stay in their own metaverses. That should terrify anyone who thinks the hard part is the rendering.</p>
<figure style="margin:1.5rem 0">
<img src="https://cdn.cinevva.com/blog/lived-or-died-recroom.webp" alt="Rec Room, the cross-platform social UGC world" loading="lazy" style="width:100%;border-radius:10px">
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Rec Room, one of the largest social creation worlds, reached over 150 million lifetime players and still shut down on June 1, 2026, never having turned that reach into a sustainable business. Trailer still.</figcaption>
</figure>
<h2 id="the-survivors-and-what-actually-saved-them" tabindex="-1">The survivors, and what actually saved them <a class="header-anchor" href="#the-survivors-and-what-actually-saved-them" aria-label="Permalink to &quot;The survivors, and what actually saved them&quot;"></a></h2>
<p>Now the living. The instructive thing is that none of them won the way you'd expect, and several won despite breaking rules the failures followed.</p>
<h3 id="roblox-the-twelve-year-overnight-success" tabindex="-1">Roblox: the twelve-year overnight success <a class="header-anchor" href="#roblox-the-twelve-year-overnight-success" aria-label="Permalink to &quot;Roblox: the twelve-year overnight success&quot;"></a></h3>
<p>Everyone cites Roblox as the model. Almost nobody copies what actually happened, because what actually happened was twelve years of looking like a failure.</p>
<figure style="margin:1.5rem 0">
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:10px">
<iframe src="https://www.youtube-nocookie.com/embed/LTnMKjXEnMY" title="ROBLOX - Game Trailer" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
</div>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Roblox. It drew about 100 people in 2006 and took twelve years to reach exponential growth, all on the back of letting the community build the games.</figcaption>
</figure>
<p>Roblox's 2006 beta drew <a href="https://www.howtheygrow.co/p/how-roblox-grows" target="_blank" rel="noreferrer">around 100 tech enthusiasts with a peak concurrency of 30 to 40 people</a>. It then ground forward for over a decade. It went from 9 million users in early 2016 to 90 million in 2019 to 214 million by 2023. Founder David Baszucki refused to pivot off the user-generated-content vision even when Minecraft exploded past him in 2009. He treated it as a deliberate slow bake, and the one move that mattered was the decision to let the community build all the games rather than building them in-house. That removed content cost and turned every creator into a growth engine.</p>
<p>But here is the part the cargo-culters miss, and it's why I keep a second source open. Matthew Ball's <a href="https://www.matthewball.co/all/roblox2024" target="_blank" rel="noreferrer">2024 teardown</a> shows Roblox operating at a brutal loss even at massive scale, a roughly negative-38-percent operating margin, with only 6 percent of users buying its currency in a given month and revenue per user a fraction of console platforms. Roblox is the biggest game in the world and it still struggles to make money. The flywheel is real and the economics are hard. Anyone who tells you a creator platform is a license to print money is selling you something.</p>
<p>The deeper lesson, though, is hiding in plain sight in those user numbers. The overwhelming majority of Roblox's hundreds of millions of users never build anything. They show up to <em>play</em> a catalog of tens of millions of games other people made. That catalog took a decade and a developer-payout economy to grow. Roblox is not a building tool that people happen to play. It is a place to play that a small fraction of people happen to build. That ordering is everything.</p>
<h3 id="minecraft-the-world-that-could-not-be-empty" tabindex="-1">Minecraft: the world that could not be empty <a class="header-anchor" href="#minecraft-the-world-that-could-not-be-empty" aria-label="Permalink to &quot;Minecraft: the world that could not be empty&quot;"></a></h3>
<p>Minecraft never had the empty-world problem, and understanding why is the whole game.</p>
<figure style="margin:1.5rem 0">
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:10px">
<iframe src="https://www.youtube-nocookie.com/embed/MmB9b5njVbA" title="Official Minecraft Trailer" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
</div>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Minecraft, official trailer. Procedural survival meant the world was never empty and never needed user content to be worth playing.</figcaption>
</figure>
<p>Minecraft shipped as a survival game with procedural generation. The world builds itself, infinitely, and on minute one you are not staring at a blank canvas wondering what to do. Night is falling and something is going to kill you, so you dig. The game is fun for one person with zero other users and zero user-generated content, forever, because the procedural generator is an inexhaustible content machine. The mods, the servers, the custom maps, the entire creative ecosystem, all of that grew <em>on top of</em> a game that already sold itself. Creative mode came after millions were already hooked on survival. Word of mouth and YouTube did the rest.</p>
<p>Minecraft solved cold-start by never having one. That is the cheat code, and we'll steal it.</p>
<h3 id="browser-games-distribution-as-the-entire-moat" tabindex="-1">Browser games: distribution as the entire moat <a class="header-anchor" href="#browser-games-distribution-as-the-entire-moat" aria-label="Permalink to &quot;Browser games: distribution as the entire moat&quot;"></a></h3>
<p>Then there's the genre that's closest to our actual delivery mechanism, the no-download browser game.</p>
<figure style="margin:1.5rem 0">
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:10px">
<iframe src="https://www.youtube-nocookie.com/embed/oznO3GGzu2s" title="Krunker.io trailer" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
</div>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Krunker, a browser FPS in the .io tradition. The genre's whole moat is distribution, a link that loads a full game with no download.</figcaption>
</figure>
<p><a href="https://news.viverse.com/post/what-are-io-games" target="_blank" rel="noreferrer">Agar.io reached roughly 5 million daily players within weeks</a> of its 2015 launch. Slither.io racked up 68 million mobile downloads and 67 million browser plays. The research is blunt about why: <a href="https://news.viverse.com/post/what-are-io-games" target="_blank" rel="noreferrer">&quot;the competitive moat for the .io genre is distribution itself, not the game mechanic.&quot;</a> You click a link and you are playing. No install, no account, no app store. <a href="https://medium.com/the-megacool-blog/9-growth-hacks-from-viral-hit-game-agar-io-7d8f71f70a0c" target="_blank" rel="noreferrer">Agar.io's onboarding was one click</a>: type a name, hit play, you're in. It spread through chat apps the way video spreads on TikTok.</p>
<p>But there's a knife in this gift, and it cuts our way too. No download means no commitment. The same frictionlessness that gets someone in gets them out, because closing a tab costs nothing. Browser virality is real and it is also shallow by default. The .io games survived it by being instantly, obviously fun in the first ten seconds. The browser worlds that died were frictionless to enter and gave you no reason to stay once you arrived.</p>
<h3 id="among-us-figma-townscaper-vrchat-four-more-ways-to-win" tabindex="-1">Among Us, Figma, Townscaper, VRChat: four more ways to win <a class="header-anchor" href="#among-us-figma-townscaper-vrchat-four-more-ways-to-win" aria-label="Permalink to &quot;Among Us, Figma, Townscaper, VRChat: four more ways to win&quot;"></a></h3>
<p>Four quick ones, because each isolates a different lever.</p>
<p><a href="https://www.esports.net/news/how-among-us-blew-up-online-two-years-after-release/" target="_blank" rel="noreferrer">Among Us</a> launched in 2018 and was a commercial nothing for two years. The developers nearly abandoned it. Then in September 2020 a <a href="https://howtomarketagame.com/2020/09/14/among-us-the-4-lessons-of-their-viral-success/" target="_blank" rel="noreferrer">ladder of progressively bigger streamers</a> discovered it, culminating in a multi-hour Sodapoppin stream, and it became a global phenomenon. The early oxygen came from an itch.io front-page feature and a Korean audience that became half its sales. Lesson: the breakout can come years after launch, from a channel you didn't plan, if you survive long enough to be discovered.</p>
<figure style="margin:1.5rem 0">
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:10px">
<iframe src="https://www.youtube-nocookie.com/embed/NSJ4cESNQfE" title="Among Us Steam Release Trailer" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
</div>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Among Us. It sat near-unnoticed for two years before a ladder of streamers turned it into a global phenomenon in 2020.</figcaption>
</figure>
<p><a href="https://review.firstround.com/the-5-phases-of-figmas-community-led-growth-from-stealth-to-enterprise/" target="_blank" rel="noreferrer">Figma</a> stayed in stealth for about three years, launched free, and waited two more years before charging. Its activation moment was multiplayer: the instant a designer felt someone else editing the same file. And here is the part that matters. Figma's free tier originally limited you to 2 collaborators, which strangled the exact magic moment that sold the product, and they fixed it by making collaborators unlimited so people could actually feel the thing. They seeded the community by targeting design influencers on Twitter directly. Lesson: find your aha moment, then remove every obstacle between a new user and it.</p>
<figure style="margin:1.5rem 0">
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:10px">
<iframe src="https://www.youtube-nocookie.com/embed/n5gJgkO2Dg0" title="Figma product launch keynote" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
</div>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Figma. Its activation moment was multiplayer, so it scrapped the free-tier collaborator limit that was blocking the very magic that sold it.</figcaption>
</figure>
<p><a href="https://www.gamedeveloper.com/game-platforms/how-townscaper-works-a-story-four-games-in-the-making" target="_blank" rel="noreferrer">Townscaper</a> was a tiny creative toy that became a hit on the strength of screenshots. Its developer watched his Twitter following jump from around 20,000 to 90,000 just by posting clips during development, and read that as the early signal it would do well. Lesson: if your thing is beautiful and people share it unprompted before you've even launched, that is the realest validation there is.</p>
<figure style="margin:1.5rem 0">
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:10px">
<iframe src="https://www.youtube-nocookie.com/embed/hqq25n6cQqo" title="Townscaper Launch Trailer" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
</div>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:0.5rem">Townscaper, launch trailer. A creative toy with no goals that spread on the strength of screenshots people shared unprompted.</figcaption>
</figure>
<p>And <a href="https://www.uploadvr.com/vrchat-statement-after-rec-room-and-horizon-worlds-fold/" target="_blank" rel="noreferrer">VRChat</a>, in the same months that Meta's billions and Rec Room both folded, hit a record of nearly 160,000 concurrent users. It is community-driven, creator-driven, and not remotely as well funded as the things that died around it. VRChat credits its survival to community loyalty and creativity over capital. Lesson, stated by the survivor itself: engaged creator passion outlasts money.</p>
<h2 id="what-actually-separates-the-living-from-the-dead" tabindex="-1">What actually separates the living from the dead <a class="header-anchor" href="#what-actually-separates-the-living-from-the-dead" aria-label="Permalink to &quot;What actually separates the living from the dead&quot;"></a></h2>
<p>Lay the stories side by side and the pattern is not subtle.</p>
<table tabindex="0">
<thead>
<tr>
<th>Platform</th>
<th>What it bet on</th>
<th>Where it ended up</th>
<th>The lesson</th>
</tr>
</thead>
<tbody>
<tr>
<td>Minecraft</td>
<td>Procedural survival, fun for one player</td>
<td>Best-selling game ever, 350M+ copies</td>
<td>Never have an empty world</td>
</tr>
<tr>
<td>Roblox</td>
<td>Community-built games plus creator payouts</td>
<td>80M+ daily users over twelve years</td>
<td>Be a place to play first, a tool second</td>
</tr>
<tr>
<td>.io games</td>
<td>One-click, no-download browser play</td>
<td>Millions of players in weeks</td>
<td>Distribution is the moat, commitment is shallow</td>
</tr>
<tr>
<td>Figma</td>
<td>Free tier, real-time multiplayer</td>
<td>A ~$20B acquisition offer</td>
<td>Clear every barrier to the aha moment</td>
</tr>
<tr>
<td>Among Us</td>
<td>A cheap social game, and patience</td>
<td>3.8M concurrent, two years late</td>
<td>Survive long enough to be found</td>
</tr>
<tr>
<td>Townscaper</td>
<td>A beautiful toy worth screenshotting</td>
<td>~380K sales, organic spread</td>
<td>Unprompted sharing is the realest signal</td>
</tr>
<tr>
<td>VRChat</td>
<td>Community and creator energy</td>
<td>~160K concurrent in 2026</td>
<td>Passion outlasts capital</td>
</tr>
<tr>
<td>Horizon Worlds</td>
<td>Billions in capital, VR-first</td>
<td>VR shut down 2026, ~1 in 10 returned</td>
<td>Money cannot fill an empty world</td>
</tr>
<tr>
<td>Decentraland / Sandbox</td>
<td>A token economy before a game</td>
<td>~$1.3B valuations, hundreds of daily users</td>
<td>A valuation is not an audience</td>
</tr>
<tr>
<td>Dreams</td>
<td>A deep tool, no economy, no multiplayer</td>
<td>Live service ended 2023</td>
<td>Pay creators, ship multiplayer, be a game</td>
</tr>
<tr>
<td>Crayta</td>
<td>A UGC platform living on Stadia</td>
<td>Shut down with Stadia, 2023</td>
<td>Never build on a platform that can evict you</td>
</tr>
<tr>
<td>Project Spark / Sansar</td>
<td>Powerful tools, no reason to return</td>
<td>Shut down or pivoted away</td>
<td>A tool without a game has no daily habit</td>
</tr>
</tbody>
</table>
<p>The thing that kills creative worlds is the empty room. Horizon Worlds, Decentraland, Dreams, Crayta, Project Spark, Sansar, all died the same death: not enough people, not enough content, not enough reason to come back, in a loop that feeds on itself. The cold-start problem is the boss fight. Everything else is a tutorial level.</p>
<p>The thing that saves them is having a reason to exist before the crowd shows up. Minecraft had procedural survival. Roblox had a decade of patiently seeded games. The .io games had ten-second fun. Every survivor was worth your time with an empty catalog. Every casualty was a beautiful container waiting for other people to make it worth visiting. This is the Dreams confession made general: build a game first and a platform second. The platform is the second act. You earn it by first being something people use for its own sake.</p>
<p>The metrics that actually predict survival are not the ones in the press release. The research is consistent here. The defining signal of product-market fit is that your <a href="https://newsletter.pmcurve.com/p/retention-masterclass" target="_blank" rel="noreferrer">retention curve flattens</a>: some cohort of people keeps coming back forever. A curve that declines to zero means no fit, no matter how big the top of the funnel. Rough industry benchmarks are <a href="https://solsten.io/blog/d1-d7-d30-retention-in-gaming" target="_blank" rel="noreferrer">40 percent day-1, 20 percent day-7, 10 percent day-30</a> retention, with day-30 being the one that predicts long-term health. And for anything with a creator side, the supply side is where you die: <a href="https://forkoff.xyz/blog/founder-growth/two-sided-marketplace-cold-start-2026" target="_blank" rel="noreferrer">about 67 percent of failed marketplaces collapse on supply</a>, not demand. Creator retention is the vital sign. Registered accounts and valuations are the vanity.</p>
<p>A few more that fall out of the stories. Monetize your creators or lose them, because Dreams proved that talented people will not invest years for no upside. Multiplayer is usually the point, not a feature, per Media Molecule's own regret. Never build your house on a platform that can evict you, per Crayta. And no-download is a double-edged sword: it is the best acquisition channel in existence and the weakest commitment device, so the burden falls entirely on the first sixty seconds being good.</p>
<h2 id="the-trap-underneath-all-of-it" tabindex="-1">The trap underneath all of it <a class="header-anchor" href="#the-trap-underneath-all-of-it" aria-label="Permalink to &quot;The trap underneath all of it&quot;"></a></h2>
<p>There's one more layer, and it's the one specific to our moment, because we have a tool the dead never had. We can generate worlds with AI. Which raises a paradox I've gone back and forth on for weeks.</p>
<p>If it's AI, shouldn't every world be unique, generated fresh for each person, tuned to them? But if each person's world is unique, how does anyone ever bump into anyone else, given that a shared moment requires a shared place? And if the world has to be shared anyway, then why use AI at all, instead of handcrafting one beautiful place like a real studio?</p>
<p>Each of those pulls, followed to its end, lands on something that doesn't work. A world generated just for you is a world with nobody in it. Google's Genie can spin up a playable environment from a sentence and it vanishes a minute later, single-player, no memory, no neighbors. That's the lonely dead end. But handcrafting one fixed world is also a dead end: it's a hundred-person, multi-year job to fill two square kilometers by hand, and when you're done it's frozen, the same on your hundredth visit as your first, unable to grow as people arrive.</p>
<p>The way out is to stop treating &quot;the world&quot; as one thing. It's three layers, and uniqueness and sharedness can live in different ones without fighting. The ground everyone stands on is one shared world, curated to a high bar, the meeting place. What you bring to it, your AI-generated assets, the building you made, is wildly unique, and you deposit it <em>into</em> the shared world so your uniqueness becomes everyone else's discovery. And your path through it, the quests you get, what it points you toward, is personalized for free, because a path is not a place. The false choice was &quot;a personalized world,&quot; which kills multiplayer, versus &quot;a personalized path through a shared world,&quot; which deepens it. We want the second.</p>
<p>So AI's job was never to hand each person a private world. Its job is to make one shared world feel infinite, alive, and personal to move through. The uniqueness comes from everything that accretes in it over time, every creator's one-of-a-kind contribution. Every Minecraft server is unique. Everyone on a given server shares it. That is not a contradiction. That is the whole trick, and the screenshot at the top of this post, the AI building a house that stays put in a world other people can walk into, is that trick made real.</p>
<h2 id="how-we-re-launching-cinevva-world" tabindex="-1">How we're launching Cinevva World <a class="header-anchor" href="#how-we-re-launching-cinevva-world" aria-label="Permalink to &quot;How we're launching Cinevva World&quot;"></a></h2>
<p>So here is what all of this turns into for us. Not a marketing plan. Five bets, placed deliberately against the five things that killed everyone in the first half of this piece. And I'll grade each one by how much the evidence actually backs it, because a plan you can't grade is just a wish with good lighting.</p>
<p><strong>Bet one, the one I would stake the company on: we launch a game, not a platform.</strong> This is not a preference, it is the most consistent finding in the entire study. Minecraft, Roblox, and Fortnite all led with something fun to <em>do</em> and grew the creator layer on top of it. Media Molecule, the most gifted creation studio alive, concluded after the fact that Dreams should have been a game first. So the first thing a new person does in Cinevva World will not be staring at an empty plot. It will be a loop with a goal, a reason to be there on a Tuesday when almost no one else is online. Most people who show up will play and never build, and that is the design, not a failure. It is Roblox's actual shape: hundreds of millions who play, sitting on a thin layer who create. The flywheel is the second act, and we have to earn it. I'm sure of this one because the graveyard already ran the experiment.</p>
<p><strong>Bet two, the one that is honestly a wager: AI lets us manufacture the supply everyone else died waiting for.</strong> I want to be precise about why this is a bet and not a guarantee, because no platform in this study was ever bootstrapped with generative AI. I have a mechanism, not a proof. The mechanism: roughly two thirds of two-sided platforms die on the supply side, not the demand side. The classic fix is to fake supply until it's real, the way DoorDash drove the food itself, Reddit's team posted under fake handles, and Roblox's founders hand-built the early games for years. We can do that with generation, at a cost and speed none of them had. But here is the part that keeps me honest, and it is the Decentraland tombstone: Decentraland was full of generated stuff and drew about thirty people a day, because pretty is not the same as worth-your-time. So I am not betting that AI makes the world fun. It can't. I am betting that AI removes the asset bottleneck so that every hour of our scarce human effort goes into the one thing that actually retains people, the loop. AI builds the hill cheaply. We hand-build the reason to climb it. And we cap the synthetic seed at <a href="https://forkoff.xyz/blog/founder-growth/two-sided-marketplace-cold-start-2026" target="_blank" rel="noreferrer">roughly 30 percent, converting it to real creator content inside about 60 days</a>, because fake supply that never turns real breaks trust the instant anyone notices.</p>
<p><strong>Bet three: we publish our own kill-criteria.</strong> This is the difference between a grounded plan and a hopeful one. The metric that predicted survival in every single case was whether the <a href="https://newsletter.pmcurve.com/p/retention-masterclass" target="_blank" rel="noreferrer">retention curve flattens</a>, whether some cohort keeps coming back instead of decaying to zero. So that is the test we hold ourselves to, out loud. If alpha day-7 retention does not flatten, if the players who never build do not return, if the AI-seeded content gets walked past and ignored, the thesis is wrong, and we change it or we stop. We will track players and creators as two separate funnels with two separate aha moments, because the data is clear they are not the same person. And we will not pretend money does the early work. At fifty users, or even a thousand, tips and marketplace earnings round to zero, so they retain no one yet. Early retention comes from the loop or it does not come at all. Dreams is the receipt for getting that backwards.</p>
<p><strong>Bet four: we start narrow on purpose.</strong> The research on networked products is unanimous. You do not launch to a market, you launch to an <a href="https://www.lennysnewsletter.com/p/atomic-network" target="_blank" rel="noreferrer">&quot;atomic network,&quot;</a> the smallest group dense enough to be alive on its own. Facebook started at one school. Ours already exists: the Cinevva creators who have generated 3D assets, music, and sound, and who already have inventory to place. Fifty of them in one world at the same hour, with a goal and a frontier, is a denser and more alive thing than fifty thousand scattered signups will ever be. The evidence backs this one hard.</p>
<p><strong>Bet five, the softest, and I'll say so: we tell the truth about the emptiness instead of hiding it.</strong> A polished, quiet world reads as dead, and the failures all tried to cover that with ambient effects. We will try the opposite. This is the least proven move of the five, closer to a hunch than a finding. The first people in are not visiting an unfinished product. They are the founding citizens of a place that is empty <em>because they're early</em>, and what they build will still be standing when the next thousand arrive.</p>
<p>That is the plan, graded honestly. Four bets the graveyard strongly supports, and one real wager on a tool none of the dead had, fenced in by the exact discipline they lacked. We can generate the hill. We cannot generate the feeling of being first up it, or the stranger who waves from the top. The job is to make that wave possible, prove it on the retention curve, and stop fooling ourselves the moment the curve says no.</p>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/blog/ai-builder-hero.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 30: A camera that respects walls]]></title>
            <link>https://app.cinevva.com/blog/2026-06-04-open-world-browser-part-30-collision-aware-camera</link>
            <guid>https://app.cinevva.com/blog/2026-06-04-open-world-browser-part-30-collision-aware-camera</guid>
            <pubDate>Thu, 04 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Twenty-nine parts built a world you can walk, swim, and build in, all watched by a camera that clipped straight through hills and house walls. Part 30 fixes that with a collision-aware boom: a spring arm that owns only its own length, a pluggable collider contract so terrain, props, and buildings all stop the camera through the same interface, and a signed-distance probe that honours the one thing a heightmap never could, overhangs and caves. It plugs in as a post-process over OrbitControls without rewriting a line of the orbit input we already trusted.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-30-a-camera-that-respects-walls" tabindex="-1">Building an open world in the browser, part 30: A camera that respects walls <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-30-a-camera-that-respects-walls" aria-label="Permalink to &quot;Building an open world in the browser, part 30: A camera that respects walls&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-15-open-world-browser-part-29-pluggable-character.html">Part 29</a> gave us one controller that drives any body. The body moves correctly now. It walks, slides, swims, glides, climbs into caves, and ducks under overhangs. The problem is what's watching it. For twenty-nine parts the camera was a stock orbit rig that followed the player and did exactly one clever thing to avoid embarrassment, it refused to tilt below the horizon so it couldn't slide under flat ground. That clamp is the tell. It exists because the camera had no idea where the world's geometry actually was, so the only defence against clipping was to forbid the angles where clipping was most likely. Walk up to a hill and the camera sat inside the hill. Step into one of the marching-cubes caves from <a href="/blog/2026-02-25-open-world-browser-part-07-marching-cubes.html">Part 7</a> and you were looking at the inside of a rock. Build a house with the authoring tools from <a href="/blog/2026-04-28-open-world-browser-part-16-structure-and-authoring.html">Part 16</a> and stand in a room, and the camera floated outside the wall looking at siding. This part gives the camera the same respect for geometry the body already has.</p>
<h2 id="the-shape-of-the-problem-and-the-shape-of-the-fix" tabindex="-1">The shape of the problem, and the shape of the fix <a class="header-anchor" href="#the-shape-of-the-problem-and-the-shape-of-the-fix" aria-label="Permalink to &quot;The shape of the problem, and the shape of the fix&quot;"></a></h2>
<p>A third-person camera has one job that's hard and a dozen that are easy. The easy ones are following, smoothing, and orbit input, and we already had those. The hard one is the academic literature calls it a visibility constraint, and the survey everyone cites, Christie and Olivier's <em>Camera Control in Computer Graphics</em>, frames the whole field around it: keep the subject framed and unoccluded while respecting the world. At runtime that reduces to a deceptively simple question asked every frame. The player is the pivot. The user has rotated and zoomed to a desired camera position some distance behind them. How far back can the camera actually sit along that line before it pokes into something solid? Answer that honestly and the camera tucks itself in front of the hill, slides down the boom as you back into a corner, and stops at the cave ceiling instead of punching through it.</p>
<p>The pattern that answers it is old and proven. Unreal calls it a spring arm, Godot ships a <code>SpringArm3D</code> node, and Unity's Cinemachine splits it across a third-person follow rig and a deoccluder extension. The idea is always the same. Hang the camera off the end of a boom anchored at the pivot. Hold it at the desired length when the path is clear, retract it toward the pivot when something's in the way, and spring it back out when the way clears. Mark Haigh-Hutchinson's <em>Real-Time Cameras</em>, written by the camera lead on Metroid Prime, spends whole chapters on the failure modes that turn a naive version of this into something that makes players sick. We took the pattern and built our own, small enough to read in one sitting, in <code>public/world/src/camera-rig.mjs</code>.</p>
<figure style="margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12);border-radius:8px;overflow:hidden;background:#0d1117">
<svg viewBox="0 0 680 290" width="100%" role="img" aria-label="A spring-arm boom retracting along its own line so the probe sphere rests against a wall instead of clipping through it" style="display:block">
  <rect x="382" y="40" width="46" height="222" fill="#39424f" stroke="#5b6675"/>
  <text x="405" y="280" fill="#9aa7b4" font-size="13" text-anchor="middle" font-family="sans-serif">wall</text>
  <line x1="110" y1="212" x2="600" y2="70" stroke="#9aa7b4" stroke-width="2" stroke-dasharray="6 6"/>
  <line x1="110" y1="212" x2="360" y2="140" stroke="#6ea8fe" stroke-width="3"/>
  <circle cx="110" cy="212" r="6" fill="#e6edf3"/>
  <text x="110" y="234" fill="#e6edf3" font-size="13" text-anchor="middle" font-family="sans-serif">pivot (player head)</text>
  <circle cx="600" cy="70" r="9" fill="none" stroke="#9aa7b4" stroke-width="2"/>
  <text x="600" y="52" fill="#9aa7b4" font-size="13" text-anchor="middle" font-family="sans-serif">desired (user zoom)</text>
  <circle cx="360" cy="140" r="22" fill="rgba(110,168,254,0.18)" stroke="#6ea8fe" stroke-width="2"/>
  <circle cx="360" cy="140" r="5" fill="#6ea8fe"/>
  <text x="300" y="104" fill="#6ea8fe" font-size="13" text-anchor="middle" font-family="sans-serif">camera + probe radius r</text>
  <text x="222" y="192" fill="#6ea8fe" font-size="13" font-family="sans-serif" transform="rotate(-16 222 192)">ℓ (clamped)</text>
</svg>
<figcaption style="padding:0.6rem 1rem;color:#9aa7b4;font-size:0.85rem;border-top:1px solid rgba(255,255,255,0.12);font-family:sans-serif">The boom holds the camera at the user's desired zoom until an obstacle intrudes, then retracts along the same line so the probe sphere rests against the surface instead of the near plane punching through it.</figcaption>
</figure>
<h2 id="a-boom-that-owns-only-its-length" tabindex="-1">A boom that owns only its length <a class="header-anchor" href="#a-boom-that-owns-only-its-length" aria-label="Permalink to &quot;A boom that owns only its length&quot;"></a></h2>
<p>The design rule that kept the rig small is a borrow from the controller work in Part 29: own one thing, completely, and refuse the rest. The rig owns the length of the boom and nothing else. Yaw, pitch, damping of the orbit, touch and wheel gestures, all of that stays with OrbitControls, which already solves it well and which we have no interest in rewriting. So the rig is not a camera controller. It's a post-process that runs after the orbit math and corrects exactly one number, the distance from pivot to camera.</p>
<p>That decision sounds tidy and it almost broke immediately, because of how OrbitControls thinks. At the top of every update it reads the camera's current position and derives its orbit radius from it. That's normally invisible. But the instant our rig shortens the camera to dodge a wall, the next frame OrbitControls reads that shortened position, concludes the user must have zoomed in, and bakes the shortening into the user's desired zoom. A few frames of that and the camera has collapsed onto the player's head and won't come back out. The fix is two calls that bracket the orbit update and it's the whole integration. Before OrbitControls runs, <code>beforeControls()</code> restores the camera to last frame's full, un-shortened distance, so the orbit math always reads the user's true zoom. After OrbitControls runs, <code>afterControls(dt)</code> reads that freshly orbited desired position, resolves the collision, damps the length, and writes the camera where it should actually render. The user's intent and the collision correction never touch each other, and the dolly stays exactly as responsive as it was before the rig existed. The headless test we wrote for the rig pins this precisely: drive the boom into a wall for sixty frames, clear the wall, and the length springs back to the user's full zoom of ten metres rather than sticking at the collision distance.</p>
<h2 id="one-contract-any-collider" tabindex="-1">One contract, any collider <a class="header-anchor" href="#one-contract-any-collider" aria-label="Permalink to &quot;One contract, any collider&quot;"></a></h2>
<p>The rig never asks what the world is made of. A collider is just an object with a <code>probe</code> method, and the rig hands it a ray, a maximum distance, and the camera's probe radius, and gets back one number, the furthest the camera may travel before that collider blocks it. The rig queries every registered collider and takes the nearest hit. That's the entire contract, and it's the same move the character controller made when it turned locomotion into pluggable behaviours. Terrain plugs in, props plug in, building shells plug in, each behind the same <code>probe</code>, and the rig stays ignorant of all of them. Adding a new kind of obstacle is adding a collider to a list, not editing the camera.</p>
<p>One detail in the contract earns its keep, and it's the probe radius. We don't cast a thin ray from pivot to camera, we cast a sphere big enough to contain the camera's near plane. A single ray stops the camera centre at the wall, but the near plane has width, so its corners would already be buried in the wall before the centre ray ever reported a hit. Sweeping a small sphere instead of a ray is what every shipping implementation does, Unreal exposes it as the probe size, and it's the difference between a camera that rests cleanly against a surface and one that lets you see through it at the edges of the screen.</p>
<p>We don't guess that radius, we derive it. The furthest point of the near plane from the camera is one of its corners, and the distance to it falls straight out of the projection. With near plane <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.357ex" height="1.025ex" role="img" focusable="false" viewBox="0 -442 600 453" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>n</mi></math></mjx-assistive-mml></mjx-container> and vertical field of view <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.023ex;" xmlns="http://www.w3.org/2000/svg" width="1.061ex" height="1.618ex" role="img" focusable="false" viewBox="0 -705 469 715" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D703" d="M35 200Q35 302 74 415T180 610T319 704Q320 704 327 704T339 705Q393 701 423 656Q462 596 462 495Q462 380 417 261T302 66T168 -10H161Q125 -10 99 10T60 63T41 130T35 200ZM383 566Q383 668 330 668Q294 668 260 623T204 521T170 421T157 371Q206 370 254 370L351 371Q352 372 359 404T375 484T383 566ZM113 132Q113 26 166 26Q181 26 198 36T239 74T287 161T335 307L340 324H145Q145 321 136 286T120 208T113 132Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>θ</mi></math></mjx-assistive-mml></mjx-container>, the half-height is <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="14.408ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 6368.2 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(853.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1909.6,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2676.2,0)"><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(389,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(889,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4121.2,0)"><path data-c="2061" d="" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4121.2,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4510.2,0)"><path data-c="1D703" d="M35 200Q35 302 74 415T180 610T319 704Q320 704 327 704T339 705Q393 701 423 656Q462 596 462 495Q462 380 417 261T302 66T168 -10H161Q125 -10 99 10T60 63T41 130T35 200ZM383 566Q383 668 330 668Q294 668 260 623T204 521T170 421T157 371Q206 370 254 370L351 371Q352 372 359 404T375 484T383 566ZM113 132Q113 26 166 26Q181 26 198 36T239 74T287 161T335 307L340 324H145Q145 321 136 286T120 208T113 132Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(4979.2,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mn" transform="translate(5479.2,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(5979.2,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>h</mi><mo>=</mo><mi>n</mi><mi>tan</mi><mo data-mjx-texclass="NONE">⁡</mo><mo stretchy="false">(</mo><mi>θ</mi><mrow data-mjx-texclass="ORD"><mo>/</mo></mrow><mn>2</mn><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>, the half-width is <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.439ex;" xmlns="http://www.w3.org/2000/svg" width="13.744ex" height="2.009ex" role="img" focusable="false" viewBox="0 -694 6075 888" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(993.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2049.6,0)"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2847.8,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(3348,0)"><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(894,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1450,0)" style="stroke-width: 3;"/><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" transform="translate(1894,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(2338,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>w</mi><mo>=</mo><mi>h</mi><mo>⋅</mo><mtext>aspect</mtext></math></mjx-assistive-mml></mjx-container>, and the corner sits at</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.357ex;" xmlns="http://www.w3.org/2000/svg" width="22.335ex" height="2.878ex" role="img" focusable="false" viewBox="0 -1114.2 9872 1272" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(484,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mtext"><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(1500,0)" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(2149.6,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msqrt" transform="translate(3205.4,0)"><g transform="translate(1020,0)"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(633,289) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1258.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(2259,0)"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(749,289) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3633.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(4634,0)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(609,289) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(0,204.2)"><path data-c="221A" d="M263 249Q264 249 315 130T417 -108T470 -228L725 302Q981 837 982 839Q989 850 1001 850Q1008 850 1013 844T1020 832V826L741 243Q645 43 540 -176Q479 -303 469 -324T453 -348Q449 -350 436 -350L424 -349L315 -96Q206 156 205 156L171 130Q138 104 137 104L111 130L263 249Z" style="stroke-width: 3;"/></g><rect width="5646.5" height="60" x="1020" y="994.2"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><msub><mi>r</mi><mrow data-mjx-texclass="ORD"><mtext>near</mtext></mrow></msub><mo>=</mo><msqrt><msup><mi>n</mi><mn>2</mn></msup><mo>+</mo><msup><mi>w</mi><mn>2</mn></msup><mo>+</mo><msup><mi>h</mi><mn>2</mn></msup></msqrt></math></mjx-assistive-mml></mjx-container><p>The probe radius is that corner distance times a small safety margin, with a fixed floor so it never drops below a sensible minimum on a very narrow frustum. Whenever the field of view, aspect ratio, or near plane changes, the radius is recomputed, so a window resize or a zoom that touches the projection can't quietly leave the probe too small to cover the corners it's meant to protect.</p>
<h2 id="the-collider-that-knows-about-caves" tabindex="-1">The collider that knows about caves <a class="header-anchor" href="#the-collider-that-knows-about-caves" aria-label="Permalink to &quot;The collider that knows about caves&quot;"></a></h2>
<p>The terrain collider is where this gets interesting, because terrain in our engine isn't a heightmap. Since Part 7 it's been a signed distance field, a function that returns how far any point in space is from the nearest solid surface and whether it's inside or outside the rock. Positive is air, negative is rock, and that single fact is why our camera can do something a heightmap camera structurally cannot. A heightmap knows the ground height at an x and z. It has no concept of a ceiling, because there's only ever one surface above any point. So a heightmap camera can stop you walking into a hill, but it has no idea an overhang lip or a cave roof is hanging over the pivot, and it sails right through both. A distance field knows about every surface in three dimensions, so the same probe that stops the camera against a hillside stops it against a cave ceiling without a single special case.</p>
<figure style="margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12);border-radius:8px;overflow:hidden;background:#0d1117">
<svg viewBox="0 0 680 320" width="100%" role="img" aria-label="Under an overhang a heightmap lets the boom clip through the ceiling rock, while a signed distance field stops it at the ceiling underside" style="display:block">
  <path d="M680,320 L680,40 L250,40 L250,150 L430,150 L430,250 L250,250 L250,320 Z" fill="#39424f" stroke="#5b6675" stroke-width="1.5"/>
  <text x="340" y="205" fill="#9aa7b4" font-size="13" text-anchor="middle" font-family="sans-serif">cave</text>
  <text x="540" y="100" fill="#5b6675" font-size="13" text-anchor="middle" font-family="sans-serif">solid rock</text>
  <line x1="330" y1="212" x2="230" y2="60" stroke="#9aa7b4" stroke-width="2" stroke-dasharray="6 6"/>
  <line x1="290" y1="150" x2="250" y2="90" stroke="#f85149" stroke-width="4"/>
  <circle cx="330" cy="212" r="6" fill="#e6edf3"/>
  <text x="330" y="234" fill="#e6edf3" font-size="13" text-anchor="middle" font-family="sans-serif">player</text>
  <circle cx="230" cy="60" r="10" fill="none" stroke="#f85149" stroke-width="2"/>
  <text x="230" y="42" fill="#f85149" font-size="13" text-anchor="middle" font-family="sans-serif">heightmap: clips through ✗</text>
  <circle cx="305" cy="170" r="9" fill="rgba(86,211,100,0.2)" stroke="#56d364" stroke-width="2"/>
  <text x="420" y="172" fill="#56d364" font-size="13" text-anchor="middle" font-family="sans-serif">SDF: stops at ceiling ✓</text>
</svg>
<figcaption style="padding:0.6rem 1rem;color:#9aa7b4;font-size:0.85rem;border-top:1px solid rgba(255,255,255,0.12);font-family:sans-serif">A heightmap stores one surface per column, so it never sees the slab of rock above the player and lets the boom punch up through the ceiling. The distance field is negative inside that slab, so the probe contacts it and the camera holds just below the cave roof.</figcaption>
</figure>
<p>Walking the probe along the field is a technique with a name and a paper behind it. John Hart's <em>Sphere Tracing</em>, from 1996, is the standard way to march a ray against a distance field, and the trick is that the field doesn't just tell you whether you've hit something, it tells you a safe distance you can advance without hitting anything. So instead of creeping along in tiny fixed steps, you sample the field, step forward by the slack it reports, and repeat, taking long strides through open air and short careful ones as you close on a surface.</p>
<p>There's a catch our terrain forces on us, though. A true distance field reports the real Euclidean distance to the nearest surface, and a full stride by that slack is always safe. But where the terrain is still a heightmap rather than carved voxels, the field we can cheaply sample isn't true distance, it's vertical clearance, the gap straight down to the ground. On a slope that number over-reports how far the camera can actually move, because the nearest rock is off to the side, not directly below. The honest distance is smaller by a factor that grows with the gradient, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.628ex;" xmlns="http://www.w3.org/2000/svg" width="12.643ex" height="2.851ex" role="img" focusable="false" viewBox="0 -982.5 5588 1260" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msqrt"><g transform="translate(1020,0)"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1722.4,0)"><path data-c="2016" d="M133 736Q138 750 153 750Q164 750 170 739Q172 735 172 250T170 -239Q164 -250 152 -250Q144 -250 138 -244L137 -243Q133 -241 133 -179T132 250Q132 731 133 736ZM329 739Q334 750 346 750Q353 750 361 744L362 743Q366 741 366 679T367 250T367 -178T362 -243L361 -244Q355 -250 347 -250Q335 -250 329 -239Q327 -235 327 250T329 739Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2222.4,0)"><path data-c="2207" d="M46 676Q46 679 51 683H781Q786 679 786 676Q786 674 617 326T444 -26Q439 -33 416 -33T388 -26Q385 -22 216 326T46 676ZM697 596Q697 597 445 597T193 596Q195 591 319 336T445 80L697 596Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3055.4,0)"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(3631.4,0)"><g data-mml-node="mo"><path data-c="2016" d="M133 736Q138 750 153 750Q164 750 170 739Q172 735 172 250T170 -239Q164 -250 152 -250Q144 -250 138 -244L137 -243Q133 -241 133 -179T132 250Q132 731 133 736ZM329 739Q334 750 346 750Q353 750 361 744L362 743Q366 741 366 679T367 250T367 -178T362 -243L361 -244Q355 -250 347 -250Q335 -250 329 -239Q327 -235 327 250T329 739Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(533,289) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(0,72.5)"><path data-c="221A" d="M263 249Q264 249 315 130T417 -108T470 -228L725 302Q981 837 982 839Q989 850 1001 850Q1008 850 1013 844T1020 832V826L741 243Q645 43 540 -176Q479 -303 469 -324T453 -348Q449 -350 436 -350L424 -349L315 -96Q206 156 205 156L171 130Q138 104 137 104L111 130L263 249Z" style="stroke-width: 3;"/></g><rect width="4568" height="60" x="1020" y="862.5"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msqrt><mn>1</mn><mo>+</mo><mo data-mjx-texclass="OPEN" fence="false" stretchy="false">‖</mo><mi mathvariant="normal">∇</mi><mi>h</mi><msup><mo data-mjx-texclass="CLOSE" fence="false" stretchy="false">‖</mo><mn>2</mn></msup></msqrt></math></mjx-assistive-mml></mjx-container>, so a stride sized for the reported slack overshoots and can hop clean over a ridge. The fix is under-relaxation, advancing by only a fraction of the reported slack rather than all of it, which stays safe up to roughly a sixty-degree slope and costs nothing on the true-distance voxel regions beyond a few extra samples. We pair that with a stride floor, so a cave wall a cell or two thick is never stepped clean over, and a tighter ceiling that keeps the march cheap. When the sphere finally touches the surface a short bisection tightens the contact point, and the camera backs off by its own radius and rests there.</p>
<p>Written out, the boom is a ray <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="13.518ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 5975 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D42B" d="M405 293T374 293T324 312T305 361Q305 378 312 394Q315 397 315 399Q305 399 294 394T266 375T238 329T222 249Q221 241 221 149V62H308V0H298Q280 3 161 3Q47 3 38 0H29V62H98V210V303Q98 353 96 363T83 376Q69 380 42 380H29V442H32L118 446Q204 450 205 450H210V414L211 378Q247 449 315 449H321Q384 449 413 422T442 360Q442 332 424 313Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(474,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(863,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1224,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1890.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(2946.6,0)"><g data-mml-node="mi"><path data-c="1D429" d="M32 442L123 446Q214 450 215 450H221V409Q222 409 229 413T251 423T284 436T328 446T382 450Q480 450 540 388T600 223Q600 128 539 61T361 -6H354Q292 -6 236 28L227 34V-132H296V-194H287Q269 -191 163 -191Q56 -191 38 -194H29V-132H98V113V284Q98 330 97 348T93 370T83 376Q69 380 42 380H29V442H32ZM457 224Q457 303 427 349T350 395Q282 395 235 352L227 345V104L233 97Q274 45 337 45Q383 45 420 86T457 224Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3807.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4808,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(5169,0)"><g data-mml-node="mspace"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(5336,0)"><g data-mml-node="mi"><path data-c="1D41D" d="M351 686L442 690Q533 694 534 694H540V389Q540 327 540 253T539 163Q539 97 541 83T555 66Q569 62 596 62H609V31Q609 0 608 0Q588 0 510 -3T412 -6Q411 -6 411 16V38L401 31Q337 -6 265 -6Q159 -6 99 58T38 224Q38 265 51 303T92 375T165 429T272 449Q359 449 417 412V507V555Q417 597 415 607T402 620Q388 624 361 624H348V686H351ZM411 350Q362 399 291 399Q278 399 256 392T218 371Q195 351 189 320T182 238V221Q182 179 183 159T191 115T212 74Q241 46 288 46Q358 46 404 100L411 109V350Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">r</mi></mrow><mo stretchy="false">(</mo><mi>t</mi><mo stretchy="false">)</mo><mo>=</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">p</mi></mrow><mo>+</mo><mi>t</mi><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">d</mi></mrow></math></mjx-assistive-mml></mjx-container> from the pivot <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.439ex;" xmlns="http://www.w3.org/2000/svg" width="1.446ex" height="1.457ex" role="img" focusable="false" viewBox="0 -450 639 644" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D429" d="M32 442L123 446Q214 450 215 450H221V409Q222 409 229 413T251 423T284 436T328 446T382 450Q480 450 540 388T600 223Q600 128 539 61T361 -6H354Q292 -6 236 28L227 34V-132H296V-194H287Q269 -191 163 -191Q56 -191 38 -194H29V-132H98V113V284Q98 330 97 348T93 370T83 376Q69 380 42 380H29V442H32ZM457 224Q457 303 427 349T350 395Q282 395 235 352L227 345V104L233 97Q274 45 337 45Q383 45 420 86T457 224Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">p</mi></mrow></math></mjx-assistive-mml></mjx-container> along the unit direction <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.014ex;" xmlns="http://www.w3.org/2000/svg" width="1.446ex" height="1.584ex" role="img" focusable="false" viewBox="0 -694 639 700" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D41D" d="M351 686L442 690Q533 694 534 694H540V389Q540 327 540 253T539 163Q539 97 541 83T555 66Q569 62 596 62H609V31Q609 0 608 0Q588 0 510 -3T412 -6Q411 -6 411 16V38L401 31Q337 -6 265 -6Q159 -6 99 58T38 224Q38 265 51 303T92 375T165 429T272 449Q359 449 417 412V507V555Q417 597 415 607T402 620Q388 624 361 624H348V686H351ZM411 350Q362 399 291 399Q278 399 256 392T218 371Q195 351 189 320T182 238V221Q182 179 183 159T191 115T212 74Q241 46 288 46Q358 46 404 100L411 109V350Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">d</mi></mrow></math></mjx-assistive-mml></mjx-container>, and the march advances by an under-relaxed fraction of the field's slack to contact, clamped at both ends:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.791ex;" xmlns="http://www.w3.org/2000/svg" width="46.581ex" height="2.713ex" role="img" focusable="false" viewBox="0 -849.5 20588.6 1199" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(394,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(2049.7,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3105.5,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(394,-150) scale(0.707)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(4196,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5196.2,0)"><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(444,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(722,0)" style="stroke-width: 3;"/><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1222,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(2055,0)" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(7807.2,0)"><g data-mml-node="mspace"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(7806.9,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M152 251Q152 646 388 850H416Q422 844 422 841Q422 837 403 816T357 753T302 649T255 482T236 250Q236 124 255 19T301 -147T356 -251T403 -315T422 -340Q422 -343 416 -349H388Q359 -325 332 -296T271 -213T212 -97T170 56T152 251Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(8264.9,0)"><path data-c="1D706" d="M166 673Q166 685 183 694H202Q292 691 316 644Q322 629 373 486T474 207T524 67Q531 47 537 34T546 15T551 6T555 2T556 -2T550 -11H482Q457 3 450 18T399 152L354 277L340 262Q327 246 293 207T236 141Q211 112 174 69Q123 9 111 -1T83 -12Q47 -12 47 20Q47 37 61 52T199 187Q229 216 266 252T321 306L338 322Q338 323 288 462T234 612Q214 657 183 657Q166 657 166 673Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(8847.9,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mo" transform="translate(9014.9,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(9403.9,0)"><path data-c="3A6" d="M312 622Q310 623 307 625T303 629T297 631T286 634T270 635T246 636T211 637H184V683H196Q220 680 361 680T526 683H538V637H511Q468 637 447 635T422 631T411 622V533L425 531Q525 519 595 466T665 342Q665 301 642 267T583 209T506 172T425 152L411 150V61Q417 55 421 53T447 48T511 46H538V0H526Q502 3 361 3T196 0H184V46H211Q231 46 245 46T270 47T286 48T297 51T303 54T307 57T312 61V150H310Q309 151 289 153T232 166T160 195Q149 201 136 210T103 238T69 284T56 342Q56 414 128 467T294 530Q309 532 310 533H312V622ZM170 342Q170 207 307 188H312V495H309Q301 495 282 491T231 469T186 423Q170 389 170 342ZM415 188Q487 199 519 236T551 342Q551 384 539 414T507 459T470 481T434 491T415 495H410V188H415Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(10125.9,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(10514.9,0)"><g data-mml-node="mi"><path data-c="1D42B" d="M405 293T374 293T324 312T305 361Q305 378 312 394Q315 397 315 399Q305 399 294 394T266 375T238 329T222 249Q221 241 221 149V62H308V0H298Q280 3 161 3Q47 3 38 0H29V62H98V210V303Q98 353 96 363T83 376Q69 380 42 380H29V442H32L118 446Q204 450 205 450H210V414L211 378Q247 449 315 449H321Q384 449 413 422T442 360Q442 332 424 313Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(10988.9,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(11377.9,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(394,-150) scale(0.707)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(12246.1,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(12635.1,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(13246.4,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(14246.6,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(14697.6,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(15086.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(15364.6,0)"><g data-mml-node="mspace"/></g><g data-mml-node="msub" transform="translate(15809.3,0)"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(502,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mo"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1111,0)" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(17540,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(17818,0)"><g data-mml-node="mspace"/></g><g data-mml-node="msub" transform="translate(18262.7,0)"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(502,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mo"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="78" d="M201 0Q189 3 102 3Q26 3 17 0H11V46H25Q48 47 67 52T96 61T121 78T139 96T160 122T180 150L226 210L168 288Q159 301 149 315T133 336T122 351T113 363T107 370T100 376T94 379T88 381T80 383Q74 383 44 385H16V431H23Q59 429 126 429Q219 429 229 431H237V385Q201 381 201 369Q201 367 211 353T239 315T268 274L272 270L297 304Q329 345 329 358Q329 364 327 369T322 376T317 380T310 384L307 385H302V431H309Q324 428 408 428Q487 428 493 431H499V385H492Q443 385 411 368Q394 360 377 341T312 257L296 236L358 151Q424 61 429 57T446 50Q464 46 499 46H516V0H510H502Q494 1 482 1T457 2T432 2T414 3Q403 3 377 3T327 1L304 0H295V46H298Q309 46 320 51T331 63Q331 65 291 120L250 175Q249 174 219 133T185 88Q181 83 181 74Q181 63 188 55T206 46Q208 46 208 23V0H201Z" transform="translate(1333,0)" style="stroke-width: 3;"/></g></g></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(20130.6,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M305 251Q305 -145 69 -349H56Q43 -349 39 -347T35 -338Q37 -333 60 -307T108 -239T160 -136T204 27T221 250T204 473T160 636T108 740T60 807T35 839Q35 850 50 850H56H69Q197 743 256 566Q305 425 305 251Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><msub><mi>t</mi><mrow data-mjx-texclass="ORD"><mi>n</mi><mo>+</mo><mn>1</mn></mrow></msub><mo>=</mo><msub><mi>t</mi><mi>n</mi></msub><mo>+</mo><mi>clamp</mi><mstyle scriptlevel="0"><mspace width="-0.167em"/></mstyle><mrow data-mjx-texclass="ORD"><mo minsize="1.2em" maxsize="1.2em">(</mo></mrow><mi>λ</mi><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mo stretchy="false">(</mo><mi mathvariant="normal">Φ</mi><mo stretchy="false">(</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">r</mi></mrow><mo stretchy="false">(</mo><msub><mi>t</mi><mi>n</mi></msub><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mo>−</mo><mi>r</mi><mo stretchy="false">)</mo><mo>,</mo><mstyle scriptlevel="0"><mspace width="0.278em"/></mstyle><msub><mi>s</mi><mrow data-mjx-texclass="ORD"><mo data-mjx-texclass="OP" movablelimits="true">min</mo></mrow></msub><mo>,</mo><mstyle scriptlevel="0"><mspace width="0.278em"/></mstyle><msub><mi>s</mi><mrow data-mjx-texclass="ORD"><mo data-mjx-texclass="OP" movablelimits="true">max</mo></mrow></msub><mrow data-mjx-texclass="ORD"><mo minsize="1.2em" maxsize="1.2em">)</mo></mrow></math></mjx-assistive-mml></mjx-container><p>Here <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.633ex" height="1.545ex" role="img" focusable="false" viewBox="0 -683 722 683" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="3A6" d="M312 622Q310 623 307 625T303 629T297 631T286 634T270 635T246 636T211 637H184V683H196Q220 680 361 680T526 683H538V637H511Q468 637 447 635T422 631T411 622V533L425 531Q525 519 595 466T665 342Q665 301 642 267T583 209T506 172T425 152L411 150V61Q417 55 421 53T447 48T511 46H538V0H526Q502 3 361 3T196 0H184V46H211Q231 46 245 46T270 47T286 48T297 51T303 54T307 57T312 61V150H310Q309 151 289 153T232 166T160 195Q149 201 136 210T103 238T69 284T56 342Q56 414 128 467T294 530Q309 532 310 533H312V622ZM170 342Q170 207 307 188H312V495H309Q301 495 282 491T231 469T186 423Q170 389 170 342ZM415 188Q487 199 519 236T551 342Q551 384 539 414T507 459T470 481T434 491T415 495H410V188H415Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi mathvariant="normal">Φ</mi></math></mjx-assistive-mml></mjx-container> is the signed distance, positive in air and negative in rock, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.02ex" height="1.025ex" role="img" focusable="false" viewBox="0 -442 451 453" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>r</mi></math></mjx-assistive-mml></mjx-container> is the probe radius, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="8.862ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 3917.2 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D706" d="M166 673Q166 685 183 694H202Q292 691 316 644Q322 629 373 486T474 207T524 67Q531 47 537 34T546 15T551 6T555 2T556 -2T550 -11H482Q457 3 450 18T399 152L354 277L340 262Q327 246 293 207T236 141Q211 112 174 69Q123 9 111 -1T83 -12Q47 -12 47 20Q47 37 61 52T199 187Q229 216 266 252T321 306L338 322Q338 323 288 462T234 612Q214 657 183 657Q166 657 166 673Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(860.8,0)"><path data-c="2208" d="M84 250Q84 372 166 450T360 539Q361 539 377 539T419 540T469 540H568Q583 532 583 520Q583 511 570 501L466 500Q355 499 329 494Q280 482 242 458T183 409T147 354T129 306T124 272V270H568Q583 262 583 250T568 230H124V228Q124 207 134 177T167 112T231 48T328 7Q355 1 466 0H570Q583 -10 583 -20Q583 -32 568 -40H471Q464 -40 446 -40T417 -41Q262 -41 172 45Q84 127 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1805.6,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2194.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2694.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3139.2,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3639.2,0)"><path data-c="5D" d="M22 710V750H159V-250H22V-210H119V710H22Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>λ</mi><mo>∈</mo><mo stretchy="false">(</mo><mn>0</mn><mo>,</mo><mn>1</mn><mo stretchy="false">]</mo></math></mjx-assistive-mml></mjx-container> is the under-relaxation factor that keeps the over-reporting heightmap field from hopping a slope, and <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.439ex;" xmlns="http://www.w3.org/2000/svg" width="9.148ex" height="1.439ex" role="img" focusable="false" viewBox="0 -442 4043.3 636" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(502,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mo"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1111,0)" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(1730.7,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2175.4,0)"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(502,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mo"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="78" d="M201 0Q189 3 102 3Q26 3 17 0H11V46H25Q48 47 67 52T96 61T121 78T139 96T160 122T180 150L226 210L168 288Q159 301 149 315T133 336T122 351T113 363T107 370T100 376T94 379T88 381T80 383Q74 383 44 385H16V431H23Q59 429 126 429Q219 429 229 431H237V385Q201 381 201 369Q201 367 211 353T239 315T268 274L272 270L297 304Q329 345 329 358Q329 364 327 369T322 376T317 380T310 384L307 385H302V431H309Q324 428 408 428Q487 428 493 431H499V385H492Q443 385 411 368Q394 360 377 341T312 257L296 236L358 151Q424 61 429 57T446 50Q464 46 499 46H516V0H510H502Q494 1 482 1T457 2T432 2T414 3Q403 3 377 3T327 1L304 0H295V46H298Q309 46 320 51T331 63Q331 65 291 120L250 175Q249 174 219 133T185 88Q181 83 181 74Q181 63 188 55T206 46Q208 46 208 23V0H201Z" transform="translate(1333,0)" style="stroke-width: 3;"/></g></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>s</mi><mrow data-mjx-texclass="ORD"><mo data-mjx-texclass="OP" movablelimits="true">min</mo></mrow></msub><mo>,</mo><msub><mi>s</mi><mrow data-mjx-texclass="ORD"><mo data-mjx-texclass="OP" movablelimits="true">max</mo></mrow></msub></math></mjx-assistive-mml></mjx-container> are the step clamps that keep a thin wall from being skipped and the march from running long. Contact is the first <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="0.817ex" height="1.441ex" role="img" focusable="false" viewBox="0 -626 361 637" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>t</mi></math></mjx-assistive-mml></mjx-container> where <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="11.08ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 4897.6 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="3A6" d="M312 622Q310 623 307 625T303 629T297 631T286 634T270 635T246 636T211 637H184V683H196Q220 680 361 680T526 683H538V637H511Q468 637 447 635T422 631T411 622V533L425 531Q525 519 595 466T665 342Q665 301 642 267T583 209T506 172T425 152L411 150V61Q417 55 421 53T447 48T511 46H538V0H526Q502 3 361 3T196 0H184V46H211Q231 46 245 46T270 47T286 48T297 51T303 54T307 57T312 61V150H310Q309 151 289 153T232 166T160 195Q149 201 136 210T103 238T69 284T56 342Q56 414 128 467T294 530Q309 532 310 533H312V622ZM170 342Q170 207 307 188H312V495H309Q301 495 282 491T231 469T186 423Q170 389 170 342ZM415 188Q487 199 519 236T551 342Q551 384 539 414T507 459T470 481T434 491T415 495H410V188H415Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(1111,0)"><g data-mml-node="mi"><path data-c="1D42B" d="M405 293T374 293T324 312T305 361Q305 378 312 394Q315 397 315 399Q305 399 294 394T266 375T238 329T222 249Q221 241 221 149V62H308V0H298Q280 3 161 3Q47 3 38 0H29V62H98V210V303Q98 353 96 363T83 376Q69 380 42 380H29V442H32L118 446Q204 450 205 450H210V414L211 378Q247 449 315 449H321Q384 449 413 422T442 360Q442 332 424 313Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1585,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1974,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2335,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2724,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3390.8,0)"><path data-c="2264" d="M674 636Q682 636 688 630T694 615T687 601Q686 600 417 472L151 346L399 228Q687 92 691 87Q694 81 694 76Q694 58 676 56H670L382 192Q92 329 90 331Q83 336 83 348Q84 359 96 365Q104 369 382 500T665 634Q669 636 674 636ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4446.6,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi mathvariant="normal">Φ</mi><mo stretchy="false">(</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">r</mi></mrow><mo stretchy="false">(</mo><mi>t</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mo>≤</mo><mi>r</mi></math></mjx-assistive-mml></mjx-container>, meaning the sphere surface has reached the rock, and the camera holds at that arc length less its radius.</p>
<p>The collider carries one more method as a safety net, depenetration. Collision should keep the camera out of solid in the first place, but a few situations sneak past it, a pivot straddling a wall thin enough that the boom starts inside it, a cave freshly carved by the terrain tools while the camera sat in the rock that's now gone, a building piece dropped around the camera. For those the collider checks whether the camera ended the frame inside solid, where <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="8.587ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 3795.6 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="3A6" d="M312 622Q310 623 307 625T303 629T297 631T286 634T270 635T246 636T211 637H184V683H196Q220 680 361 680T526 683H538V637H511Q468 637 447 635T422 631T411 622V533L425 531Q525 519 595 466T665 342Q665 301 642 267T583 209T506 172T425 152L411 150V61Q417 55 421 53T447 48T511 46H538V0H526Q502 3 361 3T196 0H184V46H211Q231 46 245 46T270 47T286 48T297 51T303 54T307 57T312 61V150H310Q309 151 289 153T232 166T160 195Q149 201 136 210T103 238T69 284T56 342Q56 414 128 467T294 530Q309 532 310 533H312V622ZM170 342Q170 207 307 188H312V495H309Q301 495 282 491T231 469T186 423Q170 389 170 342ZM415 188Q487 199 519 236T551 342Q551 384 539 414T507 459T470 481T434 491T415 495H410V188H415Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(1111,0)"><g data-mml-node="mi"><path data-c="1D41C" d="M447 131H458Q478 131 478 117Q478 112 471 95T439 51T377 9Q330 -6 286 -6Q196 -6 135 35Q39 96 39 222Q39 324 101 384Q169 453 286 453Q359 453 411 431T464 353Q464 319 445 302T395 284Q360 284 343 305T325 353Q325 380 338 396H333Q317 398 295 398H292Q280 398 271 397T245 390T218 373T197 338T183 283Q182 275 182 231Q182 199 184 180T193 132T220 85T270 57Q289 50 317 50H326Q385 50 414 115Q419 127 423 129T447 131Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1622,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2288.8,0)"><path data-c="3C" d="M694 -11T694 -19T688 -33T678 -40Q671 -40 524 29T234 166L90 235Q83 240 83 250Q83 261 91 266Q664 540 678 540Q681 540 687 534T694 519T687 505Q686 504 417 376L151 250L417 124Q686 -4 687 -5Q694 -11 694 -19Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3344.6,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi mathvariant="normal">Φ</mi><mo stretchy="false">(</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">c</mi></mrow><mo stretchy="false">)</mo><mo>&lt;</mo><mi>r</mi></math></mjx-assistive-mml></mjx-container>, and if it did, reads the field's gradient, which points straight toward open air because <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.633ex" height="1.545ex" role="img" focusable="false" viewBox="0 -683 722 683" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="3A6" d="M312 622Q310 623 307 625T303 629T297 631T286 634T270 635T246 636T211 637H184V683H196Q220 680 361 680T526 683H538V637H511Q468 637 447 635T422 631T411 622V533L425 531Q525 519 595 466T665 342Q665 301 642 267T583 209T506 172T425 152L411 150V61Q417 55 421 53T447 48T511 46H538V0H526Q502 3 361 3T196 0H184V46H211Q231 46 245 46T270 47T286 48T297 51T303 54T307 57T312 61V150H310Q309 151 289 153T232 166T160 195Q149 201 136 210T103 238T69 284T56 342Q56 414 128 467T294 530Q309 532 310 533H312V622ZM170 342Q170 207 307 188H312V495H309Q301 495 282 491T231 469T186 423Q170 389 170 342ZM415 188Q487 199 519 236T551 342Q551 384 539 414T507 459T470 481T434 491T415 495H410V188H415Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi mathvariant="normal">Φ</mi></math></mjx-assistive-mml></mjx-container> grows as you leave the rock, and steps the camera out along it:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -2.172ex;" xmlns="http://www.w3.org/2000/svg" width="29.076ex" height="5.475ex" role="img" focusable="false" viewBox="0 -1460 12851.4 2420" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D41C" d="M447 131H458Q478 131 478 117Q478 112 471 95T439 51T377 9Q330 -6 286 -6Q196 -6 135 35Q39 96 39 222Q39 324 101 384Q169 453 286 453Q359 453 411 431T464 353Q464 319 445 302T395 284Q360 284 343 305T325 353Q325 380 338 396H333Q317 398 295 398H292Q280 398 271 397T245 390T218 373T197 338T183 283Q182 275 182 231Q182 199 184 180T193 132T220 85T270 57Q289 50 317 50H326Q385 50 414 115Q419 127 423 129T447 131Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(788.8,0)"><path data-c="2190" d="M944 261T944 250T929 230H165Q167 228 182 216T211 189T244 152T277 96T303 25Q308 7 308 0Q308 -11 288 -11Q281 -11 278 -11T272 -7T267 2T263 21Q245 94 195 151T73 236Q58 242 55 247Q55 254 59 257T73 264Q121 283 158 314T215 375T247 434T264 480L267 497Q269 503 270 505T275 509T288 511Q308 511 308 500Q308 493 303 475Q293 438 278 406T246 352T215 315T185 287T165 270H929Q944 261 944 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(2066.6,0)"><g data-mml-node="mi"><path data-c="1D41C" d="M447 131H458Q478 131 478 117Q478 112 471 95T439 51T377 9Q330 -6 286 -6Q196 -6 135 35Q39 96 39 222Q39 324 101 384Q169 453 286 453Q359 453 411 431T464 353Q464 319 445 302T395 284Q360 284 343 305T325 353Q325 380 338 396H333Q317 398 295 398H292Q280 398 271 397T245 390T218 373T197 338T183 283Q182 275 182 231Q182 199 184 180T193 132T220 85T270 57Q289 50 317 50H326Q385 50 414 115Q419 127 423 129T447 131Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2799.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(3800,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M152 251Q152 646 388 850H416Q422 844 422 841Q422 837 403 816T357 753T302 649T255 482T236 250Q236 124 255 19T301 -147T356 -251T403 -315T422 -340Q422 -343 416 -349H388Q359 -325 332 -296T271 -213T212 -97T170 56T152 251Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(4258,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4931.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5931.4,0)"><path data-c="3A6" d="M312 622Q310 623 307 625T303 629T297 631T286 634T270 635T246 636T211 637H184V683H196Q220 680 361 680T526 683H538V637H511Q468 637 447 635T422 631T411 622V533L425 531Q525 519 595 466T665 342Q665 301 642 267T583 209T506 172T425 152L411 150V61Q417 55 421 53T447 48T511 46H538V0H526Q502 3 361 3T196 0H184V46H211Q231 46 245 46T270 47T286 48T297 51T303 54T307 57T312 61V150H310Q309 151 289 153T232 166T160 195Q149 201 136 210T103 238T69 284T56 342Q56 414 128 467T294 530Q309 532 310 533H312V622ZM170 342Q170 207 307 188H312V495H309Q301 495 282 491T231 469T186 423Q170 389 170 342ZM415 188Q487 199 519 236T551 342Q551 384 539 414T507 459T470 481T434 491T415 495H410V188H415Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6653.4,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(7042.4,0)"><g data-mml-node="mi"><path data-c="1D41C" d="M447 131H458Q478 131 478 117Q478 112 471 95T439 51T377 9Q330 -6 286 -6Q196 -6 135 35Q39 96 39 222Q39 324 101 384Q169 453 286 453Q359 453 411 431T464 353Q464 319 445 302T395 284Q360 284 343 305T325 353Q325 380 338 396H333Q317 398 295 398H292Q280 398 271 397T245 390T218 373T197 338T183 283Q182 275 182 231Q182 199 184 180T193 132T220 85T270 57Q289 50 317 50H326Q385 50 414 115Q419 127 423 129T447 131Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(7553.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(7942.4,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M305 251Q305 -145 69 -349H56Q43 -349 39 -347T35 -338Q37 -333 60 -307T108 -239T160 -136T204 27T221 250T204 473T160 636T108 740T60 807T35 839Q35 850 50 850H56H69Q197 743 256 566Q305 425 305 251Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mstyle" transform="translate(8400.4,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mfrac" transform="translate(8567.4,0)"><g data-mml-node="mrow" transform="translate(720,710)"><g data-mml-node="mi"><path data-c="2207" d="M46 676Q46 679 51 683H781Q786 679 786 676Q786 674 617 326T444 -26Q439 -33 416 -33T388 -26Q385 -22 216 326T46 676ZM697 596Q697 597 445 597T193 596Q195 591 319 336T445 80L697 596Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(833,0)"><path data-c="3A6" d="M312 622Q310 623 307 625T303 629T297 631T286 634T270 635T246 636T211 637H184V683H196Q220 680 361 680T526 683H538V637H511Q468 637 447 635T422 631T411 622V533L425 531Q525 519 595 466T665 342Q665 301 642 267T583 209T506 172T425 152L411 150V61Q417 55 421 53T447 48T511 46H538V0H526Q502 3 361 3T196 0H184V46H211Q231 46 245 46T270 47T286 48T297 51T303 54T307 57T312 61V150H310Q309 151 289 153T232 166T160 195Q149 201 136 210T103 238T69 284T56 342Q56 414 128 467T294 530Q309 532 310 533H312V622ZM170 342Q170 207 307 188H312V495H309Q301 495 282 491T231 469T186 423Q170 389 170 342ZM415 188Q487 199 519 236T551 342Q551 384 539 414T507 459T470 481T434 491T415 495H410V188H415Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1555,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(1944,0)"><g data-mml-node="mi"><path data-c="1D41C" d="M447 131H458Q478 131 478 117Q478 112 471 95T439 51T377 9Q330 -6 286 -6Q196 -6 135 35Q39 96 39 222Q39 324 101 384Q169 453 286 453Q359 453 411 431T464 353Q464 319 445 302T395 284Q360 284 343 305T325 353Q325 380 338 396H333Q317 398 295 398H292Q280 398 271 397T245 390T218 373T197 338T183 283Q182 275 182 231Q182 199 184 180T193 132T220 85T270 57Q289 50 317 50H326Q385 50 414 115Q419 127 423 129T447 131Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2455,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mrow" transform="translate(220,-710)"><g data-mml-node="mo"><path data-c="2016" d="M133 736Q138 750 153 750Q164 750 170 739Q172 735 172 250T170 -239Q164 -250 152 -250Q144 -250 138 -244L137 -243Q133 -241 133 -179T132 250Q132 731 133 736ZM329 739Q334 750 346 750Q353 750 361 744L362 743Q366 741 366 679T367 250T367 -178T362 -243L361 -244Q355 -250 347 -250Q335 -250 329 -239Q327 -235 327 250T329 739Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(500,0)"><path data-c="2207" d="M46 676Q46 679 51 683H781Q786 679 786 676Q786 674 617 326T444 -26Q439 -33 416 -33T388 -26Q385 -22 216 326T46 676ZM697 596Q697 597 445 597T193 596Q195 591 319 336T445 80L697 596Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1333,0)"><path data-c="3A6" d="M312 622Q310 623 307 625T303 629T297 631T286 634T270 635T246 636T211 637H184V683H196Q220 680 361 680T526 683H538V637H511Q468 637 447 635T422 631T411 622V533L425 531Q525 519 595 466T665 342Q665 301 642 267T583 209T506 172T425 152L411 150V61Q417 55 421 53T447 48T511 46H538V0H526Q502 3 361 3T196 0H184V46H211Q231 46 245 46T270 47T286 48T297 51T303 54T307 57T312 61V150H310Q309 151 289 153T232 166T160 195Q149 201 136 210T103 238T69 284T56 342Q56 414 128 467T294 530Q309 532 310 533H312V622ZM170 342Q170 207 307 188H312V495H309Q301 495 282 491T231 469T186 423Q170 389 170 342ZM415 188Q487 199 519 236T551 342Q551 384 539 414T507 459T470 481T434 491T415 495H410V188H415Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2055,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(2444,0)"><g data-mml-node="mi"><path data-c="1D41C" d="M447 131H458Q478 131 478 117Q478 112 471 95T439 51T377 9Q330 -6 286 -6Q196 -6 135 35Q39 96 39 222Q39 324 101 384Q169 453 286 453Q359 453 411 431T464 353Q464 319 445 302T395 284Q360 284 343 305T325 353Q325 380 338 396H333Q317 398 295 398H292Q280 398 271 397T245 390T218 373T197 338T183 283Q182 275 182 231Q182 199 184 180T193 132T220 85T270 57Q289 50 317 50H326Q385 50 414 115Q419 127 423 129T447 131Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2955,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3344,0)"><path data-c="2016" d="M133 736Q138 750 153 750Q164 750 170 739Q172 735 172 250T170 -239Q164 -250 152 -250Q144 -250 138 -244L137 -243Q133 -241 133 -179T132 250Q132 731 133 736ZM329 739Q334 750 346 750Q353 750 361 744L362 743Q366 741 366 679T367 250T367 -178T362 -243L361 -244Q355 -250 347 -250Q335 -250 329 -239Q327 -235 327 250T329 739Z" style="stroke-width: 3;"/></g></g><rect width="4044" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">c</mi></mrow><mo stretchy="false">←</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">c</mi></mrow><mo>+</mo><mrow data-mjx-texclass="ORD"><mo minsize="1.2em" maxsize="1.2em">(</mo></mrow><mi>r</mi><mo>−</mo><mi mathvariant="normal">Φ</mi><mo stretchy="false">(</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">c</mi></mrow><mo stretchy="false">)</mo><mrow data-mjx-texclass="ORD"><mo minsize="1.2em" maxsize="1.2em">)</mo></mrow><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mfrac><mrow><mi mathvariant="normal">∇</mi><mi mathvariant="normal">Φ</mi><mo stretchy="false">(</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">c</mi></mrow><mo stretchy="false">)</mo></mrow><mrow><mo data-mjx-texclass="OPEN" fence="false" stretchy="false">‖</mo><mi mathvariant="normal">∇</mi><mi mathvariant="normal">Φ</mi><mo stretchy="false">(</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">c</mi></mrow><mo stretchy="false">)</mo><mo data-mjx-texclass="CLOSE" fence="false" stretchy="false">‖</mo></mrow></mfrac></math></mjx-assistive-mml></mjx-container><p>A handful of those iterations converges on the <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.02ex" height="1.025ex" role="img" focusable="false" viewBox="0 -442 451 453" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>r</mi></math></mjx-assistive-mml></mjx-container>-isosurface, and it's the thing that means a sculpting tool can dig the ground out from under the camera and the view recovers on the next frame instead of going black.</p>
<h2 id="snapping-in-easing-out-and-not-flinching-at-fence-posts" tabindex="-1">Snapping in, easing out, and not flinching at fence posts <a class="header-anchor" href="#snapping-in-easing-out-and-not-flinching-at-fence-posts" aria-label="Permalink to &quot;Snapping in, easing out, and not flinching at fence posts&quot;"></a></h2>
<p>A boom that simply jumps to the collision distance every frame is worse than no boom at all, because the world is full of thin things the camera passes behind for a single frame, a fence post, a lamp, a tree trunk, and a camera that lunges to dodge each one and lunges back is nauseating. Haigh-Hutchinson's book and Itay Keren's much-loved talk on camera motion both land on the same intuition, which is that the camera should react to threats and danger faster than it relaxes from them. So the damping is deliberately asymmetric. When an occluder appears and the boom needs to shorten, it snaps in almost instantly, because a frame of clipping is ugly and the player forgives a fast tuck. When the occluder clears and the boom wants to lengthen, it eases out slowly, and only after a short dwell timer of continuous clearance has elapsed. That dwell is the hysteresis that kills the flinch. Whip the camera past a thin post and the post never clears long enough to trigger the slow extension, so the camera glides past it as if it weren't there, which is exactly what your eye wants. Cinemachine exposes the same idea as separate damping-into and damping-out-of collision values, and the asymmetry is the part that makes it feel like a camera operator rather than a spring.</p>
<p>In code it's one line of exponential smoothing with the rate switched on the sign of the change. If <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.045ex;" xmlns="http://www.w3.org/2000/svg" width="0.943ex" height="1.64ex" role="img" focusable="false" viewBox="0 -705 417 725" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="2113" d="M345 104T349 104T361 95T369 80T352 59Q268 -20 206 -20Q170 -20 146 3T113 53T99 104L94 129Q94 130 79 116T48 86T28 70Q22 70 15 79T7 94Q7 98 12 103T58 147L91 179V185Q91 186 91 191T92 200Q92 282 128 400T223 612T336 705Q397 705 397 636V627Q397 453 194 233Q185 223 180 218T174 211T171 208T165 201L163 186Q159 142 159 123Q159 17 208 17Q228 17 253 30T293 56T335 94Q345 104 349 104ZM360 634Q360 655 354 661T336 668Q328 668 322 666T302 645T272 592Q252 547 229 467T192 330L179 273Q179 272 186 280T204 300T221 322Q327 453 355 590Q360 612 360 634Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>ℓ</mi></math></mjx-assistive-mml></mjx-container> is the current boom length, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.023ex;" xmlns="http://www.w3.org/2000/svg" width="1.197ex" height="1.02ex" role="img" focusable="false" viewBox="0 -441 529 451" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>a</mi></math></mjx-assistive-mml></mjx-container> the length collision allows this frame, and <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.701ex" height="1.645ex" role="img" focusable="false" viewBox="0 -716 1194 727" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="394" d="M51 0Q46 4 46 7Q46 9 215 357T388 709Q391 716 416 716Q439 716 444 709Q447 705 616 357T786 7Q786 4 781 0H51ZM507 344L384 596L137 92L383 91H630Q630 93 507 344Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(833,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi mathvariant="normal">Δ</mi><mi>t</mi></math></mjx-assistive-mml></mjx-container> the frame time, then</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -2.149ex;" xmlns="http://www.w3.org/2000/svg" width="62.104ex" height="5.43ex" role="img" focusable="false" viewBox="0 -1450 27450 2400" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="2113" d="M345 104T349 104T361 95T369 80T352 59Q268 -20 206 -20Q170 -20 146 3T113 53T99 104L94 129Q94 130 79 116T48 86T28 70Q22 70 15 79T7 94Q7 98 12 103T58 147L91 179V185Q91 186 91 191T92 200Q92 282 128 400T223 612T336 705Q397 705 397 636V627Q397 453 194 233Q185 223 180 218T174 211T171 208T165 201L163 186Q159 142 159 123Q159 17 208 17Q228 17 253 30T293 56T335 94Q345 104 349 104ZM360 634Q360 655 354 661T336 668Q328 668 322 666T302 645T272 592Q252 547 229 467T192 330L179 273Q179 272 186 280T204 300T221 322Q327 453 355 590Q360 612 360 634Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(694.8,0)"><path data-c="2190" d="M944 261T944 250T929 230H165Q167 228 182 216T211 189T244 152T277 96T303 25Q308 7 308 0Q308 -11 288 -11Q281 -11 278 -11T272 -7T267 2T263 21Q245 94 195 151T73 236Q58 242 55 247Q55 254 59 257T73 264Q121 283 158 314T215 375T247 434T264 480L267 497Q269 503 270 505T275 509T288 511Q308 511 308 500Q308 493 303 475Q293 438 278 406T246 352T215 315T185 287T165 270H929Q944 261 944 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1972.6,0)"><path data-c="2113" d="M345 104T349 104T361 95T369 80T352 59Q268 -20 206 -20Q170 -20 146 3T113 53T99 104L94 129Q94 130 79 116T48 86T28 70Q22 70 15 79T7 94Q7 98 12 103T58 147L91 179V185Q91 186 91 191T92 200Q92 282 128 400T223 612T336 705Q397 705 397 636V627Q397 453 194 233Q185 223 180 218T174 211T171 208T165 201L163 186Q159 142 159 123Q159 17 208 17Q228 17 253 30T293 56T335 94Q345 104 349 104ZM360 634Q360 655 354 661T336 668Q328 668 322 666T302 645T272 592Q252 547 229 467T192 330L179 273Q179 272 186 280T204 300T221 322Q327 453 355 590Q360 612 360 634Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2611.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3612,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4001,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4752.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5752.4,0)"><path data-c="2113" d="M345 104T349 104T361 95T369 80T352 59Q268 -20 206 -20Q170 -20 146 3T113 53T99 104L94 129Q94 130 79 116T48 86T28 70Q22 70 15 79T7 94Q7 98 12 103T58 147L91 179V185Q91 186 91 191T92 200Q92 282 128 400T223 612T336 705Q397 705 397 636V627Q397 453 194 233Q185 223 180 218T174 211T171 208T165 201L163 186Q159 142 159 123Q159 17 208 17Q228 17 253 30T293 56T335 94Q345 104 349 104ZM360 634Q360 655 354 661T336 668Q328 668 322 666T302 645T272 592Q252 547 229 467T192 330L179 273Q179 272 186 280T204 300T221 322Q327 453 355 590Q360 612 360 634Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6169.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(6558.4,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M152 251Q152 646 388 850H416Q422 844 422 841Q422 837 403 816T357 753T302 649T255 482T236 250Q236 124 255 19T301 -147T356 -251T403 -315T422 -340Q422 -343 416 -349H388Q359 -325 332 -296T271 -213T212 -97T170 56T152 251Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mn" transform="translate(7016.4,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(7738.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(8738.9,0)"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(499,413) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mo"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(778,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(1299,0) scale(1.414)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(1535.2,0)"><path data-c="394" d="M51 0Q46 4 46 7Q46 9 215 357T388 709Q391 716 416 716Q439 716 444 709Q447 705 616 357T786 7Q786 4 781 0H51ZM507 344L384 596L137 92L383 91H630Q630 93 507 344Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2368.2,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(11217.7,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M305 251Q305 -145 69 -349H56Q43 -349 39 -347T35 -338Q37 -333 60 -307T108 -239T160 -136T204 27T221 250T204 473T160 636T108 740T60 807T35 839Q35 850 50 850H56H69Q197 743 256 566Q305 425 305 251Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(11675.7,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(11953.7,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(14120.4,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(14919.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mrow" transform="translate(15974.9,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="7B" d="M618 -943L612 -949H582L568 -943Q472 -903 411 -841T332 -703Q327 -682 327 -653T325 -350Q324 -28 323 -18Q317 24 301 61T264 124T221 171T179 205T147 225T132 234Q130 238 130 250Q130 255 130 258T131 264T132 267T134 269T139 272T144 275Q207 308 256 367Q310 436 323 519Q324 529 325 851Q326 1124 326 1154T332 1205Q369 1358 566 1443L582 1450H612L618 1444V1429Q618 1413 616 1411L608 1406Q599 1402 585 1393T552 1372T515 1343T479 1305T449 1257T429 1200Q425 1180 425 1152T423 851Q422 579 422 549T416 498Q407 459 388 424T346 364T297 318T250 284T214 264T197 254L188 251L205 242Q290 200 345 138T416 3Q421 -18 421 -48T423 -349Q423 -397 423 -472Q424 -677 428 -694Q429 -697 429 -699Q434 -722 443 -743T465 -782T491 -816T519 -845T548 -868T574 -886T595 -899T610 -908L616 -910Q618 -912 618 -928V-943Z" style="stroke-width: 3;"/></g><g data-mml-node="mtable" transform="translate(750,0)"><g data-mml-node="mtr" transform="translate(0,700)"><g data-mml-node="mtd"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(554,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mtext"><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(278,0)" style="stroke-width: 3;"/></g></g></g></g><g data-mml-node="mtd" transform="translate(2625.8,0)"><g data-mml-node="mi"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(806.8,0)"><path data-c="2264" d="M674 636Q682 636 688 630T694 615T687 601Q686 600 417 472L151 346L399 228Q687 92 691 87Q694 81 694 76Q694 58 676 56H670L382 192Q92 329 90 331Q83 336 83 348Q84 359 96 365Q104 369 382 500T665 634Q669 636 674 636ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1862.6,0)"><path data-c="2113" d="M345 104T349 104T361 95T369 80T352 59Q268 -20 206 -20Q170 -20 146 3T113 53T99 104L94 129Q94 130 79 116T48 86T28 70Q22 70 15 79T7 94Q7 98 12 103T58 147L91 179V185Q91 186 91 191T92 200Q92 282 128 400T223 612T336 705Q397 705 397 636V627Q397 453 194 233Q185 223 180 218T174 211T171 208T165 201L163 186Q159 142 159 123Q159 17 208 17Q228 17 253 30T293 56T335 94Q345 104 349 104ZM360 634Q360 655 354 661T336 668Q328 668 322 666T302 645T272 592Q252 547 229 467T192 330L179 273Q179 272 186 280T204 300T221 322Q327 453 355 590Q360 612 360 634Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mtr" transform="translate(0,-700)"><g data-mml-node="mtd"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(554,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mtext"><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" style="stroke-width: 3;"/><path data-c="75" d="M383 58Q327 -10 256 -10H249Q124 -10 105 89Q104 96 103 226Q102 335 102 348T96 369Q86 385 36 385H25V408Q25 431 27 431L38 432Q48 433 67 434T105 436Q122 437 142 438T172 441T184 442H187V261Q188 77 190 64Q193 49 204 40Q224 26 264 26Q290 26 311 35T343 58T363 90T375 120T379 144Q379 145 379 161T380 201T380 248V315Q380 361 370 372T320 385H302V431Q304 431 378 436T457 442H464V264Q464 84 465 81Q468 61 479 55T524 46H542V0Q540 0 467 -5T390 -11H383V58Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(1056,0)" style="stroke-width: 3;"/></g></g></g></g><g data-mml-node="mtd" transform="translate(2625.8,0)"><g data-mml-node="mi"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(806.8,0)"><path data-c="3E" d="M84 520Q84 528 88 533T96 539L99 540Q106 540 253 471T544 334L687 265Q694 260 694 250T687 235Q685 233 395 96L107 -40H101Q83 -38 83 -20Q83 -19 83 -17Q82 -10 98 -1Q117 9 248 71Q326 108 378 132L626 250L378 368Q90 504 86 509Q84 513 84 520Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1862.6,0)"><path data-c="2113" d="M345 104T349 104T361 95T369 80T352 59Q268 -20 206 -20Q170 -20 146 3T113 53T99 104L94 129Q94 130 79 116T48 86T28 70Q22 70 15 79T7 94Q7 98 12 103T58 147L91 179V185Q91 186 91 191T92 200Q92 282 128 400T223 612T336 705Q397 705 397 636V627Q397 453 194 233Q185 223 180 218T174 211T171 208T165 201L163 186Q159 142 159 123Q159 17 208 17Q228 17 253 30T293 56T335 94Q345 104 349 104ZM360 634Q360 655 354 661T336 668Q328 668 322 666T302 645T272 592Q252 547 229 467T192 330L179 273Q179 272 186 280T204 300T221 322Q327 453 355 590Q360 612 360 634Z" style="stroke-width: 3;"/></g></g></g></g><g data-mml-node="mo" transform="translate(5655.3,0) translate(0 250)"/></g><g data-mml-node="mo" transform="translate(21630.3,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(21908.3,0)"><g data-mml-node="mspace"/></g><g data-mml-node="msub" transform="translate(23074.9,0)"><g data-mml-node="mi"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(554,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mtext"><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(278,0)" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(24546.4,0)"><path data-c="226B" d="M55 539T55 547T60 561T74 567Q81 567 207 498Q297 449 365 412Q633 265 636 261Q639 255 639 250Q639 241 626 232Q614 224 365 88Q83 -65 79 -66Q76 -67 73 -67Q65 -67 60 -61T55 -47Q55 -39 61 -33Q62 -33 95 -15T193 39T320 109L321 110H322L323 111H324L325 112L326 113H327L329 114H330L331 115H332L333 116L334 117H335L336 118H337L338 119H339L340 120L341 121H342L343 122H344L345 123H346L347 124L348 125H349L351 126H352L353 127H354L355 128L356 129H357L358 130H359L360 131H361L362 132L363 133H364L365 134H366L367 135H368L369 136H370L371 137L372 138H373L374 139H375L376 140L378 141L576 251Q63 530 62 533Q55 539 55 547ZM360 539T360 547T365 561T379 567Q386 567 512 498Q602 449 670 412Q938 265 941 261Q944 255 944 250Q944 241 931 232Q919 224 670 88Q388 -65 384 -66Q381 -67 378 -67Q370 -67 365 -61T360 -47Q360 -39 366 -33Q367 -33 400 -15T498 39T625 109L626 110H627L628 111H629L630 112L631 113H632L634 114H635L636 115H637L638 116L639 117H640L641 118H642L643 119H644L645 120L646 121H647L648 122H649L650 123H651L652 124L653 125H654L656 126H657L658 127H659L660 128L661 129H662L663 130H664L665 131H666L667 132L668 133H669L670 134H671L672 135H673L674 136H675L676 137L677 138H678L679 139H680L681 140L683 141L881 251Q368 530 367 533Q360 539 360 547Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(25824.2,0)"><g data-mml-node="mi"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(554,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mtext"><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" style="stroke-width: 3;"/><path data-c="75" d="M383 58Q327 -10 256 -10H249Q124 -10 105 89Q104 96 103 226Q102 335 102 348T96 369Q86 385 36 385H25V408Q25 431 27 431L38 432Q48 433 67 434T105 436Q122 437 142 438T172 441T184 442H187V261Q188 77 190 64Q193 49 204 40Q224 26 264 26Q290 26 311 35T343 58T363 90T375 120T379 144Q379 145 379 161T380 201T380 248V315Q380 361 370 372T320 385H302V431Q304 431 378 436T457 442H464V264Q464 84 465 81Q468 61 479 55T524 46H542V0Q540 0 467 -5T390 -11H383V58Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(1056,0)" style="stroke-width: 3;"/></g></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>ℓ</mi><mo stretchy="false">←</mo><mi>ℓ</mi><mo>+</mo><mo stretchy="false">(</mo><mi>a</mi><mo>−</mo><mi>ℓ</mi><mo stretchy="false">)</mo><mrow data-mjx-texclass="ORD"><mo minsize="1.2em" maxsize="1.2em">(</mo></mrow><mn>1</mn><mo>−</mo><msup><mi>e</mi><mrow data-mjx-texclass="ORD"><mo>−</mo><mi>k</mi><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mi mathvariant="normal">Δ</mi><mi>t</mi></mrow></msup><mrow data-mjx-texclass="ORD"><mo minsize="1.2em" maxsize="1.2em">)</mo></mrow><mo>,</mo><mstyle scriptlevel="0"><mspace width="2em"/></mstyle><mi>k</mi><mo>=</mo><mrow data-mjx-texclass="INNER"><mo data-mjx-texclass="OPEN">{</mo><mtable columnalign="left left" columnspacing="1em" rowspacing="0.4em 0.2em"><mtr><mtd><msub><mi>k</mi><mrow data-mjx-texclass="ORD"><mtext>in</mtext></mrow></msub></mtd><mtd><mi>a</mi><mo>≤</mo><mi>ℓ</mi></mtd></mtr><mtr><mtd><msub><mi>k</mi><mrow data-mjx-texclass="ORD"><mtext>out</mtext></mrow></msub></mtd><mtd><mi>a</mi><mo>&gt;</mo><mi>ℓ</mi></mtd></mtr></mtable><mo data-mjx-texclass="CLOSE" fence="true" stretchy="true" symmetric="true"/></mrow><mo>,</mo><mstyle scriptlevel="0"><mspace width="1em"/></mstyle><msub><mi>k</mi><mrow data-mjx-texclass="ORD"><mtext>in</mtext></mrow></msub><mo>≫</mo><msub><mi>k</mi><mrow data-mjx-texclass="ORD"><mtext>out</mtext></mrow></msub></math></mjx-assistive-mml></mjx-container><p>and the ease-out branch only runs once the clearance has held for the dwell time <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.029ex;" xmlns="http://www.w3.org/2000/svg" width="1.17ex" height="1.005ex" role="img" focusable="false" viewBox="0 -431 517 444" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D70F" d="M39 284Q18 284 18 294Q18 301 45 338T99 398Q134 425 164 429Q170 431 332 431Q492 431 497 429Q517 424 517 402Q517 388 508 376T485 360Q479 358 389 358T299 356Q298 355 283 274T251 109T233 20Q228 5 215 -4T186 -13Q153 -13 153 20V30L203 192Q214 228 227 272T248 336L254 357Q254 358 208 358Q206 358 197 358T183 359Q105 359 61 295Q56 287 53 286T39 284Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>τ</mi></math></mjx-assistive-mml></mjx-container>. The <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="9.505ex" height="2.152ex" role="img" focusable="false" viewBox="0 -869.3 4201.3 951.3" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(1722.4,0)"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(499,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mo"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(778,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(1299,0) scale(1.414)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(1535.2,0)"><path data-c="394" d="M51 0Q46 4 46 7Q46 9 215 357T388 709Q391 716 416 716Q439 716 444 709Q447 705 616 357T786 7Q786 4 781 0H51ZM507 344L384 596L137 92L383 91H630Q630 93 507 344Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2368.2,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>1</mn><mo>−</mo><msup><mi>e</mi><mrow data-mjx-texclass="ORD"><mo>−</mo><mi>k</mi><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mi mathvariant="normal">Δ</mi><mi>t</mi></mrow></msup></math></mjx-assistive-mml></mjx-container> form matters beyond looking tidy. It fixes the response time constant at <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="3.441ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 1521 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(500,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(1000,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>1</mn><mrow data-mjx-texclass="ORD"><mo>/</mo></mrow><mi>k</mi></math></mjx-assistive-mml></mjx-container> regardless of frame rate, so the camera feels identical at 30 and 144 frames per second, where the naive constant-blend <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="16.286ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 7198.4 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="2113" d="M345 104T349 104T361 95T369 80T352 59Q268 -20 206 -20Q170 -20 146 3T113 53T99 104L94 129Q94 130 79 116T48 86T28 70Q22 70 15 79T7 94Q7 98 12 103T58 147L91 179V185Q91 186 91 191T92 200Q92 282 128 400T223 612T336 705Q397 705 397 636V627Q397 453 194 233Q185 223 180 218T174 211T171 208T165 201L163 186Q159 142 159 123Q159 17 208 17Q228 17 253 30T293 56T335 94Q345 104 349 104ZM360 634Q360 655 354 661T336 668Q328 668 322 666T302 645T272 592Q252 547 229 467T192 330L179 273Q179 272 186 280T204 300T221 322Q327 453 355 590Q360 612 360 634Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(694.8,0)"><path data-c="2190" d="M944 261T944 250T929 230H165Q167 228 182 216T211 189T244 152T277 96T303 25Q308 7 308 0Q308 -11 288 -11Q281 -11 278 -11T272 -7T267 2T263 21Q245 94 195 151T73 236Q58 242 55 247Q55 254 59 257T73 264Q121 283 158 314T215 375T247 434T264 480L267 497Q269 503 270 505T275 509T288 511Q308 511 308 500Q308 493 303 475Q293 438 278 406T246 352T215 315T185 287T165 270H929Q944 261 944 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1972.6,0)"><path data-c="2113" d="M345 104T349 104T361 95T369 80T352 59Q268 -20 206 -20Q170 -20 146 3T113 53T99 104L94 129Q94 130 79 116T48 86T28 70Q22 70 15 79T7 94Q7 98 12 103T58 147L91 179V185Q91 186 91 191T92 200Q92 282 128 400T223 612T336 705Q397 705 397 636V627Q397 453 194 233Q185 223 180 218T174 211T171 208T165 201L163 186Q159 142 159 123Q159 17 208 17Q228 17 253 30T293 56T335 94Q345 104 349 104ZM360 634Q360 655 354 661T336 668Q328 668 322 666T302 645T272 592Q252 547 229 467T192 330L179 273Q179 272 186 280T204 300T221 322Q327 453 355 590Q360 612 360 634Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2611.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3612,0)"><path data-c="1D6FC" d="M34 156Q34 270 120 356T309 442Q379 442 421 402T478 304Q484 275 485 237V208Q534 282 560 374Q564 388 566 390T582 393Q603 393 603 385Q603 376 594 346T558 261T497 161L486 147L487 123Q489 67 495 47T514 26Q528 28 540 37T557 60Q559 67 562 68T577 70Q597 70 597 62Q597 56 591 43Q579 19 556 5T512 -10H505Q438 -10 414 62L411 69L400 61Q390 53 370 41T325 18T267 -2T203 -11Q124 -11 79 39T34 156ZM208 26Q257 26 306 47T379 90L403 112Q401 255 396 290Q382 405 304 405Q235 405 183 332Q156 292 139 224T121 120Q121 71 146 49T208 26Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4252,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4641,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(5392.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6392.4,0)"><path data-c="2113" d="M345 104T349 104T361 95T369 80T352 59Q268 -20 206 -20Q170 -20 146 3T113 53T99 104L94 129Q94 130 79 116T48 86T28 70Q22 70 15 79T7 94Q7 98 12 103T58 147L91 179V185Q91 186 91 191T92 200Q92 282 128 400T223 612T336 705Q397 705 397 636V627Q397 453 194 233Q185 223 180 218T174 211T171 208T165 201L163 186Q159 142 159 123Q159 17 208 17Q228 17 253 30T293 56T335 94Q345 104 349 104ZM360 634Q360 655 354 661T336 668Q328 668 322 666T302 645T272 592Q252 547 229 467T192 330L179 273Q179 272 186 280T204 300T221 322Q327 453 355 590Q360 612 360 634Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6809.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>ℓ</mi><mo stretchy="false">←</mo><mi>ℓ</mi><mo>+</mo><mi>α</mi><mo stretchy="false">(</mo><mi>a</mi><mo>−</mo><mi>ℓ</mi><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container> would snap faster on a fast machine and mush on a slow one.</p>
<p>The last behaviour is for the tight interiors that started this whole part. When the boom collapses short enough that the camera is right on top of the player, we hide the player's own avatar and let the view sit near first person. This is what Breath of the Wild does in a cramped shrine and what most third-person games fall back to in a corner, because the alternative, a camera jammed against a wall staring at the back of a head, is useless. The rig exposes a single flag for it, and the world loop reads the flag and toggles the local avatar's visibility. Below a metre of boom you're effectively in first person, the walls are honoured, and the moment you back into a room with space the avatar fades back in and the boom extends.</p>
<h2 id="what-plugs-in-next" tabindex="-1">What plugs in next <a class="header-anchor" href="#what-plugs-in-next" aria-label="Permalink to &quot;What plugs in next&quot;"></a></h2>
<p>The terrain collider ships today and it's the hard half, because terrain is everywhere and a distance field is the awkward thing to probe. The props and the building shells from the authoring spikes are the easy half, and the contract is already waiting for them. A second collider, a raycast collider, casts from the pivot toward the camera against a list of meshes and reports the nearest hit the same way the terrain collider does. The cheap version casts a single ray, which is fine until prop counts climb, and the upgrade is to swap the ray for a swept sphere using three-mesh-bvh, Garrett Johnson's library that wraps a mesh in a bounding-volume hierarchy so spatial queries run in log time instead of brute force. Either way the rig doesn't change. It queries a longer list of colliders and takes the nearest hit, which is the entire point of having built the contract first and the colliders second.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>A boom that owns one number.</strong> The camera rig is a post-process over OrbitControls, not a replacement for it. It owns the length of the boom and leaves yaw, pitch, zoom, and gesture handling with the orbit controller we already trusted. The integration is two bracketing calls: <code>beforeControls()</code> restores the previous frame's full distance so the orbit math reads the user's true zoom rather than mistaking a collision shortening for a dolly-in, and <code>afterControls(dt)</code> resolves the collision and writes the rendered position. Without that pair the camera collapses onto the player over a few frames.</p>
<p><strong>A pluggable collider contract.</strong> A collider is any object with a <code>probe</code> that answers &quot;how far can the camera travel from the pivot along this ray before you block it.&quot; The rig queries every collider and takes the nearest hit, ignorant of whether the obstacle is terrain, a prop, or a wall, which is the same own-one-thing discipline the <a href="/blog/2026-05-15-open-world-browser-part-29-pluggable-character.html">pluggable character controller</a> used for locomotion. The probe is a swept sphere sized to contain the near plane, not a thin ray, so the frustum corners never clip a surface the centre ray would miss. Its radius is derived from the projection, the distance to a near-plane corner <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.212ex;" xmlns="http://www.w3.org/2000/svg" width="14.705ex" height="2.398ex" role="img" focusable="false" viewBox="0 -966.5 6499.5 1060" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msqrt"><g transform="translate(853,0)"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(633,289) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1258.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(2259,0)"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(749,289) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3633.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(4634,0)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(609,289) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(0,106.5)"><path data-c="221A" d="M95 178Q89 178 81 186T72 200T103 230T169 280T207 309Q209 311 212 311H213Q219 311 227 294T281 177Q300 134 312 108L397 -77Q398 -77 501 136T707 565T814 786Q820 800 834 800Q841 800 846 794T853 782V776L620 293L385 -193Q381 -200 366 -200Q357 -200 354 -197Q352 -195 256 15L160 225L144 214Q129 202 113 190T95 178Z" style="stroke-width: 3;"/></g><rect width="5646.5" height="60" x="853" y="846.5"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msqrt><msup><mi>n</mi><mn>2</mn></msup><mo>+</mo><msup><mi>w</mi><mn>2</mn></msup><mo>+</mo><msup><mi>h</mi><mn>2</mn></msup></msqrt></math></mjx-assistive-mml></mjx-container> times a safety margin, and recomputed whenever the field of view, aspect, or near plane changes.</p>
<p><strong>A signed-distance terrain probe that honours overhangs and caves.</strong> Because terrain is a <a href="/blog/2026-02-25-open-world-browser-part-07-marching-cubes.html">signed distance field</a> rather than a heightmap, the same probe that stops the camera against a hillside stops it against a cave ceiling, the case a heightmap raycast structurally cannot see. The march is Hart-style sphere tracing, stepping by an under-relaxed fraction of the slack the field reports and clamped at both ends, because the heightmap regions report vertical clearance rather than true distance and over-report how far the camera can move on a slope, so a full stride would hop a ridge. A gradient-driven depenetration pass is the safety net that recovers the view when the ground is sculpted out from under the camera.</p>
<p><strong>Asymmetric damping with a dwell timer.</strong> The boom snaps in fast when an occluder appears and eases out slowly once it clears, and only after a short window of continuous clearance, so whipping past a fence post never makes the camera lunge. Below a collapse threshold the rig flags near-first-person and the world loop hides the local avatar, the standard fallback for tight interiors instead of a camera buried in a wall.</p>
<h2 id="references" tabindex="-1">References <a class="header-anchor" href="#references" aria-label="Permalink to &quot;References&quot;"></a></h2>
<p>The framing of camera control as a visibility constraint comes from Marc Christie and Patrick Olivier, <a href="https://people.irisa.fr/Marc.Christie/Publications/2008/CON08.html" target="_blank" rel="noreferrer"><em>Camera Control in Computer Graphics</em></a> (Computer Graphics Forum, 2008). The distance-field march is John C. Hart, <a href="https://graphics.stanford.edu/courses/cs348b-20-spring-content/uploads/hart.pdf" target="_blank" rel="noreferrer"><em>Sphere Tracing: A Geometric Method for the Antialiased Ray Tracing of Implicit Surfaces</em></a> (The Visual Computer, 1996). The spring-arm pattern and its probe-sphere collision are documented in Epic's <a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/using-spring-arm-components-in-unreal-engine" target="_blank" rel="noreferrer">Spring Arm Component</a> and Unity's <a href="https://docs.unity3d.com/Packages/com.unity.cinemachine@3.1/manual/CinemachineDeoccluder.html" target="_blank" rel="noreferrer">Cinemachine Deoccluder</a> and <a href="https://docs.unity3d.com/Packages/com.unity.cinemachine@3.1/manual/CinemachineThirdPersonFollow.html" target="_blank" rel="noreferrer">Third Person Follow</a>. The motion and damping intuition is from Mark Haigh-Hutchinson, <em>Real-Time Cameras</em> (Morgan Kaufmann, 2009), and Itay Keren, <a href="https://www.gamedeveloper.com/design/scroll-back-the-theory-and-practice-of-cameras-in-side-scrollers" target="_blank" rel="noreferrer"><em>Scroll Back: The Theory and Practice of Cameras in Side-Scrollers</em></a> (GDC 2015). The mesh-collider upgrade path is Garrett Johnson's <a href="https://github.com/gkjohnson/three-mesh-bvh" target="_blank" rel="noreferrer">three-mesh-bvh</a>.</p>
<hr>
<p>Part 30 of 30.
Previous: <a href="/blog/2026-05-15-open-world-browser-part-29-pluggable-character.html">Part 29 - One controller, any body</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Math rendering test]]></title>
            <link>https://app.cinevva.com/blog/math-test</link>
            <guid>https://app.cinevva.com/blog/math-test</guid>
            <pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A scratch page to verify LaTeX math formulas render correctly in VitePress.]]></description>
            <content:encoded><![CDATA[<h1 id="math-rendering-test" tabindex="-1">Math rendering test <a class="header-anchor" href="#math-rendering-test" aria-label="Permalink to &quot;Math rendering test&quot;"></a></h1>
<p>This page exists to verify that LaTeX math renders in our markdown articles. Math support is provided by <code>markdown-it-mathjax3</code>, enabled with <code>markdown: { math: true }</code> in the VitePress config.</p>
<h2 id="inline-math" tabindex="-1">Inline math <a class="header-anchor" href="#inline-math" aria-label="Permalink to &quot;Inline math&quot;"></a></h2>
<p>Write inline formulas by wrapping them in single dollar signs. Einstein's relation <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="8.699ex" height="2.072ex" role="img" focusable="false" viewBox="0 -833.9 3845.1 915.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D438" d="M492 213Q472 213 472 226Q472 230 477 250T482 285Q482 316 461 323T364 330H312Q311 328 277 192T243 52Q243 48 254 48T334 46Q428 46 458 48T518 61Q567 77 599 117T670 248Q680 270 683 272Q690 274 698 274Q718 274 718 261Q613 7 608 2Q605 0 322 0H133Q31 0 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H757Q764 676 764 669Q764 664 751 557T737 447Q735 440 717 440H705Q698 445 698 453L701 476Q704 500 704 528Q704 558 697 578T678 609T643 625T596 632T532 634H485Q397 633 392 631Q388 629 386 622Q385 619 355 499T324 377Q347 376 372 376H398Q464 376 489 391T534 472Q538 488 540 490T557 493Q562 493 565 493T570 492T572 491T574 487T577 483L544 351Q511 218 508 216Q505 213 492 213Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1041.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2097.6,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(2975.6,0)"><g data-mml-node="mi"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(466,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></math></mjx-assistive-mml></mjx-container> sits in the flow of a sentence, as does the golden ratio <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.781ex;" xmlns="http://www.w3.org/2000/svg" width="9.701ex" height="3.246ex" role="img" focusable="false" viewBox="0 -1089.5 4288 1434.5" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D711" d="M92 210Q92 176 106 149T142 108T185 85T220 72L235 70L237 71L250 112Q268 170 283 211T322 299T370 375T429 423T502 442Q547 442 582 410T618 302Q618 224 575 152T457 35T299 -10Q273 -10 273 -12L266 -48Q260 -83 252 -125T241 -179Q236 -203 215 -212Q204 -218 190 -218Q159 -215 159 -185Q159 -175 214 -2L209 0Q204 2 195 5T173 14T147 28T120 46T94 71T71 103T56 142T50 190Q50 238 76 311T149 431H162Q183 431 183 423Q183 417 175 409Q134 361 114 300T92 210ZM574 278Q574 320 550 344T486 369Q437 369 394 329T323 218Q309 184 295 109L286 64Q304 62 306 62Q423 62 498 131T574 278Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(931.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(1987.6,0)"><g data-mml-node="mrow" transform="translate(220,418.1) scale(0.707)"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(500,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msqrt" transform="translate(1278,0)"><g transform="translate(853,0)"><g data-mml-node="mn"><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(0,71.9)"><path data-c="221A" d="M95 178Q89 178 81 186T72 200T103 230T169 280T207 309Q209 311 212 311H213Q219 311 227 294T281 177Q300 134 312 108L397 -77Q398 -77 501 136T707 565T814 786Q820 800 834 800Q841 800 846 794T853 782V776L620 293L385 -193Q381 -200 366 -200Q357 -200 354 -197Q352 -195 256 15L160 225L144 214Q129 202 113 190T95 178Z" style="stroke-width: 3;"/></g><rect width="500" height="42.4" x="853" y="829.5"/></g></g><g data-mml-node="mn" transform="translate(973.4,-345) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><rect width="2060.4" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>φ</mi><mo>=</mo><mfrac><mrow><mn>1</mn><mo>+</mo><msqrt><mn>5</mn></msqrt></mrow><mn>2</mn></mfrac></math></mjx-assistive-mml></mjx-container> and a quick sum like <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.781ex;" xmlns="http://www.w3.org/2000/svg" width="15.552ex" height="3.15ex" role="img" focusable="false" viewBox="0 -1047.1 6874.2 1392.1" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="munderover"><g data-mml-node="mo"><path data-c="2211" d="M61 748Q64 750 489 750H913L954 640Q965 609 976 579T993 533T999 516H979L959 517Q936 579 886 621T777 682Q724 700 655 705T436 710H319Q183 710 183 709Q186 706 348 484T511 259Q517 250 513 244L490 216Q466 188 420 134T330 27L149 -187Q149 -188 362 -188Q388 -188 436 -188T506 -189Q679 -189 778 -162T936 -43Q946 -27 959 6H999L913 -249L489 -250Q65 -250 62 -248Q56 -246 56 -239Q56 -234 118 -161Q186 -81 245 -11L428 206Q428 207 242 462L57 717L56 728Q56 744 61 748Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(1089,477.1) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="TeXAtom" transform="translate(1089,-285.4) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(345,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1123,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mi" transform="translate(2453.3,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3076.1,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(4131.9,0)"><g data-mml-node="mrow" transform="translate(220,516.8) scale(0.707)"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(989,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1589,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2367,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2867,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mn" transform="translate(1194.4,-345) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><rect width="2502.3" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><munderover><mo data-mjx-texclass="OP">∑</mo><mrow data-mjx-texclass="ORD"><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mrow data-mjx-texclass="ORD"><mi>n</mi></mrow></munderover><mi>i</mi><mo>=</mo><mfrac><mrow><mi>n</mi><mo stretchy="false">(</mo><mi>n</mi><mo>+</mo><mn>1</mn><mo stretchy="false">)</mo></mrow><mn>2</mn></mfrac></math></mjx-assistive-mml></mjx-container>.</p>
<h2 id="block-math" tabindex="-1">Block math <a class="header-anchor" href="#block-math" aria-label="Permalink to &quot;Block math&quot;"></a></h2>
<p>Wrap a display equation in double dollar signs on their own lines.</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -2.159ex;" xmlns="http://www.w3.org/2000/svg" width="24.023ex" height="5.328ex" role="img" focusable="false" viewBox="0 -1400.6 10618.2 2354.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(550,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(939,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1511,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2177.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msubsup" transform="translate(3233.6,0)"><g data-mml-node="mo" transform="translate(0 1)"><path data-c="222B" d="M114 -798Q132 -824 165 -824H167Q195 -824 223 -764T275 -600T320 -391T362 -164Q365 -143 367 -133Q439 292 523 655T645 1127Q651 1145 655 1157T672 1201T699 1257T733 1306T777 1346T828 1360Q884 1360 912 1325T944 1245Q944 1220 932 1205T909 1186T887 1183Q866 1183 849 1198T832 1239Q832 1287 885 1296L882 1300Q879 1303 874 1307T866 1313Q851 1323 833 1323Q819 1323 807 1311T775 1255T736 1139T689 936T633 628Q574 293 510 -5T410 -437T355 -629Q278 -862 165 -862Q125 -862 92 -831T55 -746Q55 -711 74 -698T112 -685Q133 -685 150 -700T167 -741Q167 -789 114 -798Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(1046.4,1088.1) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="221E" d="M55 217Q55 305 111 373T254 442Q342 442 419 381Q457 350 493 303L507 284L514 294Q618 442 747 442Q833 442 888 374T944 214Q944 128 889 59T743 -11Q657 -11 580 50Q542 81 506 128L492 147L485 137Q381 -11 252 -11Q166 -11 111 57T55 217ZM907 217Q907 285 869 341T761 397Q740 397 720 392T682 378T648 359T619 335T594 310T574 285T559 263T548 246L543 238L574 198Q605 158 622 138T664 94T714 61T765 51Q827 51 867 100T907 217ZM92 214Q92 145 131 89T239 33Q357 33 456 193L425 233Q364 312 334 337Q285 380 233 380Q171 380 132 331T92 214Z" style="stroke-width: 3;"/></g></g><g data-mml-node="TeXAtom" transform="translate(589,-896.4) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mo"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(778,0)"><path data-c="221E" d="M55 217Q55 305 111 373T254 442Q342 442 419 381Q457 350 493 303L507 284L514 294Q618 442 747 442Q833 442 888 374T944 214Q944 128 889 59T743 -11Q657 -11 580 50Q542 81 506 128L492 147L485 137Q381 -11 252 -11Q166 -11 111 57T55 217ZM907 217Q907 285 869 341T761 397Q740 397 720 392T682 378T648 359T619 335T594 310T574 285T559 263T548 246L543 238L574 198Q605 158 622 138T664 94T714 61T765 51Q827 51 867 100T907 217ZM92 214Q92 145 131 89T239 33Q357 33 456 193L425 233Q364 312 334 337Q285 380 233 380Q171 380 132 331T92 214Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(5296.5,0)"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(457,279) translate(-250 0)"><path data-c="5E" d="M112 560L249 694L257 686Q387 562 387 560L361 531Q359 532 303 581L250 627L195 580Q182 569 169 557T148 538L140 532Q138 530 125 546L112 560Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(5846.5,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6235.5,0)"><path data-c="1D709" d="M268 632Q268 704 296 704Q314 704 314 687Q314 682 311 664T308 635T309 620V616H315Q342 619 360 619Q443 619 443 586Q439 548 358 546H344Q326 546 317 549T290 566Q257 550 226 505T195 405Q195 381 201 364T211 342T218 337Q266 347 298 347Q375 347 375 314Q374 297 359 288T327 277T280 275Q234 275 208 283L195 286Q149 260 119 214T88 130Q88 116 90 108Q101 79 129 63T229 20Q238 17 243 15Q337 -21 354 -33Q383 -53 383 -94Q383 -137 351 -171T273 -205Q240 -205 202 -190T158 -167Q156 -163 156 -159Q156 -151 161 -146T176 -140Q182 -140 189 -143Q232 -168 274 -168Q286 -168 292 -165Q313 -151 313 -129Q313 -112 301 -104T232 -75Q214 -68 204 -64Q198 -62 171 -52T136 -38T107 -24T78 -8T56 12T36 37T26 66T21 103Q21 149 55 206T145 301L154 307L148 313Q141 319 136 323T124 338T111 358T103 382T99 413Q99 471 143 524T259 602L271 607Q268 618 268 632Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6673.5,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(7062.5,0)"><g data-mml-node="mspace"/></g><g data-mml-node="msup" transform="translate(7229.5,0)"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(499,413) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(500,0)"><path data-c="1D70B" d="M132 -11Q98 -11 98 22V33L111 61Q186 219 220 334L228 358H196Q158 358 142 355T103 336Q92 329 81 318T62 297T53 285Q51 284 38 284Q19 284 19 294Q19 300 38 329T93 391T164 429Q171 431 389 431Q549 431 553 430Q573 423 573 402Q573 371 541 360Q535 358 472 358H408L405 341Q393 269 393 222Q393 170 402 129T421 65T431 37Q431 20 417 5T381 -10Q370 -10 363 -7T347 17T331 77Q330 86 330 121Q330 170 339 226T357 318T367 358H269L268 354Q268 351 249 275T206 114T175 17Q164 -11 132 -11Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1070,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1415,0)"><path data-c="1D709" d="M268 632Q268 704 296 704Q314 704 314 687Q314 682 311 664T308 635T309 620V616H315Q342 619 360 619Q443 619 443 586Q439 548 358 546H344Q326 546 317 549T290 566Q257 550 226 505T195 405Q195 381 201 364T211 342T218 337Q266 347 298 347Q375 347 375 314Q374 297 359 288T327 277T280 275Q234 275 208 283L195 286Q149 260 119 214T88 130Q88 116 90 108Q101 79 129 63T229 20Q238 17 243 15Q337 -21 354 -33Q383 -53 383 -94Q383 -137 351 -171T273 -205Q240 -205 202 -190T158 -167Q156 -163 156 -159Q156 -151 161 -146T176 -140Q182 -140 189 -143Q232 -168 274 -168Q286 -168 292 -165Q313 -151 313 -129Q313 -112 301 -104T232 -75Q214 -68 204 -64Q198 -62 171 -52T136 -38T107 -24T78 -8T56 12T36 37T26 66T21 103Q21 149 55 206T145 301L154 307L148 313Q141 319 136 323T124 338T111 358T103 382T99 413Q99 471 143 524T259 602L271 607Q268 618 268 632Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1853,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mstyle" transform="translate(9493.2,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(9660.2,0)"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(10180.2,0)"><path data-c="1D709" d="M268 632Q268 704 296 704Q314 704 314 687Q314 682 311 664T308 635T309 620V616H315Q342 619 360 619Q443 619 443 586Q439 548 358 546H344Q326 546 317 549T290 566Q257 550 226 505T195 405Q195 381 201 364T211 342T218 337Q266 347 298 347Q375 347 375 314Q374 297 359 288T327 277T280 275Q234 275 208 283L195 286Q149 260 119 214T88 130Q88 116 90 108Q101 79 129 63T229 20Q238 17 243 15Q337 -21 354 -33Q383 -53 383 -94Q383 -137 351 -171T273 -205Q240 -205 202 -190T158 -167Q156 -163 156 -159Q156 -151 161 -146T176 -140Q182 -140 189 -143Q232 -168 274 -168Q286 -168 292 -165Q313 -151 313 -129Q313 -112 301 -104T232 -75Q214 -68 204 -64Q198 -62 171 -52T136 -38T107 -24T78 -8T56 12T36 37T26 66T21 103Q21 149 55 206T145 301L154 307L148 313Q141 319 136 323T124 338T111 358T103 382T99 413Q99 471 143 524T259 602L271 607Q268 618 268 632Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>f</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mo>=</mo><msubsup><mo data-mjx-texclass="OP">∫</mo><mrow data-mjx-texclass="ORD"><mo>−</mo><mi mathvariant="normal">∞</mi></mrow><mrow data-mjx-texclass="ORD"><mi mathvariant="normal">∞</mi></mrow></msubsup><mrow data-mjx-texclass="ORD"><mover><mi>f</mi><mo stretchy="false">^</mo></mover></mrow><mo stretchy="false">(</mo><mi>ξ</mi><mo stretchy="false">)</mo><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><msup><mi>e</mi><mrow data-mjx-texclass="ORD"><mn>2</mn><mi>π</mi><mi>i</mi><mi>ξ</mi><mi>x</mi></mrow></msup><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mi>d</mi><mi>ξ</mi></math></mjx-assistive-mml></mjx-container><p>The quadratic formula:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -1.575ex;" xmlns="http://www.w3.org/2000/svg" width="20.765ex" height="5.291ex" role="img" focusable="false" viewBox="0 -1642.5 9178 2338.5" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(849.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(1905.6,0)"><g data-mml-node="mrow" transform="translate(220,676)"><g data-mml-node="mo"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(778,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1429.2,0)"><path data-c="B1" d="M56 320T56 333T70 353H369V502Q369 651 371 655Q376 666 388 666Q402 666 405 654T409 596V500V353H707Q722 345 722 333Q722 320 707 313H409V40H707Q722 32 722 20T707 0H70Q56 7 56 20T70 40H369V313H70Q56 320 56 333Z" style="stroke-width: 3;"/></g><g data-mml-node="msqrt" transform="translate(2429.4,0)"><g transform="translate(853,0)"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(462,289) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1087.8,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2088,0)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2588,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3117,0)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(0,106.5)"><path data-c="221A" d="M95 178Q89 178 81 186T72 200T103 230T169 280T207 309Q209 311 212 311H213Q219 311 227 294T281 177Q300 134 312 108L397 -77Q398 -77 501 136T707 565T814 786Q820 800 834 800Q841 800 846 794T853 782V776L620 293L385 -193Q381 -200 366 -200Q357 -200 354 -197Q352 -195 256 15L160 225L144 214Q129 202 113 190T95 178Z" style="stroke-width: 3;"/></g><rect width="3550" height="60" x="853" y="846.5"/></g></g><g data-mml-node="mrow" transform="translate(3121.7,-686)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(500,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g><rect width="7032.4" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>x</mi><mo>=</mo><mfrac><mrow><mo>−</mo><mi>b</mi><mo>±</mo><msqrt><msup><mi>b</mi><mn>2</mn></msup><mo>−</mo><mn>4</mn><mi>a</mi><mi>c</mi></msqrt></mrow><mrow><mn>2</mn><mi>a</mi></mrow></mfrac></math></mjx-assistive-mml></mjx-container><h2 id="matrices-and-aligned-equations" tabindex="-1">Matrices and aligned equations <a class="header-anchor" href="#matrices-and-aligned-equations" aria-label="Permalink to &quot;Matrices and aligned equations&quot;"></a></h2>
<p>A 2x2 matrix:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -2.149ex;" xmlns="http://www.w3.org/2000/svg" width="11.739ex" height="5.43ex" role="img" focusable="false" viewBox="0 -1450 5188.6 2400" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D434" d="M208 74Q208 50 254 46Q272 46 272 35Q272 34 270 22Q267 8 264 4T251 0Q249 0 239 0T205 1T141 2Q70 2 50 0H42Q35 7 35 11Q37 38 48 46H62Q132 49 164 96Q170 102 345 401T523 704Q530 716 547 716H555H572Q578 707 578 706L606 383Q634 60 636 57Q641 46 701 46Q726 46 726 36Q726 34 723 22Q720 7 718 4T704 0Q701 0 690 0T651 1T578 2Q484 2 455 0H443Q437 6 437 9T439 27Q443 40 445 43L449 46H469Q523 49 533 63L521 213H283L249 155Q208 86 208 74ZM516 260Q516 271 504 416T490 562L463 519Q447 492 400 412L310 260L413 259Q516 259 516 260Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1027.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mrow" transform="translate(2083.6,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="5B" d="M247 -949V1450H516V1388H309V-887H516V-949H247Z" style="stroke-width: 3;"/></g><g data-mml-node="mtable" transform="translate(528,0)"><g data-mml-node="mtr" transform="translate(0,700)"><g data-mml-node="mtd"><g data-mml-node="mi"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mtd" transform="translate(1574.5,0)"><g data-mml-node="mi"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mtr" transform="translate(0,-700)"><g data-mml-node="mtd" transform="translate(48,0)"><g data-mml-node="mi"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mtd" transform="translate(1529,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g></g></g></g><g data-mml-node="mo" transform="translate(2577,0) translate(0 -0.5)"><path data-c="5D" d="M11 1388V1450H280V-949H11V-887H218V1388H11Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>A</mi><mo>=</mo><mrow data-mjx-texclass="INNER"><mo data-mjx-texclass="OPEN">[</mo><mtable columnspacing="1em" rowspacing="4pt"><mtr><mtd><mi>a</mi></mtd><mtd><mi>b</mi></mtd></mtr><mtr><mtd><mi>c</mi></mtd><mtd><mi>d</mi></mtd></mtr></mtable><mo data-mjx-texclass="CLOSE">]</mo></mrow></math></mjx-assistive-mml></mjx-container><p>Multi-line alignment with <code>aligned</code>:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -2.339ex;" xmlns="http://www.w3.org/2000/svg" width="23.671ex" height="5.81ex" role="img" focusable="false" viewBox="0 -1533.9 10462.5 2567.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mtable"><g data-mml-node="mtr" transform="translate(0,650)"><g data-mml-node="mtd"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(389,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1140.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2140.4,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(2569.4,0)"><g data-mml-node="mo"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(422,413) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mtd" transform="translate(3395,0)"><g data-mml-node="mi"/><g data-mml-node="mo" transform="translate(277.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(1333.6,0)"><g data-mml-node="mi"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(562,413) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2521.3,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3521.6,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4021.6,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4550.6,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(5201.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(6202,0)"><g data-mml-node="mi"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(462,413) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g></g><g data-mml-node="mtr" transform="translate(0,-783.9)"><g data-mml-node="mtd"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(389,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1140.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2140.4,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(2569.4,0)"><g data-mml-node="mo"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(422,413) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mtd" transform="translate(3395,0)"><g data-mml-node="mi"/><g data-mml-node="mo" transform="translate(277.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(1333.6,0)"><g data-mml-node="mi"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(562,413) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2521.3,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3521.6,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4021.6,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4550.6,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(5201.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(6202,0)"><g data-mml-node="mi"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(462,413) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mtable displaystyle="true" columnalign="right left" columnspacing="0em" rowspacing="3pt"><mtr><mtd><mo stretchy="false">(</mo><mi>a</mi><mo>+</mo><mi>b</mi><msup><mo stretchy="false">)</mo><mn>2</mn></msup></mtd><mtd><mi/><mo>=</mo><msup><mi>a</mi><mn>2</mn></msup><mo>+</mo><mn>2</mn><mi>a</mi><mi>b</mi><mo>+</mo><msup><mi>b</mi><mn>2</mn></msup></mtd></mtr><mtr><mtd><mo stretchy="false">(</mo><mi>a</mi><mo>−</mo><mi>b</mi><msup><mo stretchy="false">)</mo><mn>2</mn></msup></mtd><mtd><mi/><mo>=</mo><msup><mi>a</mi><mn>2</mn></msup><mo>−</mo><mn>2</mn><mi>a</mi><mi>b</mi><mo>+</mo><msup><mi>b</mi><mn>2</mn></msup></mtd></mtr></mtable></math></mjx-assistive-mml></mjx-container><h2 id="greek-subscripts-and-operators" tabindex="-1">Greek, subscripts, and operators <a class="header-anchor" href="#greek-subscripts-and-operators" aria-label="Permalink to &quot;Greek, subscripts, and operators&quot;"></a></h2>
<p>Gradient descent step: <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="19.681ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 8698.8 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D703" d="M35 200Q35 302 74 415T180 610T319 704Q320 704 327 704T339 705Q393 701 423 656Q462 596 462 495Q462 380 417 261T302 66T168 -10H161Q125 -10 99 10T60 63T41 130T35 200ZM383 566Q383 668 330 668Q294 668 260 623T204 521T170 421T157 371Q206 370 254 370L351 371Q352 372 359 404T375 484T383 566ZM113 132Q113 26 166 26Q181 26 198 36T239 74T287 161T335 307L340 324H145Q145 321 136 286T120 208T113 132Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(502,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(361,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1139,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(1988.7,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3044.5,0)"><g data-mml-node="mi"><path data-c="1D703" d="M35 200Q35 302 74 415T180 610T319 704Q320 704 327 704T339 705Q393 701 423 656Q462 596 462 495Q462 380 417 261T302 66T168 -10H161Q125 -10 99 10T60 63T41 130T35 200ZM383 566Q383 668 330 668Q294 668 260 623T204 521T170 421T157 371Q206 370 254 370L351 371Q352 372 359 404T375 484T383 566ZM113 132Q113 26 166 26Q181 26 198 36T239 74T287 161T335 307L340 324H145Q145 321 136 286T120 208T113 132Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(502,-150) scale(0.707)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(4074,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5074.2,0)"><path data-c="1D702" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q156 442 175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336V326Q503 302 439 53Q381 -182 377 -189Q364 -216 332 -216Q319 -216 310 -208T299 -186Q299 -177 358 57L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(5571.2,0)"><g data-mml-node="mi"><path data-c="2207" d="M46 676Q46 679 51 683H781Q786 679 786 676Q786 674 617 326T444 -26Q439 -33 416 -33T388 -26Q385 -22 216 326T46 676ZM697 596Q697 597 445 597T193 596Q195 591 319 336T445 80L697 596Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(866,-150) scale(0.707)"><path data-c="1D703" d="M35 200Q35 302 74 415T180 610T319 704Q320 704 327 704T339 705Q393 701 423 656Q462 596 462 495Q462 380 417 261T302 66T168 -10H161Q125 -10 99 10T60 63T41 130T35 200ZM383 566Q383 668 330 668Q294 668 260 623T204 521T170 421T157 371Q206 370 254 370L351 371Q352 372 359 404T375 484T383 566ZM113 132Q113 26 166 26Q181 26 198 36T239 74T287 161T335 307L340 324H145Q145 321 136 286T120 208T113 132Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(6818.8,0)"><path data-c="1D43D" d="M447 625Q447 637 354 637H329Q323 642 323 645T325 664Q329 677 335 683H352Q393 681 498 681Q541 681 568 681T605 682T619 682Q633 682 633 672Q633 670 630 658Q626 642 623 640T604 637Q552 637 545 623Q541 610 483 376Q420 128 419 127Q397 64 333 21T195 -22Q137 -22 97 8T57 88Q57 130 80 152T132 174Q177 174 182 130Q182 98 164 80T123 56Q115 54 115 53T122 44Q148 15 197 15Q235 15 271 47T324 130Q328 142 387 380T447 625Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(7451.8,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(7840.8,0)"><path data-c="1D703" d="M35 200Q35 302 74 415T180 610T319 704Q320 704 327 704T339 705Q393 701 423 656Q462 596 462 495Q462 380 417 261T302 66T168 -10H161Q125 -10 99 10T60 63T41 130T35 200ZM383 566Q383 668 330 668Q294 668 260 623T204 521T170 421T157 371Q206 370 254 370L351 371Q352 372 359 404T375 484T383 566ZM113 132Q113 26 166 26Q181 26 198 36T239 74T287 161T335 307L340 324H145Q145 321 136 286T120 208T113 132Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(8309.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>θ</mi><mrow data-mjx-texclass="ORD"><mi>t</mi><mo>+</mo><mn>1</mn></mrow></msub><mo>=</mo><msub><mi>θ</mi><mi>t</mi></msub><mo>−</mo><mi>η</mi><msub><mi mathvariant="normal">∇</mi><mi>θ</mi></msub><mi>J</mi><mo stretchy="false">(</mo><mi>θ</mi><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>.</p>
<p>A softmax: <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -3.053ex;" xmlns="http://www.w3.org/2000/svg" width="17.099ex" height="6.111ex" role="img" focusable="false" viewBox="0 -1351.5 7557.7 2701.2" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D70E" d="M184 -11Q116 -11 74 34T31 147Q31 247 104 333T274 430Q275 431 414 431H552Q553 430 555 429T559 427T562 425T565 422T567 420T569 416T570 412T571 407T572 401Q572 357 507 357Q500 357 490 357T476 358H416L421 348Q439 310 439 263Q439 153 359 71T184 -11ZM361 278Q361 358 276 358Q152 358 115 184Q114 180 114 178Q106 141 106 117Q106 67 131 47T188 26Q242 26 287 73Q316 103 334 153T356 233T361 278Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(571,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(960,0)"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(1425,0)"><g data-mml-node="mo"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(422,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2418.7,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(3474.5,0)"><g data-mml-node="mfrac"><g data-mml-node="msup" transform="translate(1487.1,676)"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(499,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(498,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g></g><g data-mml-node="mrow" transform="translate(220,-920)"><g data-mml-node="munderover"><g data-mml-node="mo"><path data-c="2211" d="M61 748Q64 750 489 750H913L954 640Q965 609 976 579T993 533T999 516H979L959 517Q936 579 886 621T777 682Q724 700 655 705T436 710H319Q183 710 183 709Q186 706 348 484T511 259Q517 250 513 244L490 216Q466 188 420 134T330 27L149 -187Q149 -188 362 -188Q388 -188 436 -188T506 -189Q679 -189 778 -162T936 -43Q946 -27 959 6H999L913 -249L489 -250Q65 -250 62 -248Q56 -246 56 -239Q56 -234 118 -161Q186 -81 245 -11L428 206Q428 207 242 462L57 717L56 728Q56 744 61 748Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(1089,477.1) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D43E" d="M285 628Q285 635 228 637Q205 637 198 638T191 647Q191 649 193 661Q199 681 203 682Q205 683 214 683H219Q260 681 355 681Q389 681 418 681T463 682T483 682Q500 682 500 674Q500 669 497 660Q496 658 496 654T495 648T493 644T490 641T486 639T479 638T470 637T456 637Q416 636 405 634T387 623L306 305Q307 305 490 449T678 597Q692 611 692 620Q692 635 667 637Q651 637 651 648Q651 650 654 662T659 677Q662 682 676 682Q680 682 711 681T791 680Q814 680 839 681T869 682Q889 682 889 672Q889 650 881 642Q878 637 862 637Q787 632 726 586Q710 576 656 534T556 455L509 418L518 396Q527 374 546 329T581 244Q656 67 661 61Q663 59 666 57Q680 47 717 46H738Q744 38 744 37T741 19Q737 6 731 0H720Q680 3 625 3Q503 3 488 0H478Q472 6 472 9T474 27Q478 40 480 43T491 46H494Q544 46 544 71Q544 75 517 141T485 216L427 354L359 301L291 248L268 155Q245 63 245 58Q245 51 253 49T303 46H334Q340 37 340 35Q340 19 333 5Q328 0 317 0Q314 0 280 1T180 2Q118 2 85 2T49 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Z" style="stroke-width: 3;"/></g></g><g data-mml-node="TeXAtom" transform="translate(1089,-285.4) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D457" d="M297 596Q297 627 318 644T361 661Q378 661 389 651T403 623Q403 595 384 576T340 557Q322 557 310 567T297 596ZM288 376Q288 405 262 405Q240 405 220 393T185 362T161 325T144 293L137 279Q135 278 121 278H107Q101 284 101 286T105 299Q126 348 164 391T252 441Q253 441 260 441T272 442Q296 441 316 432Q341 418 354 401T367 348V332L318 133Q267 -67 264 -75Q246 -125 194 -164T75 -204Q25 -204 7 -183T-12 -137Q-12 -110 7 -91T53 -71Q70 -71 82 -81T95 -112Q95 -148 63 -167Q69 -168 77 -168Q111 -168 139 -140T182 -74L193 -32Q204 11 219 72T251 197T278 308T289 365Q289 372 288 376Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(412,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1190,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="msup" transform="translate(2500.7,0)"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(499,318.6) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(498,-150) scale(0.707)"><path data-c="1D457" d="M297 596Q297 627 318 644T361 661Q378 661 389 651T403 623Q403 595 384 576T340 557Q322 557 310 567T297 596ZM288 376Q288 405 262 405Q240 405 220 393T185 362T161 325T144 293L137 279Q135 278 121 278H107Q101 284 101 286T105 299Q126 348 164 391T252 441Q253 441 260 441T272 442Q296 441 316 432Q341 418 354 401T367 348V332L318 133Q267 -67 264 -75Q246 -125 194 -164T75 -204Q25 -204 7 -183T-12 -137Q-12 -110 7 -91T53 -71Q70 -71 82 -81T95 -112Q95 -148 63 -167Q69 -168 77 -168Q111 -168 139 -140T182 -74L193 -32Q204 11 219 72T251 197T278 308T289 365Q289 372 288 376Z" style="stroke-width: 3;"/></g></g></g></g></g><rect width="3843.2" height="60" x="120" y="220"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>σ</mi><mo stretchy="false">(</mo><mi>z</mi><msub><mo stretchy="false">)</mo><mi>i</mi></msub><mo>=</mo><mstyle displaystyle="true" scriptlevel="0"><mfrac><msup><mi>e</mi><mrow data-mjx-texclass="ORD"><msub><mi>z</mi><mi>i</mi></msub></mrow></msup><mrow><munderover><mo data-mjx-texclass="OP">∑</mo><mrow data-mjx-texclass="ORD"><mi>j</mi><mo>=</mo><mn>1</mn></mrow><mrow data-mjx-texclass="ORD"><mi>K</mi></mrow></munderover><msup><mi>e</mi><mrow data-mjx-texclass="ORD"><msub><mi>z</mi><mi>j</mi></msub></mrow></msup></mrow></mfrac></mstyle></math></mjx-assistive-mml></mjx-container>.</p>
<h2 id="escaping-a-literal-dollar-sign" tabindex="-1">Escaping a literal dollar sign <a class="header-anchor" href="#escaping-a-literal-dollar-sign" aria-label="Permalink to &quot;Escaping a literal dollar sign&quot;"></a></h2>
<p>To show a real dollar amount without triggering math, escape it: it costs $5 to $10. Only unescaped <code>$</code> pairs are treated as math.</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 29: One controller, any body]]></title>
            <link>https://app.cinevva.com/blog/2026-05-15-open-world-browser-part-29-pluggable-character</link>
            <guid>https://app.cinevva.com/blog/2026-05-15-open-world-browser-part-29-pluggable-character</guid>
            <pubDate>Fri, 15 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 58 builds a character controller where the physics engine knows nothing about walking, swimming, or gliding. Every behavior is a pluggable controller, the same engine drives the player, a rideable horse, and an autonomous NPC, and the whole thing runs headless in Node. Spike 59 hangs a retargeted avatar on it without touching a controller, and spike 60 hangs a Synty pack that needs no retargeting at all, behind a clip-picker you can unit test.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-29-one-controller-any-body" tabindex="-1">Building an open world in the browser, part 29: One controller, any body <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-29-one-controller-any-body" aria-label="Permalink to &quot;Building an open world in the browser, part 29: One controller, any body&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-14-open-world-browser-part-28-grass-and-occlusion.html">Part 28</a> covered grass and occlusion. Twenty-eight parts built a world to stand in: terrain you can read, water you can swim, foliage that holds up to the horizon, a server that remembers what you changed. This part is about the thing that moves through all of it, and it's the payoff of the whole engine, because the goal isn't a player controller. It's a controller that doesn't care what body it's driving, where its animations came from, or whether a human or an AI is at the wheel. Spike 58 builds movement as a stack of pluggable behaviors over a physics engine that knows nothing about locomotion, then proves the architecture by running three different bodies through one copy of it. Spike 59 hangs a real retargeted avatar on that controller without changing a single line of it. Spike 60 hangs a different animation pack that needs no retargeting at all, and pulls the clip-picking logic out into something you can actually test.</p>
<h2 id="a-physics-engine-that-knows-nothing-about-walking" tabindex="-1">A physics engine that knows nothing about walking <a class="header-anchor" href="#a-physics-engine-that-knows-nothing-about-walking" aria-label="Permalink to &quot;A physics engine that knows nothing about walking&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/58-pluggable-character/" title="Spike 58 Pluggable Character" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/58-pluggable-character/" target="_blank">Open Spike 58 in a new tab ↗</a> · <a href="/blog/spike-source?spike=58-pluggable-character">View source</a></p>
<p>The design rule is severe and it's the whole point: the capsule engine owns no locomotion. No walk, no run, no jump, no friction, no top-speed cap, not even gravity. It integrates a kinematic capsule against terrain and nothing more. Every locomotion behavior, walk, slide, glide, climb, swim, crouch, stamina, lives in a self-contained controller registered with the engine. Each frame the engine ticks every controller, asks each one whether it wants control, and lets the highest-priority claimant write velocity. Walk has no special status. It's just the lowest-priority controller that always says yes, so it's the default when nothing else fires. A controller is six small functions: a <code>tick</code> that updates its own internal state every frame even when it isn't active, a pure <code>wantsControl</code> predicate that claims the frame, an <code>applyForces</code> that only the winner runs and that writes velocity and applies its own gravity if it wants any, plus optional <code>onEnter</code>, <code>onExit</code>, and <code>stateName</code>. Swim returns <code>ownsCollision: true</code> from <code>applyForces</code> to take over terrain handling, because its buoyancy spring would otherwise fight the engine's foot-snap.</p>
<p>Even the things that aren't locomotion still leak through that contract, which is what forced the next idea. The piece that earns the architecture is channels. An earlier single-channel design ran everything through one arbitration, which meant a stamina tracker or a crouch stance had to pretend to be locomotion and then decline control with a <code>wantsControl</code> returns-false hack just to run its bookkeeping. The fix splits controllers into named channels that arbitrate independently and apply in a fixed order: resource, then stance, then locomotion. Resource runs first because its writes, like stamina decay, get read by the others. Stance runs second because crouch shrinking the capsule height has to land before walk reads it to cap top speed. Locomotion runs last and owns the per-frame velocity write. So a stamina observer and a crouch modifier and an active swim controller all coexist cleanly, each in its own channel, with no controller having to lie about what it is. The demo visualizes all of this with a bare capsule that changes color by active controller, so you can watch arbitration happen: green walk flips to orange on a steep slope as slide takes over, to cyan in the lake as swim wins, to cream midair when you tap glide.</p>
<h2 id="one-engine-three-bodies" tabindex="-1">One engine, three bodies <a class="header-anchor" href="#one-engine-three-bodies" aria-label="Permalink to &quot;One engine, three bodies&quot;"></a></h2>
<p>The real test of &quot;owns no locomotion&quot; isn't the player. It's whether the same engine, untouched, can drive something that isn't a player at all. <code>createCapsuleEngine</code> is a pure factory with no module-level state, no singletons, and no per-instance side effects, so the spike instantiates it three times. The player is one instance with the full controller set. A rideable horse is a second instance that registers only a walk controller, which is the entire idea made literal: a body's movement vocabulary is just whichever controllers you registered, so the horse is faster on the flat and physically can't climb, swim, or glide, because those controllers were never added. An autonomous NPC is a third instance, ticked every frame alongside the player, driven by a wander controller that synthesizes its own input so the body steers itself with no hands on the keyboard. The engine never learns that one of its bodies is a horse or that another is AI-driven. They're all the same capsule integrator with different controller lists.</p>
<p>Mounting is the one piece that deliberately lives outside the engine. Swapping control between the player and the horse means coordinating two engines, and a controller runs inside one engine and can't see across that boundary, so the mount logic sits at the host level: it's a small state machine that decides which engine gets stepped this frame, freezes the other, eases the rider onto the saddle with a smoothstep over three quarters of a second, and tells the camera which body to track. The engine factory never hears about any of it. That's the line the architecture draws and keeps. Behaviors that belong to one body are controllers; coordination between bodies is the host's job, and keeping those separate is why a fourth or fortieth body would cost nothing new.</p>
<h2 id="tested-without-a-browser" tabindex="-1">Tested without a browser <a class="header-anchor" href="#tested-without-a-browser" aria-label="Permalink to &quot;Tested without a browser&quot;"></a></h2>
<p>Because the engine reaches for no <code>window</code>, no <code>document</code>, and no Three.js, the whole thing runs headless. The spike ships a Node harness that mocks the terrain interface, drives the engine frame by frame with scripted input, and asserts on the resulting state, so the regressions that are miserable to catch by play-testing get caught in a script instead: the jump-and-land transition, swim entry and exit thresholds, climb auto-clearing when a surface flattens, stamina drain rates. Feeding the same input and timestep sequence twice and checking for byte-identical output state pins the engine as deterministic, which is the property a networked build will eventually lean on. One regression the harness caught is worth keeping: stepping from walkable ground onto a steeper-than-walk slope used to engage the slide controller and bounce the player back up the hill. The fix was a descent gate. Sliding only starts if the capsule is actually falling onto the slope, so walking horizontally into a steep face now blocks cleanly instead of sliding, and the test that asserts &quot;walking into a non-walkable slope blocks the player&quot; keeps it fixed.</p>
<h2 id="plugging-in-a-real-avatar-without-touching-the-controllers" tabindex="-1">Plugging in a real avatar without touching the controllers <a class="header-anchor" href="#plugging-in-a-real-avatar-without-touching-the-controllers" aria-label="Permalink to &quot;Plugging in a real avatar without touching the controllers&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/59-pluggable-with-avatar/" title="Spike 59 Pluggable With Avatar" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/59-pluggable-with-avatar/" target="_blank">Open Spike 59 in a new tab ↗</a> · <a href="/blog/spike-source?spike=59-pluggable-with-avatar">View source</a></p>
<p>Spike 59 is the test of whether the controller layer is really decoupled from the body. It swaps the colored capsule for a real skinned character, the 3MIKE FBX rig with Quaternius Universal Animation Library clips retargeted onto it, and the controllers don't change at all. The seam is a single string. Each controller already reports a state name through <code>stateName</code>, idle, walk, run, jump, fall, land, slide, glide, swim, swimIdle, and the avatar layer maps that name to a retargeted clip through an alias table and crossfades on switch. Walk picks its own sub-state dynamically from grounded plus vertical velocity plus horizontal speed, so a single walk controller drives idle, walk, run, jump, fall, and land, and the avatar just follows the reported name. Because this spike is single-player, the avatar drops the wire-integer indirection and multi-character abstraction from the earlier networked spikes and maps the state name straight to a clip. The proof is that the entire visual upgrade from capsule to rigged human touched zero lines of locomotion code, which is exactly what a pluggable controller is supposed to buy.</p>
<h2 id="a-pack-that-needs-no-retargeting-and-a-picker-you-can-test" tabindex="-1">A pack that needs no retargeting, and a picker you can test <a class="header-anchor" href="#a-pack-that-needs-no-retargeting-and-a-picker-you-can-test" aria-label="Permalink to &quot;A pack that needs no retargeting, and a picker you can test&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/60-polygon-animations/" title="Spike 60 Polygon Animations" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/60-polygon-animations/" target="_blank">Open Spike 60 in a new tab ↗</a> · <a href="/blog/spike-source?spike=60-polygon-animations">View source</a></p>
<p>Spike 60 plugs in a third body, Synty's POLYGON Base Locomotion pack, and the loader is almost nothing. Each clip ships as a standalone FBX carrying an embedded copy of the same Synty skeleton plus one baked animation, and because the character rig and every clip use identical bone names, you grab the clip off <code>fbx.animations[0]</code> and play it directly on the character's mixer with no retargeting library in the loop. Three.js resolves animation track targets by bone name rather than object identity, so a Synty or Mixamo-style pack authored against the matching rig just works. That's the deliberate contrast with the UAL path from the previous spike, which needs heavyweight retargeting because the source clips and the target rig were authored against different skeletons. Same controller, same state-name seam, two completely different animation pipelines behind it.</p>
<p>The other half of spike 60 is making the clip-picking logic testable. Choosing which clip to play is full of judgment thresholds, and that logic had been buried inside the avatar layer next to FBXLoader and the DOM where it couldn't be exercised. The spike extracts the picker into pure functions that touch neither Three.js nor the window: they take a plain player record (velocity, horizontal speed, facing, grounded, ground normal, impact velocity) and a clip-action stand-in, and return a clip alias string. That lets the thresholds live as named, asserted constants. Jump splits into walking, running, and sprinting variants by speed buckets aligned with the walk controller's actual sprint threshold; landings split into soft, medium, and hard by impact velocity; uphill and downhill clip variants fire from a slope-projection dot product with a flat-clip deadzone below about seven degrees; an idle-to-locomotion bridge always resolves forward so a standing start never plays a clip backward; and a stop bridge is gated by a minimum time-in-loop, because Synty's foot-phase stop clips contain about a second of authored deceleration that looks ridiculous tacked onto a step the player barely took. A small state debouncer holds off the fall state for a few frames so a one-frame loss of ground contact across a seam doesn't flicker the animation. Pulling all of that out of the rendering shell means the rules can be unit tested without a GPU, the same headless discipline the engine itself got in spike 58, which is the difference between locomotion feel you tune by guessing and locomotion feel you can pin down.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>A locomotion-free physics engine.</strong> The capsule engine integrates a kinematic body against terrain and owns no walk, run, jump, friction, speed cap, or gravity. Every behavior is a registered controller exposing <code>tick</code>, <code>wantsControl</code>, <code>applyForces</code>, and optional <code>onEnter</code>/<code>onExit</code>/<code>stateName</code>. Walk is just the lowest-priority always-yes default, and a controller can return <code>ownsCollision: true</code> to take over terrain handling (swim does, so its buoyancy spring doesn't fight foot-snap).</p>
<p><strong>Independent arbitration channels.</strong> Controllers register into named channels (resource, stance, locomotion) that arbitrate separately and apply in a fixed order, so observers like stamina and modifiers like crouch coexist with active locomotion instead of faking control and declining it. Resource writes (stamina) are read by stance and locomotion; stance writes (crouch capsule height) are read by locomotion's speed cap; locomotion runs last and owns the velocity write.</p>
<p><strong>One factory, many bodies.</strong> <code>createCapsuleEngine</code> is a pure factory with no singletons, so the same engine drives the player, a walk-only rideable horse, and a self-steering NPC fed synthetic input. A body's movement vocabulary is exactly its registered controller set, so the horse can't climb or swim because those controllers were never added. Mounting lives at the host level, not in a controller, because it coordinates two engines that can't see across each other. See <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">GPU-driven LOD</a>.</p>
<p><strong>Headless, deterministic testing.</strong> The engine touches no <code>window</code>, <code>document</code>, or Three.js, so a Node harness drives it frame by frame and asserts on jump-and-land, swim thresholds, climb auto-clear, and stamina drain. Replaying identical input twice and checking for identical output proves determinism, and a regression test pins the descent gate that stops a horizontal walk into a steep face from sliding the player backward.</p>
<p><strong>Body-agnostic avatar binding with a testable picker.</strong> Controllers report a state-name string and the visual layer maps it to a clip, so swapping a capsule for a retargeted 3MIKE + UAL avatar touches zero locomotion code. Synty POLYGON clips share bone names with the rig and play with no retargeting (Three.js binds tracks by bone name), unlike the UAL path's heavyweight retargeting. The clip picker is extracted into pure functions with named constants for jump speed buckets, landing severity, slope-projection variants, an always-forward idle bridge, a stop-bridge minimum-time guard, and a fall-state debouncer, so locomotion feel is unit-testable rather than guessed.</p>
<hr>
<p>Part 29 of 30.
Previous: <a href="/blog/2026-05-14-open-world-browser-part-28-grass-and-occlusion.html">Part 28 - Grass to the horizon, and ground that hides itself</a>
Next: <a href="/blog/2026-06-04-open-world-browser-part-30-collision-aware-camera.html">Part 30 - A camera that respects walls</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 28: Grass to the horizon, and ground that hides itself]]></title>
            <link>https://app.cinevva.com/blog/2026-05-14-open-world-browser-part-28-grass-and-occlusion</link>
            <guid>https://app.cinevva.com/blog/2026-05-14-open-world-browser-part-28-grass-and-occlusion</guid>
            <pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 56 makes half a million grass clumps read as a field instead of green confetti, with one compute kernel and a cross-quad trick. Spike 57 benches four ways to cull what a hill is hiding, and finds the accelerated path is also the geometrically correct one.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-28-grass-to-the-horizon-and-ground-that-hides-itself" tabindex="-1">Building an open world in the browser, part 28: Grass to the horizon, and ground that hides itself <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-28-grass-to-the-horizon-and-ground-that-hides-itself" aria-label="Permalink to &quot;Building an open world in the browser, part 28: Grass to the horizon, and ground that hides itself&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-13-open-world-browser-part-27-island-and-terrain.html">Part 27</a> built the island and gave it ground that doesn't look tiled. This part covers the two things that make terrain feel inhabited rather than empty. Spike 56 is grass, the surface detail that turns a textured slope into a place you'd walk through, and the problem is making a field read as a field instead of scattered streaks. Spike 57 is the opposite of drawing more: it's about not drawing what a hill is already hiding, and the interesting result is that the fast way to test for it turned out to be the correct way too.</p>
<h2 id="grass-that-reads-as-a-field-not-confetti" tabindex="-1">Grass that reads as a field, not confetti <a class="header-anchor" href="#grass-that-reads-as-a-field-not-confetti" aria-label="Permalink to &quot;Grass that reads as a field, not confetti&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/56-grass/" title="Spike 56 Grass" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/56-grass/" target="_blank">Open Spike 56 in a new tab ↗</a> · <a href="/blog/spike-source?spike=56-grass">View source</a></p>
<p>The core decision is geometric. A single thin tapered blade is sub-pixel from most camera angles, so half a million flat blades read as sparse green confetti rather than a field. The fix is a cross-quad clump: three tapered quads rotated sixty degrees apart around the local up axis, so from any viewing direction at least one quad sits nearly perpendicular to the camera and each instance covers roughly three blade-widths of actual screen area. That's the difference between seeing green streaks and seeing grass.</p>
<p>A WebGPU compute kernel places every clump once at init. It hashes the instance index into five decorrelated random streams, picks an XZ position inside the patch, samples ground height from the same FBM the CPU ground mesh uses (a TSL port with sign-preserving mod so the values match exactly and blades sit on the surface rather than floating above it), takes a central-difference normal, and rolls a per-clump width, height, and hue. The render side is a TSL vertex graph that bypasses the instance matrix entirely and writes straight to clip space: it scales the unit cross-quad, rotates local up onto the ground normal with Rodrigues' formula, and translates to the clump position. Distance LOD comes for free without a culling pass, because the vertex graph multiplies the clump's height by <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="38.925ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 17204.8 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(1722.4,0)"><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" style="stroke-width: 3;"/><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(394,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(1227,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(1727,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(2227,0)" style="stroke-width: 3;"/><path data-c="68" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 124T102 167T103 217T103 272T103 329Q103 366 103 407T103 482T102 542T102 586T102 603Q99 622 88 628T43 637H25V660Q25 683 27 683L37 684Q47 685 66 686T103 688Q120 689 140 690T170 693T181 694H184V367Q244 442 328 442Q451 442 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(2616,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(3172,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(3566,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(3955,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(4399,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6677.4,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(7066.4,0)"><path data-c="66" d="M273 0Q255 3 146 3Q43 3 34 0H26V46H42Q70 46 91 49Q99 52 103 60Q104 62 104 224V385H33V431H104V497L105 564L107 574Q126 639 171 668T266 704Q267 704 275 704T289 705Q330 702 351 679T372 627Q372 604 358 590T321 576T284 590T270 627Q270 647 288 667H284Q280 668 273 668Q245 668 223 647T189 592Q183 572 182 497V431H293V385H185V225Q185 63 186 61T189 57T194 54T199 51T206 49T213 48T222 47T231 47T241 46T251 46H282V0H273Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(306,0)" style="stroke-width: 3;"/><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" transform="translate(806,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1362,0)" style="stroke-width: 3;"/><path data-c="4E" d="M42 46Q74 48 94 56T118 69T128 86V634H124Q114 637 52 637H25V683H232L235 680Q237 679 322 554T493 303L578 178V598Q572 608 568 613T544 627T492 637H475V683H483Q498 680 600 680Q706 680 715 683H724V637H707Q634 633 622 598L621 302V6L614 0H600Q585 0 582 3T481 150T282 443T171 605V345L172 86Q183 50 257 46H274V0H265Q250 3 150 3Q48 3 33 0H25V46H42Z" transform="translate(1806,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(2556,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(3000,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(3500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(10958.4,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(11403.1,0)"><path data-c="66" d="M273 0Q255 3 146 3Q43 3 34 0H26V46H42Q70 46 91 49Q99 52 103 60Q104 62 104 224V385H33V431H104V497L105 564L107 574Q126 639 171 668T266 704Q267 704 275 704T289 705Q330 702 351 679T372 627Q372 604 358 590T321 576T284 590T270 627Q270 647 288 667H284Q280 668 273 668Q245 668 223 647T189 592Q183 572 182 497V431H293V385H185V225Q185 63 186 61T189 57T194 54T199 51T206 49T213 48T222 47T231 47T241 46T251 46H282V0H273Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(306,0)" style="stroke-width: 3;"/><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" transform="translate(806,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1362,0)" style="stroke-width: 3;"/><path data-c="46" d="M128 619Q121 626 117 628T101 631T58 634H25V680H582V676Q584 670 596 560T610 444V440H570V444Q563 493 561 501Q555 538 543 563T516 601T477 622T431 631T374 633H334H286Q252 633 244 631T233 621Q232 619 232 490V363H284Q287 363 303 363T327 364T349 367T372 373T389 385Q407 403 410 459V480H450V200H410V221Q407 276 389 296Q381 303 371 307T348 313T327 316T303 317T284 317H232V189L233 61Q240 54 245 52T270 48T333 46H360V0H348Q324 3 182 3Q51 3 36 0H25V46H58Q100 47 109 49T128 61V619Z" transform="translate(1806,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(2459,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(2959,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(14754.1,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(15198.8,0)"><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(834,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(1228,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(16815.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>1</mn><mo>−</mo><mtext>smoothstep</mtext><mo stretchy="false">(</mo><mtext>fadeNear</mtext><mo>,</mo><mtext>fadeFar</mtext><mo>,</mo><mtext>dist</mtext><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>, so far clumps collapse flat to zero height and stop costing fill. Wind is two octaves of sin and cos over the clump's world XZ plus time, applied horizontally and gated by the height fraction so the base stays anchored while the tips move, and a tiny camera-aware lean tips each clump toward the viewer so they open up perspectivally instead of reading as flat cards.</p>
<p>The color recipe is borrowed from NedMakesGames' Breath of the Wild URP shader: flat shading with the blade color a lerp from a root tone to a tip tone along the height fraction, where the root and tip tones are themselves lerped between two palettes by the per-clump hue value. That gives the dappled two-tone look a real BotW grass field has. Diffuse uses the ground normal against the sun with an ambient floor, because the cross-quad's per-quad normals are too noisy to shade individually for a stylized look. The whole field is one draw, placed and animated entirely on the GPU.</p>
<h2 id="four-ways-to-cull-what-a-hill-is-hiding" tabindex="-1">Four ways to cull what a hill is hiding <a class="header-anchor" href="#four-ways-to-cull-what-a-hill-is-hiding" aria-label="Permalink to &quot;Four ways to cull what a hill is hiding&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/57-terrain-occlusion/" title="Spike 57 Terrain Occlusion" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/57-terrain-occlusion/" target="_blank">Open Spike 57 in a new tab ↗</a> · <a href="/blog/spike-source?spike=57-terrain-occlusion">View source</a></p>
<p>Spike 57 benches four culling paths over the same procedural world the production client streams, scaled 1.8x vertically so hills actually occlude foliage. T0 is distance-only, matching what the shipping chunk visibility already does. T1 adds frustum culling, the single biggest cheap win, dropping everything outside the view cone. T2 adds a terrain-aware horizon test: march a ray from the eye to each instance and reject it if the heightmap rises above the ray anywhere along the way, so a tree behind a ridge gets culled even though it's inside the frustum. T3 keeps the same horizon test but accelerates it with a max-height pyramid, and T4 ports the whole distance-plus-frustum-plus-horizon test to a TSL compute kernel that writes a per-instance visibility scale the foliage material reads to collapse hidden vertices. A two-frame stay-visible counter smooths the single-frame flicker when a sample point lands just inside or outside a texel as the camera nudges, kept short on purpose because anything longer masks algorithmic bugs instead of fixing them.</p>
<p>The pyramid is where the spike earns its keep, and the lesson is about matching the test to what's actually on screen. The rendered terrain is a triangle mesh with vertices on a fixed two-meter grid, and between vertices the rasterizer interpolates linearly, so the real rendered height inside any rectangle is the max of the vertices inside it, never the continuous noise peak between them. If the occlusion pyramid samples the noise at a finer grid, it finds phantom peaks the mesh never displays and starts blocking rays the camera can plainly see through. So the pyramid's base texels sit exactly on the vertex grid, each storing the max of its four corner vertices, and upper levels are textbook 2x2 max reductions, which makes it exact with respect to the rendered mesh. For the per-step ray test the code deliberately point-samples the mesh with bilinear interpolation rather than querying the pyramid, because a sub-texel AABB query returns the whole texel's max and inflates the height by several meters on steep ridges, which is exactly the over-occlusion that would hide visible props.</p>
<p>The surprising result is that T2, the brute-force reference, is the one that's wrong. Because T2 point-samples the continuous noise directly, it catches peaks between mesh vertices that don't render, so it over-occludes slightly and hides foliage the player can actually see. The pyramid path is both faster, because of the <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="8.764ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 3873.7 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D442" d="M740 435Q740 320 676 213T511 42T304 -22Q207 -22 138 35T51 201Q50 209 50 244Q50 346 98 438T227 601Q351 704 476 704Q514 704 524 703Q621 689 680 617T740 435ZM637 476Q637 565 591 615T476 665Q396 665 322 605Q242 542 200 428T157 216Q157 126 200 73T314 19Q404 19 485 98T608 313Q637 408 637 476Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(763,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1152,0)"><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(278,0)" style="stroke-width: 3;"/><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" transform="translate(778,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2430,0)"><path data-c="2061" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2596.7,0)"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3484.7,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>O</mi><mo stretchy="false">(</mo><mi>log</mi><mo data-mjx-texclass="NONE">⁡</mo><mi>N</mi><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container> AABB reduction for chunk-level tests, and more geometrically correct, because it can only ever return heights the mesh truly displays. That's the spike's whole point: the accelerated structure isn't a quality compromise you accept for speed, it's the version that matches reality. Chunk culling runs a five-point test per chunk (four top corners plus center at the chunk's max height) and excludes each chunk's own footprint from the occluder set so a chunk never occludes itself. The recommended production path is T4: push the instance metadata and height field into storage buffers and run the identical loop in a compute shader with an indirect draw, since the renderer is moving to WebGPU anyway.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Cross-quad GPU grass.</strong> Three tapered quads rotated sixty degrees apart per clump guarantee one near-perpendicular quad from any angle, so half a million instances read as a field rather than sub-pixel confetti. A compute kernel places every clump (FBM ground height sampled with the same sign-preserving mod as the CPU mesh, central-difference normal, per-clump size and hue), and a TSL vertex graph bypasses the instance matrix to scale, Rodrigues-rotate onto the normal, translate, and apply height-gated wind. Distance LOD is free: clumps collapse to zero height by <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="38.925ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 17204.8 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(1722.4,0)"><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" style="stroke-width: 3;"/><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(394,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(1227,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(1727,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(2227,0)" style="stroke-width: 3;"/><path data-c="68" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 124T102 167T103 217T103 272T103 329Q103 366 103 407T103 482T102 542T102 586T102 603Q99 622 88 628T43 637H25V660Q25 683 27 683L37 684Q47 685 66 686T103 688Q120 689 140 690T170 693T181 694H184V367Q244 442 328 442Q451 442 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(2616,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(3172,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(3566,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(3955,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(4399,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6677.4,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(7066.4,0)"><path data-c="66" d="M273 0Q255 3 146 3Q43 3 34 0H26V46H42Q70 46 91 49Q99 52 103 60Q104 62 104 224V385H33V431H104V497L105 564L107 574Q126 639 171 668T266 704Q267 704 275 704T289 705Q330 702 351 679T372 627Q372 604 358 590T321 576T284 590T270 627Q270 647 288 667H284Q280 668 273 668Q245 668 223 647T189 592Q183 572 182 497V431H293V385H185V225Q185 63 186 61T189 57T194 54T199 51T206 49T213 48T222 47T231 47T241 46T251 46H282V0H273Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(306,0)" style="stroke-width: 3;"/><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" transform="translate(806,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1362,0)" style="stroke-width: 3;"/><path data-c="4E" d="M42 46Q74 48 94 56T118 69T128 86V634H124Q114 637 52 637H25V683H232L235 680Q237 679 322 554T493 303L578 178V598Q572 608 568 613T544 627T492 637H475V683H483Q498 680 600 680Q706 680 715 683H724V637H707Q634 633 622 598L621 302V6L614 0H600Q585 0 582 3T481 150T282 443T171 605V345L172 86Q183 50 257 46H274V0H265Q250 3 150 3Q48 3 33 0H25V46H42Z" transform="translate(1806,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(2556,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(3000,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(3500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(10958.4,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(11403.1,0)"><path data-c="66" d="M273 0Q255 3 146 3Q43 3 34 0H26V46H42Q70 46 91 49Q99 52 103 60Q104 62 104 224V385H33V431H104V497L105 564L107 574Q126 639 171 668T266 704Q267 704 275 704T289 705Q330 702 351 679T372 627Q372 604 358 590T321 576T284 590T270 627Q270 647 288 667H284Q280 668 273 668Q245 668 223 647T189 592Q183 572 182 497V431H293V385H185V225Q185 63 186 61T189 57T194 54T199 51T206 49T213 48T222 47T231 47T241 46T251 46H282V0H273Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(306,0)" style="stroke-width: 3;"/><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" transform="translate(806,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1362,0)" style="stroke-width: 3;"/><path data-c="46" d="M128 619Q121 626 117 628T101 631T58 634H25V680H582V676Q584 670 596 560T610 444V440H570V444Q563 493 561 501Q555 538 543 563T516 601T477 622T431 631T374 633H334H286Q252 633 244 631T233 621Q232 619 232 490V363H284Q287 363 303 363T327 364T349 367T372 373T389 385Q407 403 410 459V480H450V200H410V221Q407 276 389 296Q381 303 371 307T348 313T327 316T303 317T284 317H232V189L233 61Q240 54 245 52T270 48T333 46H360V0H348Q324 3 182 3Q51 3 36 0H25V46H58Q100 47 109 49T128 61V619Z" transform="translate(1806,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(2459,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(2959,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(14754.1,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(15198.8,0)"><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(834,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(1228,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(16815.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>1</mn><mo>−</mo><mtext>smoothstep</mtext><mo stretchy="false">(</mo><mtext>fadeNear</mtext><mo>,</mo><mtext>fadeFar</mtext><mo>,</mo><mtext>dist</mtext><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>. Color follows NedMakesGames' BotW recipe, a root-to-tip gradient over two hue-mixed palettes. See <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">GPU-driven LOD</a>.</p>
<p><strong>Terrain horizon occlusion culling.</strong> Four benched paths over the production world: distance, plus frustum, plus a heightmap ray test that rejects instances behind a ridge, plus a max-height pyramid that accelerates it, plus a TSL compute port. The pyramid samples on the exact two-meter vertex grid the mesh tessellates on (base texel = max of four corner vertices, 2x2 max-reduce upward), so it returns only heights the rasterizer actually displays.</p>
<p><strong>Accelerated and correct, not a trade.</strong> Point-sampling continuous noise (the brute-force T2 path) finds peaks between mesh vertices that never render, over-occluding visible foliage. The vertex-grid pyramid is both faster (<mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="8.764ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 3873.7 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D442" d="M740 435Q740 320 676 213T511 42T304 -22Q207 -22 138 35T51 201Q50 209 50 244Q50 346 98 438T227 601Q351 704 476 704Q514 704 524 703Q621 689 680 617T740 435ZM637 476Q637 565 591 615T476 665Q396 665 322 605Q242 542 200 428T157 216Q157 126 200 73T314 19Q404 19 485 98T608 313Q637 408 637 476Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(763,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1152,0)"><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(278,0)" style="stroke-width: 3;"/><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" transform="translate(778,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2430,0)"><path data-c="2061" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2596.7,0)"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3484.7,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>O</mi><mo stretchy="false">(</mo><mi>log</mi><mo data-mjx-texclass="NONE">⁡</mo><mi>N</mi><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container> AABB reduction) and geometrically exact, so per-step ray tests bilinear-sample the mesh rather than querying the pyramid's per-texel max, which would inflate heights by meters on steep ridges. Chunk culling uses a five-point test and excludes each chunk's own footprint so it never self-occludes. The production path pushes metadata and the height field into storage buffers for a compute-shader cull with indirect draw.</p>
<hr>
<p>Part 28 of 29.
Previous: <a href="/blog/2026-05-13-open-world-browser-part-27-island-and-terrain.html">Part 27 - An island from noise, ground that looks like ground</a>
Next: <a href="/blog/2026-05-15-open-world-browser-part-29-pluggable-character.html">Part 29 - One controller, any body</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 27: An island from noise, ground that looks like ground]]></title>
            <link>https://app.cinevva.com/blog/2026-05-13-open-world-browser-part-27-island-and-terrain</link>
            <guid>https://app.cinevva.com/blog/2026-05-13-open-world-browser-part-27-island-and-terrain</guid>
            <pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 54 grows a believable island from noise alone: domain-warped FBM shaped by a radial falloff, then hydraulic erosion that carves real drainage, then biomes assigned from elevation and moisture. Spike 55 fixes the oldest texture problem there is, the tile that repeats, with four sampling modes and the one that actually wins.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-27-an-island-from-noise-ground-that-looks-like-ground" tabindex="-1">Building an open world in the browser, part 27: An island from noise, ground that looks like ground <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-27-an-island-from-noise-ground-that-looks-like-ground" aria-label="Permalink to &quot;Building an open world in the browser, part 27: An island from noise, ground that looks like ground&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-12-open-world-browser-part-26-water.html">Part 26</a> put water on the world. This part builds the land under it, in two stages that mirror how a real place comes to be. Spike 54 is the terrain itself: not sculpted by hand but grown from noise, then weathered until it has the drainage and the coastline of somewhere that water has actually run across. Spike 55 is the skin on that terrain: solving the problem that defeats almost every procedural ground, which is that a tiled texture looks tiled. The two together answer one question, can a creator get a plausible island and ground that reads as ground without an artist touching either, and the answer is yes if you copy the right recipes.</p>
<h2 id="a-coastline-that-noise-alone-won-t-give-you" tabindex="-1">A coastline that noise alone won't give you <a class="header-anchor" href="#a-coastline-that-noise-alone-won-t-give-you" aria-label="Permalink to &quot;A coastline that noise alone won't give you&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/54-procgen-island/" title="Spike 54 Procedural Island" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/54-procgen-island/" target="_blank">Open Spike 54 in a new tab ↗</a> · <a href="/blog/spike-source?spike=54-procgen-island">View source</a></p>
<p>The heightmap follows the Red Blob Games recipes, which are a stack of small transforms each fixing a specific flaw in raw noise. Fractional Brownian motion sums several octaves of Perlin noise so the terrain has both broad hills and fine detail, but plain FBM looks soft and grid-aligned, so the sample point gets domain-warped first: you offset each coordinate by another noise field, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="19.194ex" height="2.283ex" role="img" focusable="false" viewBox="0 -759 8483.9 1009" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(536,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1058.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2114,0)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2839.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(3839.5,0)"><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(1056,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(1334,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1728,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6011.5,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6400.5,0)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6903.5,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(7514.7,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(8014.9,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>p</mi><mo data-mjx-alternate="1">′</mo></msup><mo>=</mo><mi>p</mi><mo>+</mo><mtext>noise</mtext><mo stretchy="false">(</mo><mi>p</mi><mo stretchy="false">)</mo><mo>⋅</mo><mi>s</mi></math></mjx-assistive-mml></mjx-container>, which bends the ridges into something organic instead of axis-locked. Then redistribution, raising elevation to a power <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.453ex" height="1.956ex" role="img" focusable="false" viewBox="0 -853.7 1084.4 864.7" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(499,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mstyle" transform="scale(1.414)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(236.2,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>e</mi><mrow data-mjx-texclass="ORD"><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mi>k</mi></mrow></msup></math></mjx-assistive-mml></mjx-container>, pushes the midtones down so the world has flat valleys and sharp peaks rather than everything sitting at the same gentle slope. The piece that makes it an island rather than infinite hills is a radial falloff: compute a euclidean distance from center, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -1.467ex;" xmlns="http://www.w3.org/2000/svg" width="21.741ex" height="4.208ex" role="img" focusable="false" viewBox="0 -1211.5 9609.7 1860" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(797.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1853.6,0)"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1111,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3520.6,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3909.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4409.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="msqrt" transform="translate(4854.2,0)"><g transform="translate(1020,0)"><g data-mml-node="msubsup"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(633,289) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(633,-247) scale(0.707)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1309.7,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msubsup" transform="translate(2309.9,0)"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(633,289) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(633,-247) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(0,1.5)"><path data-c="221A" d="M1001 1150Q1017 1150 1020 1132Q1020 1127 741 244L460 -643Q453 -650 436 -650H424Q423 -647 423 -645T421 -640T419 -631T415 -617T408 -594T399 -560T385 -512T367 -448T343 -364T312 -259L203 119L138 41L111 67L212 188L264 248L472 -474L983 1140Q988 1150 1001 1150Z" style="stroke-width: 3;"/></g><rect width="3346.5" height="60" x="1020" y="1091.5"/></g><g data-mml-node="mo" transform="translate(9220.7,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>d</mi><mo>=</mo><mo data-mjx-texclass="OP" movablelimits="true">min</mo><mo stretchy="false">(</mo><mn>1</mn><mo>,</mo><msqrt><msubsup><mi>n</mi><mi>x</mi><mn>2</mn></msubsup><mo>+</mo><msubsup><mi>n</mi><mi>y</mi><mn>2</mn></msubsup></msqrt><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>, and blend the elevation toward <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.073ex" height="1.756ex" role="img" focusable="false" viewBox="0 -694 2242.4 776" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1722.4,0)"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>1</mn><mo>−</mo><mi>d</mi></math></mjx-assistive-mml></mjx-container> so the terrain drops into ocean at the edges. A second independent noise field becomes a moisture map, which does nothing for the shape but feeds the biome step later.</p>
<p>That gets you a shape, but it's a noise shape. It has no rivers, no valleys cut by water, no sediment fans, because nothing has ever flowed across it. The fix is hydraulic erosion, ported from Sebastian Lague's MIT-licensed implementation. Thousands of water droplets spawn at random and roll downhill, each carrying inertia so it doesn't make hard right-angle turns, picking up sediment when it speeds up and depositing it when it slows or pools, with a precomputed circular brush spreading each erosion event over a small radius so the carve is smooth rather than a one-pixel scratch, and evaporation that retires the droplet over its lifetime. Run enough droplets and the terrain grows the thing noise can't fake: drainage networks that converge, valleys that widen downstream, and flats where sediment settled.</p>
<h2 id="erosion-on-the-gpu-and-rivers-that-know-where-they-are" tabindex="-1">Erosion on the GPU, and rivers that know where they are <a class="header-anchor" href="#erosion-on-the-gpu-and-rivers-that-know-where-they-are" aria-label="Permalink to &quot;Erosion on the GPU, and rivers that know where they are&quot;"></a></h2>
<p>The CPU erosion is correct but slow, so the spike also ports it to a WebGPU compute shader, and the interesting part is what GPU atomics force on you. Heights are stored as fixed-point <code>i32</code> with a scale of one million, because WGSL has no atomic add on floats, and the only way thousands of droplets can deposit and remove sediment from the same cell in parallel without races is <code>atomicAdd</code> and <code>atomicSub</code> on integers. The fixed-point round-trip means a cell can occasionally go slightly negative under heavy concurrent drag, which is harmless and clamped on readback. The CPU version precomputes a per-cell brush table, but at a <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="5.513ex" height="2.005ex" role="img" focusable="false" viewBox="0 -864 2436.6 886" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" transform="translate(1500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2033,393.1) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mn>1001</mn><mn>2</mn></msup></math></mjx-assistive-mml></mjx-container> grid that table is around 100 MB, so the GPU port recomputes each droplet's brush on the fly instead, trading a little arithmetic for a lot of memory. Droplet start positions still come from the CPU through a Mulberry32 generator so runs are deterministic.</p>
<p>Two more passes turn weathered terrain into a readable map. Drainage uses the Red Blob D8 flow-accumulation method: drop one unit of rain on every cell, sort all cells by elevation descending, and pass each cell's accumulated water to its single lowest neighbor, so water piles up along the natural valleys, and any cell whose accumulation crosses a threshold is marked river. The catch is that an eroded heightmap has pits, local depressions with no downhill exit, and water entering a pit just stops, breaking the accumulation. So before drainage runs, Planchon-Darboux pit filling raises each pit to just above its lowest neighbor, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.466ex;" xmlns="http://www.w3.org/2000/svg" width="17.709ex" height="2.036ex" role="img" focusable="false" viewBox="0 -694 7827.4 900" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mtext"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1111,0)" style="stroke-width: 3;"/><path data-c="4E" d="M42 46Q74 48 94 56T118 69T128 86V634H124Q114 637 52 637H25V683H232L235 680Q237 679 322 554T493 303L578 178V598Q572 608 568 613T544 627T492 637H475V683H483Q498 680 600 680Q706 680 715 683H724V637H707Q634 633 622 598L621 302V6L614 0H600Q585 0 582 3T481 150T282 443T171 605V345L172 86Q183 50 257 46H274V0H265Q250 3 150 3Q48 3 33 0H25V46H42Z" transform="translate(1667,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(2417,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(2861,0)" style="stroke-width: 3;"/><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" transform="translate(3139,0)" style="stroke-width: 3;"/><path data-c="68" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 124T102 167T103 217T103 272T103 329Q103 366 103 407T103 482T102 542T102 586T102 603Q99 622 88 628T43 637H25V660Q25 683 27 683L37 684Q47 685 66 686T103 688Q120 689 140 690T170 693T181 694H184V367Q244 442 328 442Q451 442 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(3639,0)" style="stroke-width: 3;"/><path data-c="62" d="M307 -11Q234 -11 168 55L158 37Q156 34 153 28T147 17T143 10L138 1L118 0H98V298Q98 599 97 603Q94 622 83 628T38 637H20V660Q20 683 22 683L32 684Q42 685 61 686T98 688Q115 689 135 690T165 693T176 694H179V543Q179 391 180 391L183 394Q186 397 192 401T207 411T228 421T254 431T286 439T323 442Q401 442 461 379T522 216Q522 115 458 52T307 -11ZM182 98Q182 97 187 90T196 79T206 67T218 55T233 44T250 35T271 29T295 26Q330 26 363 46T412 113Q424 148 424 212Q424 287 412 323Q385 405 300 405Q270 405 239 390T188 347L182 339V98Z" transform="translate(4195,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(4751,0)" style="stroke-width: 3;"/><path data-c="75" d="M383 58Q327 -10 256 -10H249Q124 -10 105 89Q104 96 103 226Q102 335 102 348T96 369Q86 385 36 385H25V408Q25 431 27 431L38 432Q48 433 67 434T105 436Q122 437 142 438T172 441T184 442H187V261Q188 77 190 64Q193 49 204 40Q224 26 264 26Q290 26 311 35T343 58T363 90T375 120T379 144Q379 145 379 161T380 201T380 248V315Q380 361 370 372T320 385H302V431Q304 431 378 436T457 442H464V264Q464 84 465 81Q468 61 479 55T524 46H542V0Q540 0 467 -5T390 -11H383V58Z" transform="translate(5251,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(5807,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6421.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(7421.4,0)"><path data-c="1D716" d="M227 -11Q149 -11 95 41T40 174Q40 262 87 322Q121 367 173 396T287 430Q289 431 329 431H367Q382 426 382 411Q382 385 341 385H325H312Q191 385 154 277L150 265H327Q340 256 340 246Q340 228 320 219H138V217Q128 187 128 143Q128 77 160 52T231 26Q258 26 284 36T326 57T343 68Q350 68 354 58T358 39Q358 36 357 35Q354 31 337 21T289 0T227 -11Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mtext>minNeighbour</mtext><mo>+</mo><mi>ϵ</mi></math></mjx-assistive-mml></mjx-container>, iterating under twenty passes until every cell has somewhere to drain. Finally biomes are assigned the Red Blob way, a two-axis lookup on elevation against moisture so a high-and-dry cell becomes rock while a low-and-wet one becomes marsh, with slope-aware overrides on top: steep faces force to rock regardless of moisture, the waterline band becomes beach, and the highest elevations take snow. The result is an island you can read at a glance, generated end to end from two noise seeds.</p>
<h2 id="the-tile-that-won-t-stop-repeating" tabindex="-1">The tile that won't stop repeating <a class="header-anchor" href="#the-tile-that-won-t-stop-repeating" aria-label="Permalink to &quot;The tile that won't stop repeating&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/55-terrain-shading/" title="Spike 55 Terrain Shading" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/55-terrain-shading/" target="_blank">Open Spike 55 in a new tab ↗</a> · <a href="/blog/spike-source?spike=55-terrain-shading">View source</a></p>
<p>A terrain needs a material that covers kilometers from a texture that's a couple of meters across, and the moment you scale a tile up to cover ground, the eye locks onto the repetition. Spike 55 is an A/B bench of four sampling modes on the same geometry and lighting, so the only variable is how the texture is read. The baseline is <code>plain</code>: one sample at the scaled UV, which tiles obviously every few meters and exists only as the thing to beat. The free PBR layers come from Poly Haven over their CDN, diffuse plus the packed ARM map (ambient occlusion, roughness, metalness) plus a GL normal, all CC0, loaded with anisotropy 8 and mipmaps.</p>
<p>The interesting three are different attacks on the repeat. The <code>hex-linear</code> mode is the Heitz-Neyret triangle grid: lay a skewed triangular lattice over the surface, and each fragment sits inside one triangle whose three vertices each take a randomly offset tap, blended by barycentric weights so neighboring regions sample different parts of the texture and the large-scale tiling dissolves. But a plain linear blend of three taps averages their colors at the triangle edges, which leaves a visible triangular pattern, the exact artifact spike 48 ran into. The <code>hex-vp</code> mode is the published fix from the same 2018 paper: after the barycentric sum, subtract the texture's mean color, rescale by the inverse square root of the sum of squared weights to hold variance constant, then add the mean back. That keeps contrast steady across the blend so the triangles vanish, which is why the material does a 1-by-1 readback of each loaded texture to estimate its mean color up front. The fourth mode, <code>iq-untiled</code>, is Inigo Quilez's Texture Repetition technique 3: two offset taps blended by a low-frequency variation pattern, only two samples instead of three and no triangle structure to begin with, the cheap option that holds up.</p>
<p>On top of any mode sits optional triplanar projection, which is what lets the same material wrap a cliff without stretching. Instead of one UV, the shader samples three world-axis projections, the X-facing surfaces read the YZ plane, Y reads ZX, Z reads XY, and blends them by <code>abs(normalWorld)</code> raised to a sharpening power between four and eight so a 45-degree slope doesn't ghost all three projections into mush. The bench makes the tradeoff legible: variance-preserving hex blending is the quality winner and Quilez's two-tap is the performance-conscious one, and both beat plain by a wide enough margin that no creator should ever ship the plain tile.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Recipe-stacked heightmap generation.</strong> Red Blob Games recipes compose raw noise into terrain: domain-warped FBM (<mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="19.194ex" height="2.283ex" role="img" focusable="false" viewBox="0 -759 8483.9 1009" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(536,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1058.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2114,0)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2839.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(3839.5,0)"><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(1056,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(1334,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1728,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6011.5,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6400.5,0)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6903.5,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(7514.7,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(8014.9,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>p</mi><mo data-mjx-alternate="1">′</mo></msup><mo>=</mo><mi>p</mi><mo>+</mo><mtext>noise</mtext><mo stretchy="false">(</mo><mi>p</mi><mo stretchy="false">)</mo><mo>⋅</mo><mi>s</mi></math></mjx-assistive-mml></mjx-container>) for organic ridges, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.453ex" height="1.956ex" role="img" focusable="false" viewBox="0 -853.7 1084.4 864.7" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(499,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mstyle" transform="scale(1.414)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(236.2,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>e</mi><mrow data-mjx-texclass="ORD"><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mi>k</mi></mrow></msup></math></mjx-assistive-mml></mjx-container> redistribution for flat valleys and sharp peaks, and a euclidean radial falloff blended toward <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.073ex" height="1.756ex" role="img" focusable="false" viewBox="0 -694 2242.4 776" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1722.4,0)"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>1</mn><mo>−</mo><mi>d</mi></math></mjx-assistive-mml></mjx-container> to drop the edges into ocean and make an island. A second noise field is the moisture map for biome assignment. See <a href="/guides/landscape-generation-browser.html">landscape generation</a>.</p>
<p><strong>Hydraulic erosion, CPU and GPU.</strong> Sebastian Lague's droplet model (inertia, carrying capacity, a precomputed circular deposit brush, evaporation) carves real drainage that noise can't fake. The WebGPU compute port stores heights as fixed-point <code>i32</code> at a scale of one million so droplets can <code>atomicAdd</code>/<code>atomicSub</code> the same cell in parallel, recomputes each droplet's brush on the fly to avoid a ~100 MB table at <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="5.513ex" height="2.005ex" role="img" focusable="false" viewBox="0 -864 2436.6 886" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" transform="translate(1500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2033,393.1) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mn>1001</mn><mn>2</mn></msup></math></mjx-assistive-mml></mjx-container>, and seeds start positions from a deterministic Mulberry32 generator.</p>
<p><strong>D8 drainage with pit filling.</strong> Red Blob D8 flow accumulation drops one unit of rain per cell, sorts cells by descending elevation, and passes water to each cell's lowest neighbor, marking rivers above a threshold. Planchon-Darboux pit filling first raises every local depression to <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.466ex;" xmlns="http://www.w3.org/2000/svg" width="17.709ex" height="2.036ex" role="img" focusable="false" viewBox="0 -694 7827.4 900" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mtext"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1111,0)" style="stroke-width: 3;"/><path data-c="4E" d="M42 46Q74 48 94 56T118 69T128 86V634H124Q114 637 52 637H25V683H232L235 680Q237 679 322 554T493 303L578 178V598Q572 608 568 613T544 627T492 637H475V683H483Q498 680 600 680Q706 680 715 683H724V637H707Q634 633 622 598L621 302V6L614 0H600Q585 0 582 3T481 150T282 443T171 605V345L172 86Q183 50 257 46H274V0H265Q250 3 150 3Q48 3 33 0H25V46H42Z" transform="translate(1667,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(2417,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(2861,0)" style="stroke-width: 3;"/><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" transform="translate(3139,0)" style="stroke-width: 3;"/><path data-c="68" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 124T102 167T103 217T103 272T103 329Q103 366 103 407T103 482T102 542T102 586T102 603Q99 622 88 628T43 637H25V660Q25 683 27 683L37 684Q47 685 66 686T103 688Q120 689 140 690T170 693T181 694H184V367Q244 442 328 442Q451 442 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(3639,0)" style="stroke-width: 3;"/><path data-c="62" d="M307 -11Q234 -11 168 55L158 37Q156 34 153 28T147 17T143 10L138 1L118 0H98V298Q98 599 97 603Q94 622 83 628T38 637H20V660Q20 683 22 683L32 684Q42 685 61 686T98 688Q115 689 135 690T165 693T176 694H179V543Q179 391 180 391L183 394Q186 397 192 401T207 411T228 421T254 431T286 439T323 442Q401 442 461 379T522 216Q522 115 458 52T307 -11ZM182 98Q182 97 187 90T196 79T206 67T218 55T233 44T250 35T271 29T295 26Q330 26 363 46T412 113Q424 148 424 212Q424 287 412 323Q385 405 300 405Q270 405 239 390T188 347L182 339V98Z" transform="translate(4195,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(4751,0)" style="stroke-width: 3;"/><path data-c="75" d="M383 58Q327 -10 256 -10H249Q124 -10 105 89Q104 96 103 226Q102 335 102 348T96 369Q86 385 36 385H25V408Q25 431 27 431L38 432Q48 433 67 434T105 436Q122 437 142 438T172 441T184 442H187V261Q188 77 190 64Q193 49 204 40Q224 26 264 26Q290 26 311 35T343 58T363 90T375 120T379 144Q379 145 379 161T380 201T380 248V315Q380 361 370 372T320 385H302V431Q304 431 378 436T457 442H464V264Q464 84 465 81Q468 61 479 55T524 46H542V0Q540 0 467 -5T390 -11H383V58Z" transform="translate(5251,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(5807,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6421.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(7421.4,0)"><path data-c="1D716" d="M227 -11Q149 -11 95 41T40 174Q40 262 87 322Q121 367 173 396T287 430Q289 431 329 431H367Q382 426 382 411Q382 385 341 385H325H312Q191 385 154 277L150 265H327Q340 256 340 246Q340 228 320 219H138V217Q128 187 128 143Q128 77 160 52T231 26Q258 26 284 36T326 57T343 68Q350 68 354 58T358 39Q358 36 357 35Q354 31 337 21T289 0T227 -11Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mtext>minNeighbour</mtext><mo>+</mo><mi>ϵ</mi></math></mjx-assistive-mml></mjx-container> (iterating under twenty passes) so water never gets trapped and accumulation stays connected. A two-axis elevation-by-moisture lookup assigns biomes, with slope overrides forcing rock on cliffs, beach at the waterline, and snow on peaks.</p>
<p><strong>Breaking texture tiling four ways.</strong> On identical geometry and lighting: <code>plain</code> (one tap, tiles obviously), <code>hex-linear</code> (Heitz-Neyret triangle grid, linear blend leaves a triangle pattern), <code>hex-vp</code> (the EGSR 2018 §3.3 variance-preserving fix that subtracts the mean, rescales by inverse-sqrt of squared weights, and re-adds the mean, needing a 1×1 mean readback per texture), and <code>iq-untiled</code> (Inigo Quilez two-tap technique 3). Triplanar projection samples three world-axis planes blended by <code>abs(normalWorld)</code> to a power of 4-8 so cliffs don't stretch. Variance-preserving wins on quality, two-tap on cost. See <a href="/guides/landscape-generation-browser.html#terrain-materials">terrain materials</a>.</p>
<hr>
<p>Part 27 of 29.
Previous: <a href="/blog/2026-05-12-open-world-browser-part-26-water.html">Part 26 - Three ways to make water</a>
Next: <a href="/blog/2026-05-14-open-world-browser-part-28-grass-and-occlusion.html">Part 28 - Grass to the horizon, and ground that hides itself</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 26: Three ways to make water]]></title>
            <link>https://app.cinevva.com/blog/2026-05-12-open-world-browser-part-26-water</link>
            <guid>https://app.cinevva.com/blog/2026-05-12-open-world-browser-part-26-water</guid>
            <pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 51 built screen-space reflection water and hit its fundamental wall: it can't reflect what the camera never saw. Spike 52 switched to a planar mirror, the pattern every shipping game uses, and killed a bug that ate dark reflections. Spike 53 dropped in a production water library to see what finished looks like.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-26-three-ways-to-make-water" tabindex="-1">Building an open world in the browser, part 26: Three ways to make water <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-26-three-ways-to-make-water" aria-label="Permalink to &quot;Building an open world in the browser, part 26: Three ways to make water&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-10-open-world-browser-part-25-universal-characters.html">Part 25</a> dressed the avatars. This part is about water, and it's three spikes because water is the surface where a cheap shortcut and the correct answer look identical in a screenshot and completely different in motion. Spike 51 builds reflection the screen-space way, the tempting one, and runs straight into its built-in limitation. Spike 52 switches to the method every shipping game actually uses. Spike 53 drops in a finished water library to see how far &quot;done&quot; is from where we are. All three share one refraction layer, so the only variable that moves between the first two is how the reflection gets computed.</p>
<h2 id="reflection-from-the-screen-you-already-have" tabindex="-1">Reflection from the screen you already have <a class="header-anchor" href="#reflection-from-the-screen-you-already-have" aria-label="Permalink to &quot;Reflection from the screen you already have&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/51-water-ssr-caustics/" title="Spike 51 Water SSR" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/51-water-ssr-caustics/" target="_blank">Open Spike 51 in a new tab ↗</a> · <a href="/blog/spike-source?spike=51-water-ssr-caustics">View source</a></p>
<p>Screen-space reflection reuses the frame you already rendered. For each water pixel, you reflect the view ray off the surface, march that reflected ray through the depth buffer, and when the ray passes behind a recorded surface you've found what the water reflects, sampled straight from the color buffer. The port follows three.js's <code>SSRNode</code> line by line, which in turn follows lettier's SSR primer. The march is a DDA walk in screen space: project the ray's start and end to pixel coordinates and step along whichever axis is longer, one tap per pixel. The reflected ray's depth at each step needs perspective-correct interpolation, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -1.238ex;" xmlns="http://www.w3.org/2000/svg" width="14.984ex" height="3.195ex" role="img" focusable="false" viewBox="0 -864.9 6622.8 1412" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mfrac"><g data-mml-node="mn" transform="translate(3134.6,394) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mrow" transform="translate(220,-370.3) scale(0.707)"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(500,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z" style="stroke-width: 3;"/></g></g><g data-mml-node="msub" transform="translate(1000,0)"><g data-mml-node="mi"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(498,-150) scale(0.707)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1901.6,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2679.6,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(3148.6,0) scale(1.414)"><g data-mml-node="mspace"/></g><g data-mml-node="mo" transform="translate(3384.7,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3773.7,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(4273.7,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z" style="stroke-width: 3;"/></g></g><g data-mml-node="msub" transform="translate(4773.7,0)"><g data-mml-node="mi"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(498,-150) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(5675.3,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(6453.3,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(6953.3,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z" style="stroke-width: 3;"/></g></g><g data-mml-node="msub" transform="translate(7453.3,0)"><g data-mml-node="mi"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(498,-150) scale(0.707)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(8354.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g><rect width="6382.8" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mfrac><mn>1</mn><mrow><mn>1</mn><mrow data-mjx-texclass="ORD"><mo>/</mo></mrow><msub><mi>z</mi><mn>0</mn></msub><mo>+</mo><mi>s</mi><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mo stretchy="false">(</mo><mn>1</mn><mrow data-mjx-texclass="ORD"><mo>/</mo></mrow><msub><mi>z</mi><mn>1</mn></msub><mo>−</mo><mn>1</mn><mrow data-mjx-texclass="ORD"><mo>/</mo></mrow><msub><mi>z</mi><mn>0</mn></msub><mo stretchy="false">)</mo></mrow></mfrac></math></mjx-assistive-mml></mjx-container>, because linearly interpolating view-Z is simply wrong and produces hits in the wrong place.</p>
<p>Two things make this usable rather than a slideshow. The coarse march is capped at 64 steps, because a long ray that projects across a thousand pixels would otherwise run hundreds of iterations per fragment, and a million-fragment water plane times hundreds of iterations times a few texture samples is a 30 fps scene. Quality controls the effective stride within that cap instead of the iteration count. And because a capped coarse march leaves visible stair-step banding, a six-iteration binary refinement bisects the interval between the last miss and the hit, which is 64x sub-step accuracy, enough that neighboring water fragments stop locking onto the same coarse hit position. A final point-to-line distance check confirms the candidate is genuinely on the reflection ray rather than just at the same depth, with a thickness tolerance that auto-scales to the view-space width of one pixel at that depth, tighter up close and looser far away.</p>
<p>The honest part of this spike is written into its own comments: SSR cannot reflect what the main camera never sampled. A tree's underside, anything off-screen, anything occluded, none of it exists in the buffers, so it can't appear in the reflection. That's the &quot;wrong-side information loss&quot; that no amount of march quality fixes, and it's exactly why the next spike exists.</p>
<h2 id="the-mirror-that-can-t-lie" tabindex="-1">The mirror that can't lie <a class="header-anchor" href="#the-mirror-that-can-t-lie" aria-label="Permalink to &quot;The mirror that can't lie&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/52-water-planar/" title="Spike 52 Water Planar" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/52-water-planar/" target="_blank">Open Spike 52 in a new tab ↗</a> · <a href="/blog/spike-source?spike=52-water-planar">View source</a></p>
<p>A planar reflection renders the scene a second time from a camera mirrored across the water plane, into an offscreen target, and the water shader samples that target. This is the canonical pattern, what UE5 Water, three.js's own <code>WaterMesh</code>, ABZÛ, and Sea of Thieves all use, because it has every pixel of the scene to draw from, including the geometry SSR can never see. In TSL it's almost anticlimactic: <code>reflector()</code> allocates the mirrored helper camera and its render target, you add its target to the mesh so it updates each frame, and you sample its color. When waves come later, the reflection wobbles by adding a distortion offset to the reflector's UV node, which is exactly the line <code>WaterMesh</code> uses.</p>
<p>The bug worth recording was in the safety net, not the mirror. An earlier version blended the reflector's output with a procedural sky as a fallback, weighted by the magnitude of the reflected color, on the theory that a near-zero reflection meant the target had nothing there. But a dark canopy shadow also has a low magnitude, so the clamp never reached full strength and those genuinely dark pixels got mixed with bright sky. The symptom the user caught was precise: the raw mirror target in debug mode showed perfect dark trees, while the composed render had washed-out reflections, and the diagnosis was that the shader &quot;vanishes dark colors.&quot; The fix was deletion. The reflector target is reliable after the first frame, so there's no need for a fallback at all. Both spikes share the same refraction underneath: sample the scene behind the surface, reconstruct how far below the waterline each pixel sits, and apply per-channel Beer-Lambert extinction so red dies within a few meters while blue persists, with a sky mask so the far-plane backdrop doesn't get fogged. Schlick Fresnel mixes refraction when you look straight down into the water with reflection when you look across it.</p>
<h2 id="what-finished-looks-like" tabindex="-1">What finished looks like <a class="header-anchor" href="#what-finished-looks-like" aria-label="Permalink to &quot;What finished looks like&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/53-water-pro/" title="Spike 53 Water Pro" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/53-water-pro/" target="_blank">Open Spike 53 in a new tab ↗</a> · <a href="/blog/spike-source?spike=53-water-pro">View source</a></p>
<p>Spike 53 is the build-versus-buy check. It drops in <code>threejs-water-pro</code> as it ships, with its tropical preset, default ocean clipmap, camera tracking, and Rayleigh sky, and loads the same island glTF the library's own demo uses. The point is to see the gap between a hand-rolled flat-water shader and a full ocean system, and the gap is large: this one has a buoyancy system that samples wave height at several points under a ship's hull so it pitches and rolls instead of just bobbing, a wake generator, shoreline and surface foam, and a mask pass that suppresses water rendering inside the ship's hull so ripples don't bleed through the deck.</p>
<p>Integrating it surfaced the kind of detail you only learn by using a library rather than reading its README. Foam textures are referenced by filename in the preset but loading them is the consumer's job, and without them the shoreline reads as a hard waterline edge instead of breaking surf. The island is positioned so its underwater geometry drops past the ocean floor, letting the library's floor mesh occlude the model's outer ring with no visible plane edge, which is the library's intended pattern: bring a 3D model, don't synthesize terrain. And antialiasing is deliberately off on the renderer in favor of a post-process FXAA pass, because MSAA blends edge fragments against the background before the depth-aware atmospheric fog runs, leaving a thin dark fringe wherever geometry meets fog. Resolving aliasing after the fog instead of before it removes the fringe. What this de-risks is the decision itself: a production ocean is a large, specialized system, and for the cases where we need one, adopting a maintained library beats rebuilding wakes and buoyancy and foam from scratch, while the planar-mirror shader from spike 52 stays the right answer for the smaller inland water a creator places in their own world.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Screen-space reflection water.</strong> A reflected view ray marches the depth buffer in screen space via DDA, using perspective-correct 1/z interpolation, a hard step cap to bound the per-fragment cost, and a binary refinement pass to remove the banding a capped march leaves. A point-to-line confirmation with depth-scaled thickness rejects false hits. The method's hard limit is that it can only reflect geometry the main camera already sampled, so off-screen and wrong-side surfaces never appear. See <a href="/guides/landscape-generation-browser.html#terrain-materials">terrain materials</a>.</p>
<p><strong>Planar mirror reflection.</strong> A helper camera mirrored across the water plane renders the scene into an offscreen target that the water shader samples, giving pixel-perfect reflections including geometry SSR can't see. This is the pattern UE5 Water and three.js <code>WaterMesh</code> use, with wave distortion applied as an offset to the reflector's UV node. A magnitude-weighted sky fallback wrongly erased dark reflection pixels; the reflector target is reliable after the first frame, so removing the fallback was the fix.</p>
<p><strong>Beer-Lambert depth tint refraction.</strong> Both shaders sample the scene behind the surface, reconstruct each backdrop pixel's depth below the waterline, and apply per-channel exponential extinction (red dies in meters, blue persists) composited toward a water-fog color, with a sky mask so the far plane isn't fogged. Schlick Fresnel blends refraction at normal incidence with reflection at grazing angles.</p>
<p><strong>Adopting a production water library.</strong> <code>threejs-water-pro</code> ships an ocean clipmap, Rayleigh sky, multi-point buoyancy for ship pitch and roll, wakes, foam, and a hull mask pass. Consumer-side details matter: foam textures must be loaded explicitly, island underwater geometry should drop past the ocean floor so the floor occludes its edges, and antialiasing should run as a post-process FXAA pass rather than MSAA to avoid a dark fog fringe at geometry edges.</p>
<hr>
<p>Part 26 of 29.
Previous: <a href="/blog/2026-05-10-open-world-browser-part-25-universal-characters.html">Part 25 - One skeleton, every outfit</a>
Next: <a href="/blog/2026-05-13-open-world-browser-part-27-island-and-terrain.html">Part 27 - An island from noise, ground that looks like ground</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[How modern engines beat foliage overdraw]]></title>
            <link>https://app.cinevva.com/blog/2026-05-11-foliage-overdraw</link>
            <guid>https://app.cinevva.com/blog/2026-05-11-foliage-overdraw</guid>
            <pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Z-prepasses, octahedral imposters, GPU-driven cluster culling, hashed alpha, visibility buffers, and distance-field shadows: the stack that turns 8-15x foliage overdraw into 1-2x in dense forest scenes.]]></description>
            <content:encoded><![CDATA[<h1 id="how-modern-engines-beat-foliage-overdraw" tabindex="-1">How modern engines beat foliage overdraw <a class="header-anchor" href="#how-modern-engines-beat-foliage-overdraw" aria-label="Permalink to &quot;How modern engines beat foliage overdraw&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<img src="/img/blog/foliage_overdraw_hero.webp" alt="Dense forest canopy with overlapping leaf cards stacked behind each other in front of a setting sun, suggesting heavy overdraw" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>A forest is one of the worst things you can ask a GPU to render. Each leaf is a textured quad with an alpha mask cut out of it. Dozens of those quads stack on top of each other along every view ray. The rasterizer has no way to know in advance which fragments will pass the alpha test, so the standard early-Z optimization that saves opaque scenes is mostly turned off. The result is that a single screen pixel can run the full leaf shader 10 or 15 times before the frame settles. That's foliage overdraw, and it's the single most expensive part of an open-world frame in any game shipped in the last decade.</p>
<p>The good news is that it's a solved problem. Not in the sense that anyone fixed it with one trick, but in the sense that there's a stack of seven or eight techniques that, combined, take effective overdraw from 8-15x in a dense forest down to 1-2x. Every modern engine ships some version of this stack. Here's what's in it, why each piece exists, and the canonical references for each.</p>
<h2 id="_1-why-foliage-overdraw-is-so-painful" tabindex="-1">1. Why foliage overdraw is so painful <a class="header-anchor" href="#_1-why-foliage-overdraw-is-so-painful" aria-label="Permalink to &quot;1. Why foliage overdraw is so painful&quot;"></a></h2>
<p>In a typical opaque scene, the GPU does early depth rejection: before the pixel shader even runs, the hardware checks the existing depth buffer and skips fragments that are already behind something. This is essentially free and it's what keeps the cost of dense geometry sane.</p>
<p>Alpha-tested foliage breaks this. The fragment shader has to actually run to evaluate the alpha texture and call <code>discard</code> (or <code>clip</code>) on the masked-out pixels. The hardware can't know whether a fragment will be killed until after the shader runs, so on most GPUs the use of <code>discard</code> anywhere in a shader disables early-Z for that draw call entirely. On tile-based GPUs (mobile, M-series, some consoles) it can disable Hi-Z and depth compression for the whole frame. The pixel shader runs once for every triangle that covers a pixel, and most of those evaluations end with a discard.</p>
<p>Stack 10 leaf quads in front of a single screen pixel and the leaf shader runs 10 times. Multiply by 4 million pixels and the cost is brutal. Marco Salvi's writeup <a href="https://therealmjp.github.io/posts/to-earlyz-or-not-to-earlyz/" target="_blank" rel="noreferrer">To Early-Z, or Not To Early-Z</a> is the gentlest tour of why this happens at the hardware level, and it's the right place to start if you've never thought about it before.</p>
<img src="/img/blog/foliage_overdraw_layers.webp" alt="Side view of a single screen pixel ray passing through twelve overlapping leaf cards, with each card highlighted to show stacked alpha-tested fragments" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<h2 id="_2-the-depth-prepass-for-masked-geometry" tabindex="-1">2. The depth prepass for masked geometry <a class="header-anchor" href="#_2-the-depth-prepass-for-masked-geometry" aria-label="Permalink to &quot;2. The depth prepass for masked geometry&quot;"></a></h2>
<p>The biggest single win, and the technique every modern engine ships, is splitting the foliage draw into two passes. The first pass writes only depth, with a minimal shader that does the alpha test, discards the masked pixels, and writes nothing else. The second pass renders the full material with depth testing set to &quot;equal&quot; and depth writes off. Each visible pixel now shades the full BRDF exactly once, no matter how many leaf quads were stacked behind it.</p>
<p>This sounds like double work because you're touching the same triangles twice, but the prepass shader is so cheap (one texture sample, one discard, one depth write) that the savings on the main pass dwarf the extra cost. In a dense forest the main pass goes from running the full leaf shader 10-15 times per pixel to running it exactly once.</p>
<p>Unreal, Frostbite, Decima, and the modern Rage and Dunia branches all do this. It's also the key reason masked materials are still cheaper than translucent materials in any of these engines.</p>
<p>Deep dives:</p>
<ul>
<li>Pettineo, <a href="https://therealmjp.github.io/posts/to-earlyz-or-not-to-earlyz/" target="_blank" rel="noreferrer">To Early-Z, or Not To Early-Z</a> (the canonical writeup of how <code>discard</code> interacts with Hi-Z and early-Z).</li>
<li>Wihlidal, <a href="http://www.frostbite.com/2016/03/optimizing-the-graphics-pipeline-with-compute/" target="_blank" rel="noreferrer">Optimizing the Graphics Pipeline with Compute</a> (GDC 2016, the Frostbite depth-prepass and prepass-driven culling architecture).</li>
<li>Sanders, <a href="https://www.gdcvault.com/play/1025066/Between-Tech-and-Art-The" target="_blank" rel="noreferrer">Between Tech and Art: The Vegetation of Horizon Zero Dawn</a> (GDC 2018, with Decima's two-pass foliage rendering).</li>
<li>Persson, <a href="http://www.humus.name/index.php?page=News&amp;ID=255" target="_blank" rel="noreferrer">A couple of notes about Z</a> (still one of the clearest explanations of depth-equal testing and prepass economics).</li>
</ul>
<h2 id="_3-aggressive-lods-and-octahedral-imposters" tabindex="-1">3. Aggressive LODs and octahedral imposters <a class="header-anchor" href="#_3-aggressive-lods-and-octahedral-imposters" aria-label="Permalink to &quot;3. Aggressive LODs and octahedral imposters&quot;"></a></h2>
<p>The second-biggest win is never drawing the leaves at all when you don't have to. Foliage assets ship with several LOD tiers. The closest is the full mesh with individual leaf cards. At medium range, the leaves collapse into denser composite cards (a clump of 30 leaves becomes 1 textured card with the same silhouette). Beyond a distance threshold, the entire tree becomes an <em>imposter</em>: a small piece of geometry textured with pre-rendered views of the tree from many angles.</p>
<p>The modern imposter format is the <strong>octahedral imposter</strong>: an 8-faced piece of geometry textured with an atlas of views captured from points on a sphere using octahedral mapping. At runtime, the shader picks the closest two or three pre-rendered views based on the camera direction and blends between them. The result is a few-triangle stand-in that looks 3D from any angle and can have proper shading, normal maps, and even wind animation. Ryan Brucks' implementation, originally a community plugin and now part of Unreal, is the reference. Microsoft Flight Simulator's billion-tree forests are essentially octahedral imposters everywhere except the camera.</p>
<p>The bigger structural win is that imposters are <em>opaque or near-opaque</em> at distance. The 30-leaf composite card is a single masked quad instead of 30 quads. The whole-tree imposter is a few faces, not thousands. Far-field overdraw collapses to almost nothing.</p>
<p>Deep dives:</p>
<ul>
<li>Brucks, <a href="https://shaderbits.com/blog/octahedral-impostors" target="_blank" rel="noreferrer">Octahedral Impostors</a> (the canonical reference, with the math and the UE implementation).</li>
<li>Halen, <a href="https://dev.epicgames.com/community/learning/tutorials/JmL3/octahedral-impostors-unreal-engine" target="_blank" rel="noreferrer">Octahedral Impostors in Unreal Engine</a> (the integrated UE workflow).</li>
<li>Häggström, <a href="https://www.diva-portal.org/smash/get/diva2:1135856/FULLTEXT01.pdf" target="_blank" rel="noreferrer">Real-Time Rendering of Vegetation</a> (a clean thesis covering LOD chains, imposters, and the math behind them).</li>
<li>Crytek, <a href="http://developer.nvidia.com/gpugems/gpugems3/part-i-geometry/chapter-4-next-generation-speedtree-rendering" target="_blank" rel="noreferrer">SpeedTree integration in CryEngine 3</a> (GPU Gems 3, still the best primer on tree LOD chains).</li>
</ul>
<h2 id="_4-cluster-and-gpu-driven-culling" tabindex="-1">4. Cluster and GPU-driven culling <a class="header-anchor" href="#_4-cluster-and-gpu-driven-culling" aria-label="Permalink to &quot;4. Cluster and GPU-driven culling&quot;"></a></h2>
<p>Even with a depth prepass, the prepass itself has a cost: it still has to <em>touch</em> every triangle of every visible (or potentially visible) tree. Modern engines push that cost down with GPU-driven cluster culling, which throws away whole groups of triangles before the rasterizer ever sees them.</p>
<p>The pipeline looks like this: every mesh is pre-split into clusters of 64 or 128 triangles with a tight bounding box and a cone of normals. At render time, a compute shader walks the instance list, frustum-tests each instance, then frustum-tests every cluster of every visible instance, then <em>Hi-Z occlusion-tests</em> each surviving cluster against the previous frame's depth pyramid. Whole branches of trees that are hidden behind a hill or in front of another tree get culled before any vertex shader runs. The output is a compact list of &quot;draw these clusters&quot; arguments fed straight into a single <code>DrawIndirect</code> call.</p>
<p>This is what makes a forest of 10,000 trees render in milliseconds instead of seconds. Ubisoft's Assassin's Creed Unity talk introduced this pipeline in production form (20-40% triangles culled, 30-80% shadow triangles culled, 10x more instances on screen than the previous generation), and Wihlidal's Frostbite talk took it further. UE5 Nanite is the visible end-state of this trajectory: cluster culling all the way down to the pixel.</p>
<p>Deep dives:</p>
<ul>
<li>Haar and Aaltonen, <a href="https://advances.realtimerendering.com/s2015/aaltonenhaar_siggraph2015_combined_final_footer_220dpi.pdf" target="_blank" rel="noreferrer">GPU-Driven Rendering Pipelines</a> (SIGGRAPH 2015, the foundational Assassin's Creed Unity talk).</li>
<li>Wihlidal, <a href="http://www.frostbite.com/2016/03/optimizing-the-graphics-pipeline-with-compute/" target="_blank" rel="noreferrer">Optimizing the Graphics Pipeline with Compute</a> (GDC 2016, Frostbite's GPU-driven prepass).</li>
<li>Karis, Stubbe, Wihlidal, <a href="https://advances.realtimerendering.com/s2021/Karis_Nanite_SIGGRAPH_Advances_2021_final.pdf" target="_blank" rel="noreferrer">A Deep Dive into Nanite Virtualized Geometry</a> (SIGGRAPH 2021, cluster culling at meshlet granularity).</li>
<li>Liktor, <a href="https://research.activision.com/publications/2021/09/geometry-rendering-pipeline-architecture-at-activision" target="_blank" rel="noreferrer">Geometry Rendering Pipeline Architecture at Activision</a> (the Call of Duty version of cluster culling, 2021).</li>
</ul>
<h2 id="_5-front-to-back-instance-and-cluster-sorting" tabindex="-1">5. Front-to-back instance and cluster sorting <a class="header-anchor" href="#_5-front-to-back-instance-and-cluster-sorting" aria-label="Permalink to &quot;5. Front-to-back instance and cluster sorting&quot;"></a></h2>
<p>Once the prepass is doing its job, <em>order</em> starts to matter. The prepass writes depth, but only for fragments that pass the alpha test. If you draw the back of the forest first and the front last, every front fragment overwrites a back fragment, and the prepass shader still runs for the back. If you draw front-to-back, each successive draw fills more of the depth buffer with smaller values, and Hi-Z rejects more and more of the back fragments before the shader runs.</p>
<p>This is why almost every modern engine sorts foliage instances by distance to camera before issuing the prepass. The sort is cheap (a few hundred thousand instances on the GPU using a radix sort), and it turns the prepass itself into a self-pruning operation. Cluster-level culling sorts at the meshlet granularity for the same reason. The depth prepass and front-to-back ordering are the kind of pair where each is good and the combination is great.</p>
<p>Deep dives:</p>
<ul>
<li>Persson, <a href="http://www.humus.name/Articles/Persson_DepthInDepth.pdf" target="_blank" rel="noreferrer">Depth in-depth</a> (the architectural notes on depth-buffer ordering, prepass economics, and Hi-Z behavior).</li>
<li>Giesen, <a href="https://fgiesen.wordpress.com/2011/07/04/a-trip-through-the-graphics-pipeline-2011-part-7/" target="_blank" rel="noreferrer">A trip through the graphics pipeline</a> (the deep technical explanation of how Hi-Z reject rate depends on draw order).</li>
<li>Wihlidal, <a href="http://www.frostbite.com/2016/03/optimizing-the-graphics-pipeline-with-compute/" target="_blank" rel="noreferrer">Optimizing the Graphics Pipeline with Compute</a> (GDC 2016, includes Frostbite's GPU-side instance sort).</li>
</ul>
<h2 id="_6-dithered-lod-transitions-and-hashed-alpha" tabindex="-1">6. Dithered LOD transitions and hashed alpha <a class="header-anchor" href="#_6-dithered-lod-transitions-and-hashed-alpha" aria-label="Permalink to &quot;6. Dithered LOD transitions and hashed alpha&quot;"></a></h2>
<p>The other big trap is fading. The naive way to transition between two LODs (or to fade an instance in or out as the camera approaches) is alpha blending. But blended geometry can't write to the depth buffer, which kicks every fading tree into the slow translucent path and breaks the prepass. The solution is to keep the geometry in the masked path and do the fade <em>inside the alpha test</em>.</p>
<p>Two main techniques:</p>
<ul>
<li><strong>Dithered LOD transitions</strong> sample a 4x4 or 8x8 Bayer pattern (or a screen-space blue-noise texture) and use that as a per-pixel cutoff modifier. A tree at 50% blend has a checkerboard of pixels surviving; the missing pixels are filled in by the next LOD's complementary checkerboard. TAA resolves the checker into a smooth blend across two or three frames. Cheap, stable, blends with everything else in the engine.</li>
<li><strong>Hashed alpha testing</strong> (Wyman &amp; McGuire, I3D 2017) replaces the fixed 0.5 alpha threshold with a per-pixel hashed threshold in [0,1). Distant alpha geometry that would normally vanish entirely (because the mipmapped alpha drops below 0.5) keeps a stable scattering of surviving pixels. TAA again does the cleanup.</li>
</ul>
<p>Both techniques keep foliage in the opaque/masked path where the depth prepass works, so you don't pay the full translucent rendering cost just to fade something in. <strong>Alpha to coverage</strong> is the MSAA-era cousin of the same idea: convert alpha into a sub-pixel coverage mask, get partial transparency without leaving the masked path. The catch is that A2C only really shines with MSAA, which most modern deferred renderers no longer use.</p>
<p>Deep dives:</p>
<ul>
<li>Wyman and McGuire, <a href="https://research.nvidia.com/labs/rtr/publication/wyman2017hashed/" target="_blank" rel="noreferrer">Hashed Alpha Testing</a> (I3D 2017, the canonical hashed-alpha paper).</li>
<li>Castaño, <a href="http://the-witness.net/news/2010/09/computing-alpha-mipmaps/" target="_blank" rel="noreferrer">Computing Alpha Mipmaps</a> (The Witness blog, the right way to mip alpha-tested textures so distant trees don't vanish).</li>
<li>Yuksel, <a href="https://cemyuksel.com/research/alphadistribution/alpha_distribution.pdf" target="_blank" rel="noreferrer">Alpha Distribution for Alpha Testing</a> (a more recent improvement on alpha mipmaps).</li>
<li>NVIDIA, <a href="https://developer.nvidia.com/content/transparency-or-translucency-rendering" target="_blank" rel="noreferrer">Anti-Aliased Alpha Testing</a> (a survey of A2C, hashed alpha, and dithered alternatives).</li>
</ul>
<h2 id="_7-reducing-shading-cost-on-masked-pixels" tabindex="-1">7. Reducing shading cost on masked pixels <a class="header-anchor" href="#_7-reducing-shading-cost-on-masked-pixels" aria-label="Permalink to &quot;7. Reducing shading cost on masked pixels&quot;"></a></h2>
<p>Even with a perfect prepass and perfect culling, you still have to shade every visible foliage pixel once. Engines reduce that cost too:</p>
<ul>
<li><strong>Cheaper BRDF</strong>. Foliage is matte and doesn't really need a full Cook-Torrance specular path. A wrapped-Lambertian diffuse plus a one-line specular approximation is plenty.</li>
<li><strong>Lower-frequency normal maps</strong>. Leaves are noisy already. A 256x256 normal map looks the same as a 1024x1024 one at typical viewing distances and saves bandwidth.</li>
<li><strong>No parallax, no anisotropy, no clearcoat</strong>. The PBR feature menu gets switched off for leaves.</li>
<li><strong>Two-sided thin transmission</strong> instead of full subsurface scattering. Leaves transmit light from behind, but you can fake it with a single back-light dot product.</li>
<li><strong>Half-resolution shading</strong> in some engines. Foliage gets shaded at 1/4 or 1/2 pixel rate and upscaled. Stochastic noise from TAA hides the resampling.</li>
<li><strong>Skip detail textures and decals</strong> on masked materials by default.</li>
</ul>
<p>Each of these is a small win individually. Combined, masked-foliage shaders can run 2-3x faster than the equivalent opaque material.</p>
<p>Deep dives:</p>
<ul>
<li>Lagarde and de Rousiers, <a href="https://seblagarde.wordpress.com/2015/07/14/siggraph-2014-moving-frostbite-to-physically-based-rendering/" target="_blank" rel="noreferrer">Moving Frostbite to Physically Based Rendering 3.0, sections on foliage and translucency</a> (SIGGRAPH 2014, the canonical PBR adjustments for thin two-sided materials).</li>
<li>Jimenez, <a href="https://www.iryoku.com/stare-into-the-future" target="_blank" rel="noreferrer">Next Generation Character Rendering</a> (the wrapped-Lambertian and back-translucency math, originally for skin but ported widely to leaves).</li>
<li>Sanders, <a href="https://www.gdcvault.com/play/1025066/Between-Tech-and-Art-The" target="_blank" rel="noreferrer">Between Tech and Art: The Vegetation of Horizon Zero Dawn</a> (GDC 2018, with Decima's foliage shader simplifications).</li>
</ul>
<h2 id="_8-visibility-buffers-and-nanite-for-masked-materials" tabindex="-1">8. Visibility buffers and Nanite for masked materials <a class="header-anchor" href="#_8-visibility-buffers-and-nanite-for-masked-materials" aria-label="Permalink to &quot;8. Visibility buffers and Nanite for masked materials&quot;"></a></h2>
<p>The cleanest answer to overdraw is to <em>decouple shading from rasterization entirely</em>. A visibility buffer rasterizes geometry into a thin buffer (just triangle ID and instance ID per pixel), then runs the full material as a deferred pass that reads the visibility buffer and shades each pixel exactly once. There is no overdraw at the shading stage, by construction. Burns and Hunt's 2013 paper introduced this; UE5 Nanite is the production-quality realization, including for masked foliage as of UE 5.5.</p>
<p>Nanite's twist is that it does this with cluster-level virtualized geometry, so the rasterizer itself runs on a software path for sub-pixel triangles and keeps overdraw bounded. Masked materials in Nanite need the &quot;programmable raster&quot; feature: the alpha test runs during the visibility-buffer pass, but the material shading still happens once per visible pixel in the deferred resolve. The result is that very dense Nanite foliage, once notorious for being slow on masked materials, is now competitive with opaque or even cheaper, because the overdraw at shading time is zero. There's a tradeoff: it's often better to push high-poly <em>opaque</em> tree geometry through Nanite than to keep low-poly masked-card trees, because the masked path adds programmable-raster cost.</p>
<p>Deep dives:</p>
<ul>
<li>Burns and Hunt, <a href="http://jcgt.org/published/0002/02/04/" target="_blank" rel="noreferrer">The Visibility Buffer: A Cache-Friendly Approach to Deferred Shading</a> (JCGT 2013, the original paper).</li>
<li>Karis, Stubbe, Wihlidal, <a href="https://advances.realtimerendering.com/s2021/Karis_Nanite_SIGGRAPH_Advances_2021_final.pdf" target="_blank" rel="noreferrer">A Deep Dive into Nanite Virtualized Geometry</a> (SIGGRAPH 2021, the production architecture).</li>
<li>Epic, <a href="https://media.gdcvault.com/gdc2024/Slides/GDC+slide+presentations/Nanite+GPU+Driven+Materials.pdf" target="_blank" rel="noreferrer">Nanite GPU Driven Materials</a> (GDC 2024, with the masked-material and programmable-raster pipeline).</li>
<li><a href="https://www.sctheblog.com/blog/nanite-materials-notes/" target="_blank" rel="noreferrer">Notes from &quot;Nanite GPU Driven Materials&quot;</a> (a clean third-party walkthrough of the same talk).</li>
</ul>
<h2 id="_9-separate-shadow-representations-for-foliage" tabindex="-1">9. Separate shadow representations for foliage <a class="header-anchor" href="#_9-separate-shadow-representations-for-foliage" aria-label="Permalink to &quot;9. Separate shadow representations for foliage&quot;"></a></h2>
<p>Shadow maps for alpha-tested foliage are the second-most-expensive shadow problem in any open-world frame, after large cascades. Every cascade needs its own depth prepass, every prepass runs the alpha test, and the cost stacks fast across 4 cascades and dozens of light frusta. So most engines don't render foliage shadows the same way they render foliage color.</p>
<p>Common substitutions:</p>
<ul>
<li><strong>Mesh distance-field shadows</strong> (UE Lumen, custom engines). Each mesh has a precomputed signed distance field. A short cone trace through the SDF gives a soft shadow without ever touching the alpha-tested mesh. Particularly nice for trees because the SDF captures the canopy silhouette as a single solid blob and ignores the per-leaf detail.</li>
<li><strong>Lower-resolution cascades for foliage</strong>. Foliage shadows go into a half-resolution slice and get up-sampled with edge-aware filtering. The eye doesn't notice the resolution drop because the shadows are already soft.</li>
<li><strong>Alpha-to-coverage shadow maps with MSAA</strong>. On engines that still have MSAA in their shadow path, A2C gives smooth-edged foliage shadows without the full alpha test cost.</li>
<li><strong>Capsule shadows for the trunk and big branches</strong>, distance-field for the canopy, full alpha test only at the closest cascade. Different representations for different distances, blended in the lighting pass.</li>
<li><strong>WPO disable distance</strong>. Wind-driven World Position Offset is killed beyond a threshold so that cached shadow data stays valid across frames. UE's Virtual Shadow Maps lean heavily on this.</li>
</ul>
<p>Deep dives:</p>
<ul>
<li>Epic, <a href="https://dev.epicgames.com/documentation/unreal-engine/distance-field-soft-shadows-in-unreal-engine" target="_blank" rel="noreferrer">Distance Field Soft Shadows in Unreal Engine</a> (the canonical UE reference for mesh DF shadows).</li>
<li>Wright, <a href="https://advances.realtimerendering.com/s2022/SIGGRAPH2022-Advances-Lumen-Wright%20et%20al.pdf" target="_blank" rel="noreferrer">Lumen: Real-Time Global Illumination in Unreal Engine 5</a> (SIGGRAPH 2022, with Lumen's mesh-SDF integration for foliage).</li>
<li>Epic, <a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/virtual-shadow-maps-in-unreal-engine" target="_blank" rel="noreferrer">Virtual Shadow Maps</a> (the modern UE5 shadow architecture, with the foliage-specific WPO disable and caching rules).</li>
<li>Persson, <a href="https://www.humus.name/index.php?page=3D&amp;ID=81" target="_blank" rel="noreferrer">Practical Cascaded Shadow Maps</a> (the still-canonical CSM reference with notes on alpha-tested casters).</li>
</ul>
<h2 id="_10-wind-animation-and-shadow-caching" tabindex="-1">10. Wind, animation, and shadow caching <a class="header-anchor" href="#_10-wind-animation-and-shadow-caching" aria-label="Permalink to &quot;10. Wind, animation, and shadow caching&quot;"></a></h2>
<p>A subtle related problem: most foliage moves. Wind-driven vertex animation (World Position Offset in UE, equivalent in Frostbite and Decima) means the foliage geometry isn't stable from frame to frame, which breaks shadow caching and reprojection. Modern engines fight this in two ways:</p>
<ul>
<li><strong>Cap WPO at distance</strong>. Beyond a threshold, the wind animation amplitude smoothly goes to zero. The eye can't see the sway that far away anyway, and the shadow caches stay valid.</li>
<li><strong>Bake the wind into the cluster bounds</strong>. Cluster bounding boxes are inflated by the maximum WPO offset so culling stays conservative without re-uploading per frame.</li>
<li><strong>Per-instance phase offsets</strong>. Identical trees use a per-instance random seed to offset the wind phase, so a forest doesn't sway in lockstep without paying for unique animation per tree.</li>
</ul>
<p>This is the kind of detail that doesn't show up in technique lists but is the difference between a 3 ms forest and a 9 ms forest in practice.</p>
<p>Deep dives:</p>
<ul>
<li>Sanders, <a href="https://www.gdcvault.com/play/1025066/Between-Tech-and-Art-The" target="_blank" rel="noreferrer">Between Tech and Art: The Vegetation of Horizon Zero Dawn</a> (GDC 2018, with the Decima wind-animation pipeline and shadow caching).</li>
<li>McAuley, <a href="https://www.gdcvault.com/play/1022235/Rendering-the-World-of-Far" target="_blank" rel="noreferrer">Rendering the World of Far Cry 4</a> (GDC 2015, includes the wind-grid sampling for vegetation).</li>
<li>Epic, <a href="https://dev.epicgames.com/community/learning/tutorials/qLk6/unreal-engine-virtual-shadow-maps" target="_blank" rel="noreferrer">Foliage and Virtual Shadow Maps</a> (the UE5 community guidance on WPO disable distance and VSM caching).</li>
</ul>
<h2 id="_11-the-combined-math" tabindex="-1">11. The combined math <a class="header-anchor" href="#_11-the-combined-math" aria-label="Permalink to &quot;11. The combined math&quot;"></a></h2>
<p>None of these tricks is a silver bullet. The interesting thing is what happens when you stack them:</p>
<ul>
<li>A naive masked-foliage pass on a dense forest scene measures 8-15x effective overdraw. Every visible pixel runs the leaf shader 8 to 15 times.</li>
<li>Add a depth prepass and the main pass drops to ~1x overdraw, but the prepass itself still touches everything.</li>
<li>Add front-to-back sorting and the prepass starts pruning itself.</li>
<li>Add cluster-level GPU culling and the prepass touches only what could possibly be visible.</li>
<li>Add LOD chains and imposters and the <em>count</em> of visible quads drops by an order of magnitude beyond 30 m.</li>
<li>Add the visibility-buffer / Nanite path and the shading stage truly runs once per pixel even on dense overlap.</li>
<li>Add distance-field shadows and the shadow cost stops scaling with the alpha test.</li>
</ul>
<p>The headline result, repeated across the talks linked above: effective overdraw collapses from 8-15x to 1-2x, and total foliage frame cost falls by 4-6x in a dense forest scene. That's the entire reason modern open-world games can render forests at 60+ fps on consumer hardware.</p>
<img src="/img/blog/foliage_overdraw_pipeline.webp" alt="Diagram showing the foliage rendering pipeline as five stacked stages: GPU culling, sorted prepass, masked color pass with depth-equal, visibility-buffer shading, and separate distance-field shadow path" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<h2 id="_12-what-this-means-for-the-browser" tabindex="-1">12. What this means for the browser <a class="header-anchor" href="#_12-what-this-means-for-the-browser" aria-label="Permalink to &quot;12. What this means for the browser&quot;"></a></h2>
<p>Most of this stack maps cleanly onto WebGPU. We've already shipped GPU-driven culling, indirect dispatch, Hi-Z occlusion, and front-to-back sorted prepasses in the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">open-world browser engine</a>. The depth prepass for masked geometry is straightforward: WebGPU supports depth-equal testing and <code>discard</code> in fragment shaders, with the same caveats about early-Z. Octahedral imposters port mechanically, the math is just sphere-to-octahedron unwrapping and atlas indexing.</p>
<p>The harder pieces are the modern ones. A visibility buffer in WebGPU means writing a 32-bit triangle ID into a render target and resolving materials in a fullscreen compute pass; the building blocks exist but the orchestration is involved. Mesh distance-field shadows want a 3D texture per asset and a short cone trace, both of which are in WebGPU's reach. Hashed alpha and dithered LODs are one shader function each.</p>
<p>The path forward for browser foliage is the same as for everything else in this stack: ship the cheap, robust pieces first (prepass, sorted instances, LODs, imposters, hashed alpha) and add the heavy machinery (visibility buffer, mesh-SDF shadows) on top. The browser hardware floor is finally high enough that there is no architectural reason a WebGPU forest shouldn't look and run like a console one. There are just engineering reasons, and engineering reasons are the kind we like.</p>
<h2 id="further-reading-across-the-whole-stack" tabindex="-1">Further reading across the whole stack <a class="header-anchor" href="#further-reading-across-the-whole-stack" aria-label="Permalink to &quot;Further reading across the whole stack&quot;"></a></h2>
<p>If you want one source that pulls all of this together, the SIGGRAPH &quot;Advances in Real-Time Rendering in Games&quot; archive (<a href="https://advances.realtimerendering.com/" target="_blank" rel="noreferrer">advances.realtimerendering.com</a>) has the canonical foliage and GPU-driven-rendering talks going back to 2014. Adrian Courrèges' <a href="http://www.adriancourreges.com/blog/" target="_blank" rel="noreferrer">GPU profiling articles</a> include frame-by-frame breakdowns of GTA V and Horizon Zero Dawn that show every pass discussed here in production order. For the alpha-test math specifically, Chris Wyman's <a href="https://cwyman.org/" target="_blank" rel="noreferrer">research page</a> has the hashed-alpha and stochastic-transparency papers with reference shaders. And the <a href="https://www.realtimerendering.com/" target="_blank" rel="noreferrer">Real-Time Rendering, 4th edition</a> chapters on transparency, sampling, and depth handling are still the textbook starting point.</p>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/blog/foliage_overdraw_hero.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 25: One skeleton, every outfit]]></title>
            <link>https://app.cinevva.com/blog/2026-05-10-open-world-browser-part-25-universal-characters</link>
            <guid>https://app.cinevva.com/blog/2026-05-10-open-world-browser-part-25-universal-characters</guid>
            <pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 50 tested whether a CC0 character pack could carry an avatar customization product. Four Quaternius packs turned out to share one 65-joint skeleton on purpose, which turned the spike from an animation-pipeline problem into a by-name skeleton rebinding exercise.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-25-one-skeleton-every-outfit" tabindex="-1">Building an open world in the browser, part 25: One skeleton, every outfit <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-25-one-skeleton-every-outfit" aria-label="Permalink to &quot;Building an open world in the browser, part 25: One skeleton, every outfit&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-09-open-world-browser-part-24-persistence-and-wind.html">Part 24</a> made the world persistent and gave its vegetation wind. This part is about the people in it again, but from the wardrobe side: can a creator pick a base body, swap clothing, choose a hairstyle, and play any animation, all on a free asset pack, with no rigging and no animation authoring? Building a custom rig is not the right thing to de-risk. The question worth answering is whether a CC0 pack can carry the whole load.</p>
<h2 id="a-pack-that-was-built-to-fit-together" tabindex="-1">A pack that was built to fit together <a class="header-anchor" href="#a-pack-that-was-built-to-fit-together" aria-label="Permalink to &quot;A pack that was built to fit together&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/50-universal-characters/" title="Spike 50 Universal Characters" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/50-universal-characters/" target="_blank">Open Spike 50 in a new tab ↗</a> · <a href="/blog/spike-source?spike=50-universal-characters">View source</a></p>
<p>Quaternius ships four assets that line up almost suspiciously well: Universal Base Characters with male and female bodies plus eight hairstyles and two eyebrow sets, Modular Character Outfits split into body, arms, legs, and feet parts with accessories like pauldrons and a hood, and the Universal Animation Library packs one and two, 262 clips in total covering locomotion, combat, climbing, farming, fishing, idles, and social moves. Inspecting their glTFs revealed why they fit: every single mesh in all four packs is rigged to the same 65-joint UE5-style skeleton, the same joint names, the same bind poses, root through pelvis and spine to head, mirrored arms with fifteen finger joints per side, mirrored legs. No retargeting needed. This is the exact opposite of the three-skeleton retargeting problem from Spike 35, where a 213-bone face rig had to talk to a 65-bone Mixamo rig. Here all four packs were authored on the animation library's skeleton on purpose, and the bond is exact.</p>
<p>That turned the spike into a binding exercise rather than an animation-pipeline one. The base layer loads a body glTF, clones the scene with <code>SkeletonUtils.clone</code> so each character gets a fresh skeleton instance rather than feeding bone mutations back into the loader's cached scene, and caches that 65-bone skeleton as canonical along with the armature object that roots animation.</p>
<h2 id="rebinding-a-coat-onto-a-body" tabindex="-1">Rebinding a coat onto a body <a class="header-anchor" href="#rebinding-a-coat-onto-a-body" aria-label="Permalink to &quot;Rebinding a coat onto a body&quot;"></a></h2>
<p>The trick that makes the whole thing work is that you can't simply reparent an outfit's mesh into the scene, because it still references the bones from its own glTF, which sit in a different subtree. Each modular part, a peasant body or a ranger's legs, arrives as its own SkinnedMesh with its own clone of the same armature. The binding walks the source skeleton's bones in their original order, looks each one up by name in the canonical skeleton, and builds a new <code>Skeleton</code> from those rebound bones reusing the source's bind-inverse matrices, then calls <code>SkinnedMesh.bind</code> with the source's identity bind matrix. After binding, the mesh detaches from its source scene and parents directly under the character root. Because the bind matrix captures the world transform at bind time and the root sits at identity, the mesh's own world transform never affects the output and skinning happens entirely through the now-shared bones.</p>
<p>Hair and eyebrows are slots too, and they corrected a wrong assumption. The folder structure said &quot;rigged to head bone,&quot; which suggested hair would be a static mesh parented to the head with a baked offset. It isn't. Every hair file is a SkinnedMesh weighted across all 65 joints, with the long hair's weights bleeding into the upper spine and neck so it drapes when the character looks down. So hair takes the same by-name rebinding path as any outfit part. The one special case is the outfit body slot: when it's attached, the base body mesh is hidden so the outfit doesn't clip through it, while arms, legs, feet, and accessories just stack because the pack authored them to coexist with the base body showing through wherever the clothing leaves skin exposed.</p>
<h2 id="_262-animations-for-free" tabindex="-1">262 animations for free <a class="header-anchor" href="#_262-animations-for-free" aria-label="Permalink to &quot;262 animations for free&quot;"></a></h2>
<p>Animation is almost anticlimactic once the skeleton is shared. The two library glbs load, their clips flatten into one named list, and the track names in those clips reference the exact joint names by <code>bone.position</code>, <code>bone.quaternion</code>, and <code>bone.scale</code>. An <code>AnimationMixer</code> rooted at the character's armature finds those bones by name and drives them directly, crossfading on switch. The viewer's right panel is a filterable list of all 262 clips with a source pill, so searching &quot;fish&quot; surfaces every fishing animation across both libraries. Standard three-point lighting with soft shadows, a polar grid underfoot, and ACES tone mapping keep the stylized base textures readable without crushing the dark bodysuit's shadows.</p>
<p>What this de-risks for the product is large. The avatar customization feature can ship on a CC0 pack with zero rigging work and zero animation authoring: drop in the four packs, wire a wardrobe UI, and you have roughly six outfits times eight hairstyles times two sexes times 262 animations of variation out of the box. The 65-joint skeleton is small enough that GPU skinning cost is negligible, and the batched-skinning work from Spike 45 already handles 200-plus avatars on a rig this size. CC0 carries no use restrictions, so commercial use on a paid creator platform is fine. The remaining open question is custom-uploaded outfits, which is a Phase 3 problem solved by retargeting at upload time onto this canonical skeleton, the inverse of Spike 35's runtime retargeting. The asset footprint is about 147 MB on disk as raw 2K PNGs and uncompressed animation glbs, which production would shrink to roughly 30 MB with KTX2 texture compression and a gltf-transform pass.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>One canonical skeleton across a pack.</strong> Four Quaternius packs (base bodies, modular outfits, two animation libraries) are all rigged to an identical 65-joint UE5-style skeleton with the same joint names and bind poses, so combining them needs no retargeting. Cloning the base with <code>SkeletonUtils.clone</code> gives each character a fresh skeleton instance instead of mutating the loader's cached scene.</p>
<p><strong>By-name skeleton rebinding.</strong> Each modular part ships its own SkinnedMesh referencing its own armature. Rebinding walks the source bones in order, maps each by name into the canonical skeleton, builds a new <code>Skeleton</code> reusing the source bind-inverses, and calls <code>SkinnedMesh.bind</code> with the identity bind matrix, then parents the mesh under the shared character root so skinning runs entirely through the shared bones. The outfit body slot hides the base body to avoid clipping; other slots stack.</p>
<p><strong>Hair as a skinned slot.</strong> Hair is not a static head-bone attachment but a SkinnedMesh weighted across all 65 joints, with long hair weighted into the upper spine and neck so it drapes on look-down. It takes the same rebinding path as outfit parts.</p>
<p><strong>Name-keyed animation retargeting for free.</strong> Animation Library clips reference joints by exact name, so an <code>AnimationMixer</code> rooted at the shared armature drives all 262 clips directly with crossfade, no per-clip retargeting. A CC0 license covers commercial use, and the small joint count keeps GPU skinning cheap enough to reuse the Spike 45 batched-skinning path. See <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">GPU-driven LOD</a>.</p>
<hr>
<p>Part 25 of 29.
Previous: <a href="/blog/2026-05-09-open-world-browser-part-24-persistence-and-wind.html">Part 24 - Saving a world, and wind you can see</a>
Next: <a href="/blog/2026-05-12-open-world-browser-part-26-water.html">Part 26 - Water that holds up at every scale</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 24: Saving a world, and wind you can see]]></title>
            <link>https://app.cinevva.com/blog/2026-05-09-open-world-browser-part-24-persistence-and-wind</link>
            <guid>https://app.cinevva.com/blog/2026-05-09-open-world-browser-part-24-persistence-and-wind</guid>
            <pubDate>Sat, 09 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 47 made world edits stick: a Durable Object that arbitrates ownership, persists per-chunk binary blobs, and converges every peer's terrain through a commutative edit protocol. Spike 49 ported Quaternius's Godot wind shaders into TSL faithfully, then chased a per-frame recompile that dropped the scene to 1 fps.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-24-saving-a-world-and-wind-you-can-see" tabindex="-1">Building an open world in the browser, part 24: Saving a world, and wind you can see <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-24-saving-a-world-and-wind-you-can-see" aria-label="Permalink to &quot;Building an open world in the browser, part 24: Saving a world, and wind you can see&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-08-open-world-browser-part-23-avatars-and-voice.html">Part 23</a> put people in the world and gave them a voice. This part is about making the world remember what they did to it, and making it feel alive when nobody's touching it. Spike 47 is persistence: a creator sculpts terrain and places props, and those edits survive a reload, sync to every other peer, and arbitrate cleanly when two people edit at once. Spike 49 is wind: porting a stylized nature pack's vegetation shaders so trees, bushes, and grass move the way the artist intended, which turned into a fight with the shader compiler more than with the math.</p>
<h2 id="a-world-that-remembers" tabindex="-1">A world that remembers <a class="header-anchor" href="#a-world-that-remembers" aria-label="Permalink to &quot;A world that remembers&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/47-world-persistence/" title="Spike 47 World Persistence" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/47-world-persistence/" target="_blank">Open Spike 47 in a new tab ↗</a> · <a href="/blog/spike-source?spike=47-world-persistence">View source</a></p>
<p>The setup is a shared authoring session. Players walk a world, place props with a click, remove them with a right-click, and sculpt the ground by dragging a brush, raising and lowering the heightmap or carving volumetric caves through the SDF terrain that promotes a chunk to marching cubes. Everything they do persists in a Cloudflare <code>WorldChunkDO</code>, a server-authoritative Durable Object backed by its own SQLite storage. Clients don't write state directly. They send intent, the DO arbitrates and broadcasts the result, and the DO is the single source of truth, so a fresh joiner gets a snapshot and lands in exactly the same world everyone else sees.</p>
<p>Two persistence details earned their keep. Terrain isn't stored as an event log to be replayed on join; it's stored as per-chunk binary blobs that are the source of truth, uploaded on stroke-end, so a joiner loads the committed bytes directly instead of re-running thousands of brush samples. And those blobs live one storage key per chunk rather than one big row, because a single SDF chunk is 168 KB and the DO has a 2 MB per-row limit. A chunk index tracks which keys exist so the DO can rehydrate the whole map on wake. Identity is handled with two storages on the client: the player id lives in <code>sessionStorage</code> so two tabs are two distinct peers rather than one peer that overwrites itself in the DO's player map, while the display name lives in <code>localStorage</code> so a rename in one tab carries across all of them.</p>
<h2 id="making-concurrent-edits-converge" tabindex="-1">Making concurrent edits converge <a class="header-anchor" href="#making-concurrent-edits-converge" aria-label="Permalink to &quot;Making concurrent edits converge&quot;"></a></h2>
<p>The genuinely hard part of multiplayer authoring is what happens when two people edit overlapping ground in the same instant. The spike splits edits by their algebra. Additive operations, raise and lower on the heightmap and add and subtract on the SDF, are commutative: applying them in any order lands on the same result, so they ride an optimistic-stamp path where each client applies locally and ships the stamp, and the DO broadcasts it to everyone with no coordination. Order genuinely doesn't matter, so there's nothing to coordinate.</p>
<p>The order-dependent operations, smooth and flatten, were the interesting case. The first design gave them a region lock: a client requests a lock on pointer-down, the DO grants or denies it, the client buffers samples during the press, and on pointer-up the DO applies the whole stroke atomically. It works, but it's a separate protocol with its own lock TTL and grant/deny round-trip. The cleaner answer that replaced it is a precomputed delta: the originator runs the smooth or flatten brush locally, then ships the resulting list of per-cell deltas, and every peer just adds those deltas to their own cells without re-deriving anything. That turns an order-dependent operation into a commutative one by freezing its result at the source, so the whole edit system runs on one uniform commutative protocol with identical convergence and no locks at all. Prop locks survive for a different reason: per-record locking replaced owner-only deletion, so any peer can delete any prop unless someone has locked it, and only the locker can clear it. Undo and redo work by letting the client name a prop's id before placing it, so it knows the id ahead of the server echo and can reverse its own actions deterministically. Hibernating WebSockets keep an idle room free throughout, the same property that made the avatar relay in the previous part cheap.</p>
<h2 id="wind-ported-faithfully-then-fought" tabindex="-1">Wind ported faithfully, then fought <a class="header-anchor" href="#wind-ported-faithfully-then-fought" aria-label="Permalink to &quot;Wind ported faithfully, then fought&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/49-gpu-wind-props/" title="Spike 49 GPU Wind Props" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/49-gpu-wind-props/" target="_blank">Open Spike 49 in a new tab ↗</a> · <a href="/blog/spike-source?spike=49-gpu-wind-props">View source</a></p>
<p>Spike 49 takes Quaternius's Stylized Nature MegaKit and ports its wind to our stack. The pack ships its source Godot shaders, four of them, and the right move was a faithful translation rather than a re-invention. Bark has an empty vertex function, so trunks are rigid; an earlier procedural-mask attempt had been making trunks wave, and the fix was simply to stop applying wind to bark at all. Leaves get a per-vertex chaotic sway from a triangular-pulse hash, masked by height so canopies move and the base stays planted. Base foliage gets a noise-modulated sin/cos sway in world space. Grass is base foliage plus a wind-line bobble, sampling a scrolling noise texture through a power curve so only the bright bands of the texture contribute, which produces the visible ripples that travel across a meadow. The dispatch follows the pack's own material-naming convention, so a material called <code>Leaves_Birch</code> routes to the leaves path and <code>Grass_Common</code> to the grass path, no guessing.</p>
<p>A surprise from the port is that the leaf color isn't in the texture. Quaternius authors leaf appearance entirely from a vertical gradient and a Fresnel rim: the albedo is a mix from an extra color at the canopy bottom to the leaf color at the top, keyed on height, with a subsurface-scattering tint added as emission scaled by a <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="12.282ex" height="2.451ex" role="img" focusable="false" viewBox="0 -833.2 5428.4 1083.2" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(389,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1111.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(2111.4,0)"><g data-mml-node="mi"><path data-c="1D40D" d="M314 0Q296 3 181 3T48 0H39V62H147V624H39V686H171H265Q288 686 297 686T309 684T315 679Q317 676 500 455T684 233V624H576V686H585Q603 683 718 683T851 686H860V624H752V319Q752 15 750 11Q747 4 742 2T718 0H712Q708 0 706 0T700 0T696 1T693 2T690 4T687 7T684 11T679 16T674 23Q671 27 437 311L215 579V62H323V0H314Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3233.7,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(3733.9,0)"><g data-mml-node="mi"><path data-c="1D415" d="M592 686H604Q615 685 631 685T666 684T700 684T724 683Q829 683 835 686H843V624H744L611 315Q584 254 546 165Q492 40 482 19T461 -6L460 -7H409Q398 -4 391 9Q385 20 257 315L124 624H25V686H36Q57 683 190 683Q340 683 364 686H377V624H289L384 403L480 185L492 212Q504 240 529 298T575 405L670 624H582V686H592Z" style="stroke-width: 3;"/></g></g><g data-mml-node="msup" transform="translate(4602.9,0)"><g data-mml-node="mo"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(422,363) scale(0.707)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">N</mi></mrow><mo>⋅</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">V</mi></mrow><msup><mo stretchy="false">)</mo><mn>3</mn></msup></math></mjx-assistive-mml></mjx-container> Fresnel term. That's why a plain textured pass looked flat. The colors come from the gradient and the rim, not the painted map. The port reads a baked <code>heightFactor</code> vertex attribute instead of raw local Y, normalized per leaf group at load time from world-space Y, so the gradient and the wind mask both behave correctly no matter how the FBX import rotated each mesh's local axes. The Godot color constants are tagged sRGB and converted to linear before the shader sees them, so the port does the same conversion rather than feeding the bright sRGB numbers in as linear and washing the foliage out.</p>
<h2 id="the-recompile-that-ate-the-frame-rate" tabindex="-1">The recompile that ate the frame rate <a class="header-anchor" href="#the-recompile-that-ate-the-frame-rate" aria-label="Permalink to &quot;The recompile that ate the frame rate&quot;"></a></h2>
<p>The reason this shipped as an FBX baseline first, with the full wind material set aside in a <code>.bak</code> file, is a per-frame recompile bug that dropped the scene to about 1 fps. Layering custom TSL onto FBX-loaded materials was triggering Three.js to rebuild shader programs every frame, with <code>needsUpdate</code> effectively stuck on. The diagnostic discipline was to strip each foliage material back to a plain textured pass with no custom nodes and watch whether the recompile loop persisted. If it stopped, the custom graph was the culprit; if it continued, the cause was upstream in the FBX material setup or Three.js itself. The fix that let the real shaders come back was binding every per-material difference, leaf color, SSS color, strength, and blend, as uniforms so all leaf assets share one compiled program instead of the compiler emitting a fresh shader for every unique color combination and thrashing the compile queue.</p>
<p>Two more pieces are worth keeping. Grass renders as one <code>InstancedMesh</code> per source file, and its wind is computed in world space because the sin phases key off world position. But the displacement has to be applied in local space before the instance matrix runs, and WGSL has no <code>inverse()</code> to call. For an instance matrix of translation, a Y rotation, and a uniform scale, the inverse of the upper 3×3 is just its transpose divided by the scale squared, so the shader multiplies the world displacement by the transposed model matrix and divides by the squared length of the matrix's first column, recovering the scale without a square root. After the vertex transform re-applies the matrix, the motion lands in world space exactly as authored, independent of each tuft's rotation or scale. And each tuft runs a per-vertex GPU frustum cull: it projects the instance center to clip space, and if it falls outside the frustum with margin, it collapses every vertex to the local origin so all three vertices of each triangle coincide, the rasterizer drops the degenerate triangle, and no fragment, alpha-test, or shadow work happens for off-screen grass. That stacks on top of the coarse per-chunk bounding-sphere cull Three.js already does, built from float <code>step</code> rather than booleans so it multiplies straight into the position mix.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Server-authoritative world persistence.</strong> A <code>WorldChunkDO</code> Durable Object arbitrates prop placement and terrain edits, persists per-chunk binary blobs as the source of truth (so joiners load committed bytes instead of replaying an event log), and stores one storage key per chunk to stay under the DO's 2 MB per-row limit. Player id lives in <code>sessionStorage</code> so tabs are distinct peers; display name lives in <code>localStorage</code> so renames carry across tabs.</p>
<p><strong>Commutative edit convergence.</strong> Additive terrain ops (raise/lower, SDF add/subtract) are commutative and ride an optimistic-stamp path with no coordination. Order-dependent ops (smooth, flatten) are made commutative by shipping precomputed per-cell deltas instead of acquiring a region lock, so the whole system converges under one uniform protocol with no locks. Per-record prop locks replace owner-only deletion, and client-named object ids enable deterministic undo/redo before the server echo arrives. See <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">GPU-driven LOD</a>.</p>
<p><strong>Faithful shader porting from Godot to TSL.</strong> Quaternius's four source wind shaders translate line-for-line: rigid bark, height-masked leaf sway, world-space foliage sway, and grass with a scrolling wind-line bobble, dispatched by the pack's material-naming convention. Leaf color comes from a height gradient plus a Fresnel-driven SSS rim rather than the texture, and sRGB authoring constants are converted to linear so the look matches the reference renders.</p>
<p><strong>Avoiding per-frame shader recompiles.</strong> Layering custom TSL on FBX materials can pin <code>needsUpdate</code> and rebuild programs every frame, collapsing to ~1 fps. Binding every per-material difference as a uniform lets all variants share one compiled program instead of emitting a fresh shader per unique parameter set. A plain-textured diagnostic pass isolates whether the custom graph or the upstream setup is the cause.</p>
<p><strong>World-space wind on instanced foliage.</strong> Grass wind is computed in world space and transformed back to local with a hand-derived inverse (transpose over scale-squared) because WGSL has no <code>inverse()</code>. A per-vertex GPU frustum cull collapses off-screen tufts to a degenerate triangle so no fragment or shadow work runs, stacking on top of Three.js's per-chunk bounding-sphere cull.</p>
<hr>
<p>Part 24 of 29.
Previous: <a href="/blog/2026-05-08-open-world-browser-part-23-avatars-and-voice.html">Part 23 - Fifty avatars and a voice in the room</a>
Next: <a href="/blog/2026-05-10-open-world-browser-part-25-universal-characters.html">Part 25 - One skeleton, every outfit</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 23: Fifty avatars and a voice in the room]]></title>
            <link>https://app.cinevva.com/blog/2026-05-08-open-world-browser-part-23-avatars-and-voice</link>
            <guid>https://app.cinevva.com/blog/2026-05-08-open-world-browser-part-23-avatars-and-voice</guid>
            <pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 45 cut 50 characters from 13 ms of per-frame JS to one draw call with batched GPU skinning, and spent two hours chasing a WGSL error that turned out to be a morph target. Spike 46 built proximity voice with HRTF spatial audio, then deleted half the audio chain to match what Meet and Teams actually ship.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-23-fifty-avatars-and-a-voice-in-the-room" tabindex="-1">Building an open world in the browser, part 23: Fifty avatars and a voice in the room <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-23-fifty-avatars-and-a-voice-in-the-room" aria-label="Permalink to &quot;Building an open world in the browser, part 23: Fifty avatars and a voice in the room&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-07-open-world-browser-part-22-clouds-and-meshlets.html">Part 22</a> put a sky over the world. This part puts people in it. A third-person open world wants 50-plus characters visible at any moment, and it wants to hear the ones standing next to you. Spike 45 is the rendering side: getting that many animated avatars onto the GPU without melting the main thread. Spike 46 is the audio side: peer-to-peer voice that pans and attenuates with position, tuned to sound like a normal video call rather than a tech demo.</p>
<h2 id="one-draw-call-for-fifty-dancers" tabindex="-1">One draw call for fifty dancers <a class="header-anchor" href="#one-draw-call-for-fifty-dancers" aria-label="Permalink to &quot;One draw call for fifty dancers&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/45-avatar-network-sync/" title="Spike 45 Avatar Network Sync" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/45-avatar-network-sync/" target="_blank">Open Spike 45 in a new tab ↗</a> · <a href="/blog/spike-source?spike=45-avatar-network-sync">View source</a></p>
<p>The default Three.js path gives every character its own <code>SkinnedMesh</code>, its own <code>AnimationMixer</code>, its own bone-matrix upload, and its own draw call. At 50 avatars on the local Mac that was about 13 ms of pure JavaScript overhead per frame before the GPU did a single thing. The de-risking question for the whole multiplayer track was whether one batched-skinning architecture could get that under control and scale linearly with character count.</p>
<p>The answer is a split between where the animation is computed and where it's drawn. Three character classes share one FBX template. The local player is a normal <code>Avatar</code>, a full skeleton clone with its own mixer, the standard Three.js path, because there's only ever one of it. Every remote peer is a <code>VirtualSkeleton</code>: also a full clone with its own mixer running the same clips, but every <code>SkinnedMesh</code> node is stripped immediately after cloning so only the bones survive. It never enters the scene. Each frame, after the mixer updates and the matrices settle, it packs <code>(bone.matrixWorld × boneInverse)</code> for all 100 bones into a slot of a shared <code>Float32Array</code>. The <code>BatchSkinnedRenderer</code> then owns one <code>InstancedMesh</code> per geometry piece, all reading from a single <code>StorageBufferAttribute</code> of bone matrices sized <code>maxInstances × numBones × mat4</code>, which is 60 × 100 × 64 = 384 KB. A <code>MeshStandardNodeMaterial</code> with custom <code>positionNode</code> and <code>normalNode</code> reads four bone influences per vertex straight out of that storage buffer. The result is one storage upload and one draw call per geometry piece for the entire crowd, regardless of how many people are in it. Skinning lives in the vertex shader, and the per-avatar JavaScript cost drops to running a mixer and copying 100 matrices.</p>
<p>The HUD that measures this had to be rebuilt too. The old version flagged &quot;over budget&quot; when whole-frame GPU time crossed 3 ms, but the frame always includes the shadow map, the ground, and the local player's full skinned mesh, which together run 3 to 5 ms on real hardware no matter how many synthetic peers exist. The fix is a budget that calibrates itself: while no batched avatars are present, it captures the live GPU time as a baseline through a fast EMA, then freezes that baseline and grows the budget linearly at 0.06 ms per added avatar once synthetics appear. It reads PASS at idle on every machine and tightens proportionally as the crowd grows.</p>
<h2 id="the-bug-was-a-face-you-couldn-t-see" tabindex="-1">The bug was a face you couldn't see <a class="header-anchor" href="#the-bug-was-a-face-you-couldn-t-see" aria-label="Permalink to &quot;The bug was a face you couldn't see&quot;"></a></h2>
<p>First runs in Chrome showed silver shadows on the ground and no avatars at all, with a WGSL parse error: <code>cannot index type 'f32'</code> on a line trying to subscript <code>object.nodeUniform2[i]</code> where the uniform was declared as a scalar. The honest part of this story is that the first fix was wrong and worked anyway. The guess was that <code>InstancedMesh</code>'s instance-matrix path was generating the bad code, and swapping in a <code>StorageInstancedBufferAttribute</code> made the error disappear in Chrome. But it disappeared because the new path emitted different shader code, not because it addressed the cause, which is the most dangerous kind of fix.</p>
<p>The real culprit was morph targets. The 3MIKE.fbx ships with blend-shape facial expressions, the cloned geometry inherits the <code>morphAttributes</code>, and Three.js's <code>MorphNode.setup()</code> declares <code>morphTargetInfluences</code> as a scalar <code>float</code> and then tries to <code>.element(i)</code> it inside a synthesized loop, which is exactly the scalar-subscript the compiler rejected. The fix is one line, clearing <code>geometry.morphAttributes = {}</code> on geometry that doesn't use morphs, so Three.js never injects the <code>MorphNode</code> at all. The accidental Chrome fix stayed in for a while and then bit back: on Safari, the storage-instanced path produced <code>Vertex buffer is not big enough</code> 256 times over, because Safari's WebGPU backend doesn't translate it cleanly. Reverting it was the right call, and the plain bone-matrix storage buffer, which is core WebGPU rather than a generated instance path, works fine everywhere. The lesson is the one worth carrying: when a fix works on one browser and you can't explain the mechanism, you've patched a symptom, so read the actual generated WGSL. A <code>getCompilationInfo()</code> shim added later in the spike turned Three.js's generic &quot;module is not valid&quot; into the real Tint error and paid for itself many times over.</p>
<p>A related framework-evasion trick sits next to it. Three.js detects the standard <code>skinIndex</code> and <code>skinWeight</code> attribute names and tries to inject its own <code>SkinningNode</code>, even on an <code>InstancedMesh</code> whose custom <code>positionNode</code> already does the skinning. Renaming those attributes to <code>boneIndex</code> and <code>boneWeight</code> hides them from the framework, and the custom TSL reads them under the new names.</p>
<h2 id="a-relay-that-forgets-you-between-words" tabindex="-1">A relay that forgets you between words <a class="header-anchor" href="#a-relay-that-forgets-you-between-words" aria-label="Permalink to &quot;A relay that forgets you between words&quot;"></a></h2>
<p>The first version synced peers over <code>BroadcastChannel</code>, a same-browser stand-in with the real wire format and cadence, and the protocol comment promised the swap to real transport would be one line. Cashing that promise meant an <code>AvatarRoomDO</code>, a 74-line Cloudflare Durable Object that doesn't even decode the 36-byte binary frame. It forwards each message as-is to every other peer in the room, because the sender's id is embedded in the frame and each receiver filters its own echo client-side. The relay has zero awareness of identity. Hibernating WebSockets make an idle room free: the DO drops out of memory between messages and the runtime restores the tagged sockets on the next packet. At 10 events per second per peer that's 36,000 DO requests per peer-hour, about half a cent, with egress free on Cloudflare and roughly 6 to 10 times cheaper than the equivalent AWS WebSocket shape.</p>
<p>The swap surfaced a state-machine bug worth keeping. A remote player kept walking after they'd stopped. The animation request guarded against <code>this._state</code>, the currently playing clip, instead of the last queued name, so when two network messages arrived in the same tick, a <code>walk</code> then an <code>idle</code>, the <code>idle</code> compared against a state that hadn't advanced yet and got silently dropped. The peer was stuck walking forever, because future <code>idle</code> packets were deduplicated upstream as unchanged. The fix is to always overwrite the pending name and let the transition helper short-circuit genuine same-state requests, which it already did. The class of bug is general: a dedup check against the wrong reference value quietly swallows the input that matters.</p>
<p>Safari needed two more guards. It opens the WebSocket faster than Chrome, so the first inbound peer message could arrive before the batch renderer finished constructing, dereferencing null; dropping messages while the renderer is absent is safe because peers re-broadcast every 100 ms. And <code>'gpu' in navigator</code> returned true while <code>requestAdapter()</code> returned null, so Three.js silently fell back to WebGL2, where the storage-buffer skinning chain has no valid translation and spewed errors. Checking for a real adapter and asserting the backend is actually WebGPU turns a degraded render into a clear loading-screen message. There was even a WGSL dialect gap: Three.js emits the modern two-argument <code>@interpolate(flat, either)</code> that WebKit's compiler hasn't shipped, patched by rewriting the shader source on the way into <code>createShaderModule</code> to drop the second argument, which is free because flat interpolation carries the same value on every vertex regardless.</p>
<h2 id="voice-that-pans-with-the-room" tabindex="-1">Voice that pans with the room <a class="header-anchor" href="#voice-that-pans-with-the-room" aria-label="Permalink to &quot;Voice that pans with the room&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/46-proximity-voice/" title="Spike 46 Proximity Voice" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/46-proximity-voice/" target="_blank">Open Spike 46 in a new tab ↗</a> · <a href="/blog/spike-source?spike=46-proximity-voice">View source</a></p>
<p>Spike 46 is proximity voice: peer-to-peer WebRTC with HRTF spatial audio, scoped explicitly to match Google Meet and Microsoft Teams quality in a quiet-to-moderately-noisy room. A <code>VoiceRoomDO</code> handles signaling as a JSON relay, sending each new peer a roster, announcing joins and leaves, routing SDP and ICE to one specific peer by socket tag, and broadcasting position updates that drive the spatial panners. It stamps the sender id on every message so peers can't spoof each other, and the audio itself never touches the DO. One <code>RTCPeerConnection</code> per remote peer, with the lexicographically smaller peer id always making the offer so both sides agree on who initiates without a full perfect-negotiation implementation.</p>
<p>On the receive side each peer's audio runs through a <code>PannerNode</code> set to HRTF with inverse distance rolloff, and the <code>AudioListener</code> updates every frame from the local player's position and facing using <code>forwardX = sin(facing)</code>, <code>forwardZ = cos(facing)</code>, which matches the scene's <code>atan2(wx, wz)</code> facing convention. One Chrome quirk cost an hour: a <code>MediaStream</code> consumed only by Web Audio sometimes won't pull packets, so each stream also attaches to a hidden muted <code>&lt;audio&gt;</code> element to force the decoder to schedule. On the quality side, browsers default to roughly 32 kbps mono Opus, so the spike munges the <code>fmtp</code> line on every offer and answer to bump it to 128 kbps with in-band FEC enabled and DTX disabled, then calls <code>setParameters</code> with a high max bitrate to guarantee the encoder actually uses what the SDP advertises. FEC is the second-biggest audible win after the bitrate bump, recovering from packet loss without renegotiation.</p>
<h2 id="deleting-your-way-to-clean-audio" tabindex="-1">Deleting your way to clean audio <a class="header-anchor" href="#deleting-your-way-to-clean-audio" aria-label="Permalink to &quot;Deleting your way to clean audio&quot;"></a></h2>
<p>The audio chain that shipped is much smaller than the one I started with, and shrinking it was the real lesson. The first version had a high-pass filter, a click limiter tuned to catch keyboard noise, a compressor, a noise gate, and a wet-dry crossfade, fronted by a floating panel with twelve-plus sliders. When the user reported audible keyboard clicks, the instinct was to tune the click limiter harder and drop the dry mix, a stack of bandaids. The structural answer was that once an ML denoiser is in the chain, the click limiter and the gate and most of the high-pass are all redundant, because RNNoise is trained on exactly keyboard and mouse and typing noise, and amplitude clipping is a strictly worse version of the same job. Production clients ship ML denoise, echo cancellation, automatic gain, and a soft compressor for level, and nothing else. So four stages came out, the slider panel came out, and the &quot;choose your noise reduction&quot; toggles came out, leaving one fixed pipeline.</p>
<p>Each surviving stage earns its place. Browser echo cancellation stays on because RNNoise doesn't do echo, and without it speaker-into-mic feedback is unbounded. Browser noise suppression goes off because stacking it on RNNoise produces artifacts on fricatives, so you pick one denoiser. Browser automatic gain stays on, because turning it off made the signal too quiet for the compressor to work with and Web Audio's <code>DynamicsCompressorNode</code> has no makeup-gain parameter to compensate; the browser's broad levelling and the spike's fast compressor operate on different timescales and coexist. RNNoise runs at 92 percent wet mixed with 8 percent dry, because it can over-suppress unvoiced consonants like s, sh, and f whose voice probability dips, and the small dry path preserves them at the cost of a little keystroke leak.</p>
<p>Two features round it out. Push-to-talk doesn't flip <code>track.enabled</code>, because that discards everything still in the pipeline buffers and chops the last syllable on key release. Instead a <code>GainNode</code> near the tail ramps with <code>setTargetAtTime</code>, fast attack so the first syllable survives and slow release so the last consonant drains, with the track left permanently enabled. And a five-second broadcast delay, requested as a radio-style feature, runs a bypass leg and a <code>DelayNode</code> leg crossfaded together, with a dump button that snaps the delayed output to silence and counts down on the HUD before audio resumes. Bundling the denoiser was its own small saga: the published RNNoise worklet uses bare-specifier imports that no CDN resolves, so the fix was a local esbuild bundle producing one self-contained 1.9 MB file with the WASM base64-inlined, committed to the repo and referenced by a URL relative to the module so it resolves under the dev server, the VitePress build, and the custom domain alike. If the worklet ever fails to load, the chain still produces audio through a plain high-pass and compressor, and the HUD shows the failure in red.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Batched GPU skinning for crowds.</strong> Remote avatars run a headless <code>VirtualSkeleton</code> (a full clone with the skinned meshes stripped, bones kept, its own mixer) that packs <code>bone.matrixWorld × boneInverse</code> for every bone into a shared <code>StorageBufferAttribute</code>. One <code>InstancedMesh</code> per geometry piece reads those matrices in a custom TSL <code>positionNode</code>/<code>normalNode</code>, so the entire crowd costs one storage upload and one draw call per piece, with per-avatar CPU work limited to a mixer update and a matrix copy. See <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">GPU-driven LOD</a>.</p>
<p><strong>Reading the generated WGSL, not the symptom.</strong> A <code>cannot index type 'f32'</code> compile error traced to Three.js's <code>MorphNode</code> declaring <code>morphTargetInfluences</code> as a scalar and subscripting it, fixed by clearing <code>morphAttributes</code> on geometry that doesn't use morphs. A first fix that only changed which shader path was generated masked the cause and broke Safari later. Renaming <code>skinIndex</code>/<code>skinWeight</code> to <code>boneIndex</code>/<code>boneWeight</code> hides the attributes from Three.js's automatic <code>SkinningNode</code> injection so a custom skinning material owns the math.</p>
<p><strong>Hibernating Durable Object relays.</strong> A pure-binary <code>AvatarRoomDO</code> forwards 36-byte frames to every other peer without decoding them, with sender identity embedded in the frame and self-echo filtered client-side. Hibernating WebSockets make an idle room free, and the shape costs about half a cent per peer-hour at 10 Hz, far below the equivalent managed-WebSocket pricing. A dedup guard that compared against the playing animation state rather than the last-queued one silently dropped stop messages and stuck remote players in a walk loop.</p>
<p><strong>WebRTC proximity voice with HRTF.</strong> One <code>RTCPeerConnection</code> per peer with offer/answer roles decided by peer-id ordering, audio routed through a HRTF <code>PannerNode</code> with an <code>AudioListener</code> updated per frame from player facing, and Opus munged to 128 kbps with in-band FEC for resilience. A muted hidden <code>&lt;audio&gt;</code> element forces Chrome to pull packets from a Web-Audio-only stream.</p>
<p><strong>Subtractive audio engineering.</strong> Matching Meet/Teams quality meant removing stages, not adding them: ML denoise plus echo cancellation plus automatic gain plus a soft compressor, with no gate and no click limiter, because an ML denoiser trained on keyboard noise makes amplitude clipping redundant. Push-to-talk ramps a tail <code>GainNode</code> with an asymmetric envelope instead of flipping the track so syllables aren't clipped, and the denoiser worklet ships as a single self-contained esbuild bundle to dodge bare-specifier import resolution.</p>
<hr>
<p>Part 23 of 29.
Previous: <a href="/blog/2026-05-07-open-world-browser-part-22-clouds-and-meshlets.html">Part 22 - Clouds you can light, and culling that has to be fed</a>
Next: <a href="/blog/2026-05-09-open-world-browser-part-24-persistence-and-wind.html">Part 24 - Saving a world, and wind you can see</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 22: Clouds you can light, and culling that has to be fed]]></title>
            <link>https://app.cinevva.com/blog/2026-05-07-open-world-browser-part-22-clouds-and-meshlets</link>
            <guid>https://app.cinevva.com/blog/2026-05-07-open-world-browser-part-22-clouds-and-meshlets</guid>
            <pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 43 put a physically based sky and volumetric clouds on screen for under 2 ms, with sunsets that need no per-time-of-day tuning. Spike 44 built meshlet GPU culling and learned that Hi-Z occlusion only works if the occluders are big enough to survive a max-reduce.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-22-clouds-you-can-light-and-culling-that-has-to-be-fed" tabindex="-1">Building an open world in the browser, part 22: Clouds you can light, and culling that has to be fed <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-22-clouds-you-can-light-and-culling-that-has-to-be-fed" aria-label="Permalink to &quot;Building an open world in the browser, part 22: Clouds you can light, and culling that has to be fed&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-06-open-world-browser-part-21-visibility-buffer.html">Part 21</a> was about a rendering technique that didn't pay off. This part has one that did, and one that needed a careful fix to work at all. Spike 43 is the sky: a physically based atmosphere and volumetric clouds, the foundation that makes a scene read as a place instead of a tech demo. Spike 44 is meshlet-style GPU culling, where the lesson was that the occlusion test is only as good as the occluders you feed it.</p>
<h2 id="a-sky-from-physics-not-a-gradient" tabindex="-1">A sky from physics, not a gradient <a class="header-anchor" href="#a-sky-from-physics-not-a-gradient" aria-label="Permalink to &quot;A sky from physics, not a gradient&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/43-clouds-atmosphere/" title="Spike 43 Clouds and Atmosphere" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/43-clouds-atmosphere/" target="_blank">Open Spike 43 in a new tab ↗</a> · <a href="/blog/spike-source?spike=43-clouds-atmosphere">View source</a></p>
<p>Almost every cinematic weather effect depends on two pieces of infrastructure: a physically based atmosphere, so the sky color and sun color follow time of day from physics rather than a hand-tuned gradient, and a volumetric cloud volume, so the sky has 3D structure instead of a baked cubemap. Spike 43 builds exactly that pair on the existing WebGPU and TSL stack, nothing more, because once those two exist the rest of the weather stack (fog, god rays, wet surfaces, snow) becomes a series of smaller known follow-ons.</p>
<p>The atmosphere is the Hillaire 2020 model, a set of lookup tables computed in WGSL compute shaders. A transmittance table integrates sun light through the Rayleigh, Mie, and ozone density profile and recomputes only when the sun moves. A sky-view table re-bakes every frame because it's cheap enough that gating it isn't worth the code, with a non-linear parameterization around the horizon to avoid banding. Multiple scattering uses an analytic fit instead of the proper table for now, and the sunsets read correctly, so the shortcut is hiding well. The clouds are a Schneider Nubis-style ray-march through a horizontal slab, with shape from a 128³ Perlin-Worley texture eroded by a 32³ Worley texture, both baked at boot in compute with no network fetch, lit with Beer's law extinction, a dual-lobe phase function, and a powder approximation. The key coupling is that the cloud's sun color samples the same transmittance table every step, so cloud lighting tracks sunset without a second tuning pass.</p>
<p>On an M1 the whole thing lands at 1.1 to 2.0 ms at half-resolution clouds, well under the 6 ms budget, using about 14 MB of GPU memory, running at over 100 FPS. The two thesis claims held up in practice. Sunset is the hero shot, the moment that makes the renderer feel cinematic, and it falls out of the physics without per-time-of-day tuning. And a happy accident: with the cloud slab parameterized between 800 m and 4000 m, distant clouds at the horizon read as dark mountain ridges from a low camera, which gives the world background terrain without anyone modeling background terrain.</p>
<p>One architecture note worth keeping. The natural shape is to paint the sky into the swap chain first, then let three.js draw geometry on top with <code>autoClear = false</code>. That does not survive the WebGPU renderer on r184, because the flag doesn't gate the color load op the way it does in WebGL, so three.js clobbers the sky every frame. The fix is to render three.js into an offscreen target and do the final composite (<code>mix(skyCloud, scene, scene.alpha)</code> then ACES then sRGB) in our own pass that owns the swap chain.</p>
<h2 id="culling-that-s-only-as-good-as-its-occluders" tabindex="-1">Culling that's only as good as its occluders <a class="header-anchor" href="#culling-that-s-only-as-good-as-its-occluders" aria-label="Permalink to &quot;Culling that's only as good as its occluders&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/44-meshlet-clusters/" title="Spike 44 Meshlet Clusters" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/44-meshlet-clusters/" target="_blank">Open Spike 44 in a new tab ↗</a> · <a href="/blog/spike-source?spike=44-meshlet-clusters">View source</a></p>
<p>Spike 44 benches four rendering modes against each other: plain forward, CPU cluster culling, GPU compute culling, and a visibility buffer with Hi-Z occlusion culling. The Hi-Z path is the interesting one, and it had a quiet bug: its HUD said occlusion was on, but the &quot;Hi-Z killed&quot; counter sat at exactly 0.0% forever. Frustum culling worked, so the cull shader's upstream was fine. The occlusion half was a no-op paying full cost.</p>
<p>A Hi-Z occlusion test projects a cluster's bounding box to screen, picks a depth-pyramid mip level so the screen rectangle is about 2×2 texels, samples the deepest occluder depth in that rectangle, and rejects the cluster if its closest point is still farther than that occluder. The depth pyramid is built every frame by seeding mip 0 from an opaque-depth pre-pass and max-reducing up. The opaque pre-pass deliberately includes only solid occluders, the ground and per-tree trunk proxies, because alpha-tested foliage would punch gaps that fool a max-reduce.</p>
<p>The bug was geometric, not logical. The trunk proxy was a 0.5 m by 4 m by 0.5 m box. At 30 m it projects to about 17 pixels on screen. But a typical 50 m grass cluster selects mip 5, where each texel covers 32 source pixels. A 17-pixel trunk doesn't fully cover a single mip-5 texel, so every texel touching the trunk also touches surrounding ground. The very first 2×2 max-reduce picks the larger depth value, which is the farther ground behind the trunk, and the trunk's depth gets erased on the first reduction. By mip 5 the pyramid holds ground depth almost everywhere, the cluster is never farther than the ground, and nothing is ever occluded.</p>
<p>The fix is to make the proxy big enough to dominate the texels it lands in, sizing it to the tree's silhouette rather than its wood. A roughly 2 m by 6 m by 2 m proxy is still smaller than the actual canopy, so leaves visible through gaps never get over-culled, but it's large enough to survive the max-reduce out to the distances that matter, and the occlusion counter immediately went non-zero. The takeaway generalizes to a rule for the production engine: anything trusted as a Hi-Z occluder has to be sized to its on-screen silhouette, because Hi-Z effectiveness in open foliage scenes is dominated by occluder coverage at the relevant mip, not by the elegance of the depth-test math. Grass-versus-grass occlusion can't fire anyway, since a blade sits at the same depth as the ground under it, so the real wins are trees occluding distant foliage and trees occluding trees.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Hillaire 2020 atmosphere LUTs.</strong> A transmittance table (sun light through the Rayleigh, Mie, and ozone profile) recomputed only on sun movement, plus a per-frame sky-view table with a non-linear horizon parameterization, give physically based sky and sun colors that track time of day with no hand-tuned gradient. An analytic multiple-scattering fit substitutes for the full table until an artifact forces the proper bake. Sunset falls out of the physics without per-time-of-day tuning.</p>
<p><strong>Schneider Nubis volumetric clouds.</strong> A ray-march through a horizontal slab, shaped by a boot-baked 128³ Perlin-Worley texture eroded by a 32³ Worley texture, lit with Beer's law extinction, a dual-lobe phase, and a powder term. Sampling cloud sun color from the same transmittance table every step makes cloud lighting track sunrise and sunset for free. Half-resolution ray-march runs roughly 4× cheaper than full with no visible quality loss at typical distance, the standard production trade.</p>
<p><strong>Compositing raw WebGPU with three.js on r184.</strong> Painting the sky into the swap chain and drawing three.js geometry over it with <code>autoClear = false</code> fails because the flag doesn't gate the color load op in the WebGPU backend. Render three.js into an offscreen RGBA16F target and do the final <code>mix</code> plus tone-map plus sRGB in a pass that owns the swap chain.</p>
<p><strong>Hi-Z occlusion culling and occluder sizing.</strong> A depth pyramid built by max-reduce lets a GPU cull pass reject clusters whose nearest point is behind the deepest occluder in their screen rectangle. The test silently does nothing if occluders are too small to dominate a texel at the selected mip, because the first max-reduce replaces the occluder's depth with the farther background behind it. Occluders must be sized to their on-screen silhouette, not their physical core. See <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">GPU-driven LOD</a>.</p>
<hr>
<p>Part 22 of 29.
Previous: <a href="/blog/2026-05-06-open-world-browser-part-21-visibility-buffer.html">Part 21 - A faster renderer that wasn't faster</a>
Next: <a href="/blog/2026-05-08-open-world-browser-part-23-avatars-and-voice.html">Part 23 - Fifty avatars and a voice in the room</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 21: A faster renderer that wasn't faster]]></title>
            <link>https://app.cinevva.com/blog/2026-05-06-open-world-browser-part-21-visibility-buffer</link>
            <guid>https://app.cinevva.com/blog/2026-05-06-open-world-browser-part-21-visibility-buffer</guid>
            <pubDate>Wed, 06 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 40 built a visibility buffer for dense grass, the technique everyone says wins on overdraw, and measured it losing to plain forward rendering by 44% at high density on Apple Silicon. Here's why, and when to actually reach for it.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-21-a-faster-renderer-that-wasn-t-faster" tabindex="-1">Building an open world in the browser, part 21: A faster renderer that wasn't faster <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-21-a-faster-renderer-that-wasn-t-faster" aria-label="Permalink to &quot;Building an open world in the browser, part 21: A faster renderer that wasn't faster&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-05-open-world-browser-part-20-parallax-occlusion.html">Part 20</a> faked surface depth on a flat quad. This part is about a render-architecture decision, and it's the spike where the textbook answer turned out to be wrong for our hardware. The question: for cinematic-density alpha-tested grass under a third-person camera, do we need a visibility buffer before pushing to 200-player density? The going advice is an emphatic yes. We built it, measured it, and the answer was no.</p>
<h2 id="the-technique-everyone-recommends" tabindex="-1">The technique everyone recommends <a class="header-anchor" href="#the-technique-everyone-recommends" aria-label="Permalink to &quot;The technique everyone recommends&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/40-visibility-buffer/" title="Spike 40 Visibility Buffer" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/40-visibility-buffer/" target="_blank">Open Spike 40 in a new tab ↗</a> · <a href="/blog/spike-source?spike=40-visibility-buffer">View source</a></p>
<p>A visibility buffer splits rendering into two passes. Pass 1 rasterizes geometry and writes only triangle and instance IDs into a compact integer target plus depth, doing no shading at all. Pass 2 is a fullscreen pass that reads the IDs at each covered pixel, refetches that triangle's vertices, reconstructs the interpolated attributes, and shades each visible pixel exactly once. The pitch is perfect overdraw rejection: the depth test runs against fragments that did no shading work, so the expensive material only runs on what you actually see.</p>
<p>The spike runs two paths on one canvas and one device so the only variable is where the shading lives. The forward path is a normal <code>MeshStandardNodeMaterial</code> through three.js. The vis-buffer path is a raw two-pass WebGPU pipeline running outside three.js, reading three.js's grass texture straight off the backend, writing <code>(instanceId, triId)</code> into an <code>RG32Uint</code> target in pass 1 and resolving lighting in pass 2. Both share one ground-truth lighting setup.</p>
<p>Two implementation notes worth keeping. WebGPU still has no portable <code>primitive_index</code> builtin in fragment shaders, so the trick is to bake a per-vertex triangle ID onto un-indexed geometry and read it <code>flat</code>-interpolated, which costs 3× the vertex count but is negligible on a 12-vertex grass card. And sharing the canvas with three.js's renderer is mostly a non-event as long as you never reconfigure the context or touch the canvas dimensions, both of which three.js owns. Timing the forward path was the fiddly part, since three.js exposes no hook to inject GPU timestamp queries inside its render pass; the workaround brackets its work with two no-op timestamp passes submitted before and after, which the GPU runs in submission order.</p>
<h2 id="the-numbers-go-the-wrong-way" tabindex="-1">The numbers go the wrong way <a class="header-anchor" href="#the-numbers-go-the-wrong-way" aria-label="Permalink to &quot;The numbers go the wrong way&quot;"></a></h2>
<p>On an M-series Mac at ~1080p, with cross-card grass blades over an 80 m field:</p>
<p>At 50,000 instances the vis-buffer path won by 25%, 4.13 ms against forward's 5.51 ms. At 100,000 it was break-even. At 200,000 instances forward won by 44%, 5.44 ms against the vis-buffer's 7.80 ms. The vis-buffer path gets <em>relatively worse</em> as density climbs, which is the exact opposite of the folklore that says it wins precisely when overdraw is heavy.</p>
<h2 id="why-forward-holds-up" tabindex="-1">Why forward holds up <a class="header-anchor" href="#why-forward-holds-up" aria-label="Permalink to &quot;Why forward holds up&quot;"></a></h2>
<p>Apple Silicon GPUs are tile-based deferred renderers, and that changes the whole calculation. Forward shading on a TBDR has a hidden-surface-removal stage that runs before the fragment shader: the rasterizer collects every fragment mapping to a tile, sorts them by depth, and only the survivors (post-alpha-test) ever reach the fragment shader. So the forward path is already paying most of the visibility buffer's &quot;shade once per pixel&quot; promise, for free, inside the hardware. As blades crowd the screen, more fragments get rejected at HSR before any shading fires, and forward's effective per-pixel cost stays roughly flat instead of growing with overdraw.</p>
<p>Pass 1 of the vis-buffer path gets that same TBDR benefit. The trouble is all in pass 2. Pass 2 reads each pixel's instance matrix out of a buffer that, at 200,000 instances, is 12.8 MB, far larger than any GPU cache. Screen-adjacent pixels usually belong to different grass instances (the scatter is a jittered grid, so neighboring blades have arbitrary instance IDs), so every wave hitting that buffer misses cache divergently. That incoherent random access hides about 4 ms per frame on its own. Forward dodges it entirely because the instance matrix arrives with the vertex through the per-instance attribute path, so by the time the fragment shader runs the transformed vertex data is already in tile-local registers, no megabyte-scale random read required.</p>
<p>This is exactly the cost Nanite's material-classification pass exists to amortize: bin pixels by instance and dispatch sorted compute waves so each wave's reads are coherent. We don't have that. A back-of-envelope says sorting pixels by instance would drop that 4 ms to maybe 1.5 to 2 ms and push the crossover out to 400,000 to 500,000 instances. But that's stacking optimizations on an architecture that isn't winning here in the first place.</p>
<h2 id="the-honest-conclusion-and-the-audit-that-earned-it" tabindex="-1">The honest conclusion, and the audit that earned it <a class="header-anchor" href="#the-honest-conclusion-and-the-audit-that-earned-it" aria-label="Permalink to &quot;The honest conclusion, and the audit that earned it&quot;"></a></h2>
<p>For alpha-tested cross-card foliage on Apple Silicon WebGPU, the forward path with three.js's TSL pipeline is already at or below vis-buffer cost, and the vis-buffer plumbing buys nothing visible until well past 200,000 instances and only if you also add a sorting or binning pass. The practical call for the production engine is to keep the forward plus LOD plus imposter stack from the earlier spikes and not invest in vis-buffer infrastructure until either we target discrete NVIDIA or AMD GPUs as the dominant deployment (where overdraw cost is more linear) or we move to a meshlet architecture where the vis-buffer is the natural output anyway.</p>
<p>Because that result is counterintuitive, the conclusion is only worth anything if the comparison is fair, so the spike got a full audit pass. Several real bugs surfaced and got fixed: a blade-scale slider that silently desynced the two paths, half the forward blades rendering pitch-dark from anti-parallel normals (fixed with the canonical upward-normal foliage trick), and the vis-buffer reading about 2× too bright from a hand-picked Lambert factor instead of the energy-conserving <code>1/π</code>, a hardcoded ambient term, and missing tone mapping. The fix copied three.js's exact ACES filmic curve into WGSL and reads the light colors and intensities off the actual scene lights each frame. The remaining known gap, missing direct specular in pass 2, biases the comparison <em>toward</em> the vis-buffer, meaning forward is doing strictly more per-pixel work and still winning at high density. That makes the headline conclusion conservative, not optimistic. The one caveat that stands: this is all M-series-specific, and the crossover may well invert on a discrete card, so it's worth re-running before committing the stack for non-Apple targets.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Visibility buffer rendering.</strong> Pass 1 rasterizes geometry and writes only triangle and instance IDs plus depth, doing no shading. Pass 2 is a fullscreen resolve that reads the IDs per covered pixel, refetches the source triangle, reconstructs perspective-correct barycentric attributes, and shades each visible pixel once. Since WebGPU lacks a portable fragment <code>primitive_index</code>, the triangle ID is baked as a <code>flat</code>-interpolated per-vertex attribute on un-indexed geometry.</p>
<p><strong>TBDR hidden-surface removal versus deferred resolve.</strong> On a tile-based deferred GPU (Apple Silicon), forward shading already rejects occluded fragments before the fragment shader runs, so it captures most of the visibility buffer's shade-once benefit for free, and its per-pixel cost stays roughly flat as overdraw grows. A vis-buffer resolve pass instead pays for incoherent random access into a large per-instance buffer (12.8 MB at 200k instances), which dominates at high density unless pixels are sorted or binned by instance first, the way Nanite's material classification does.</p>
<p><strong>Sharing a canvas with three.js's WebGPURenderer.</strong> Raw WebGPU command buffers interleave correctly with three.js submissions on the shared queue as long as you never call <code>context.configure()</code> again or write <code>canvas.width/height</code>, both of which the renderer owns. Forward-path GPU timing, which three.js doesn't expose a hook for, can be bracketed by two no-op timestamp render passes submitted around its render call, since the GPU runs command buffers in submission order.</p>
<p><strong>Validating a counterintuitive benchmark.</strong> A surprising performance result is only as trustworthy as the fairness of the comparison. Auditing both paths to identical scene content and shading (matched ACES tone mapping, energy-conserving Lambert, lights read from the same objects, identical blade scale) is what turned &quot;vis-buffer is slower&quot; from a likely measurement artifact into a defensible conclusion, with the one remaining asymmetry biased in the conservative direction.</p>
<hr>
<p>Part 21 of 29.
Previous: <a href="/blog/2026-05-05-open-world-browser-part-20-parallax-occlusion.html">Part 20 - Faking depth on a flat plane</a>
Next: <a href="/blog/2026-05-07-open-world-browser-part-22-clouds-and-meshlets.html">Part 22 - Clouds you can fly through, and culling that pays off</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 20: Faking depth on a flat plane]]></title>
            <link>https://app.cinevva.com/blog/2026-05-05-open-world-browser-part-20-parallax-occlusion</link>
            <guid>https://app.cinevva.com/blog/2026-05-05-open-world-browser-part-20-parallax-occlusion</guid>
            <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 39 ported parallax occlusion mapping into TSL, hit two WebGPU shader-control-flow walls, found the bug was a verbatim-port discipline failure, and built a real-geometry reference plane to keep itself honest.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-20-faking-depth-on-a-flat-plane" tabindex="-1">Building an open world in the browser, part 20: Faking depth on a flat plane <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-20-faking-depth-on-a-flat-plane" aria-label="Permalink to &quot;Building an open world in the browser, part 20: Faking depth on a flat plane&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-04-open-world-browser-part-19-imposters.html">Part 19</a> used a flat quad to fake a whole tree at distance. This part uses a flat quad to fake depth up close: parallax occlusion mapping, the trick that makes a cobblestone road look like it has 5 cm of recessed grouting without spending a single extra vertex. The goal was to land it on the production stack (Three.js r184, WebGPU, TSL) so terrain detail materials can carry that depth illusion where it matters and pay flat-texture cost everywhere else.</p>
<h2 id="three-ways-to-fake-depth-side-by-side" tabindex="-1">Three ways to fake depth, side by side <a class="header-anchor" href="#three-ways-to-fake-depth-side-by-side" aria-label="Permalink to &quot;Three ways to fake depth, side by side&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/39-parallax-pom/" title="Spike 39 Parallax Occlusion Mapping" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/39-parallax-pom/" target="_blank">Open Spike 39 in a new tab ↗</a> · <a href="/blog/spike-source?spike=39-parallax-pom">View source</a></p>
<p>The spike lays three flat 5×5 m planes side by side, all the same material scaffold, differing only in the UV that feeds the samplers. Flat samples the texture straight, the reference baseline. Single-sample parallax shifts the UV once along the view direction by the height at that point, which is cheap and OK at low amplitude but swims at grazing angles. POM ray-marches in tangent space: step along the view ray, find the first layer where the ray passes below the heightfield, and refine the crossing. Tangent space stays simple because every test plane is axis-aligned, so the view direction packs into tangent space with a couple of sign flips instead of a full per-vertex TBN matrix. The texture set pulls live from the Polyhaven file API, the same path Part 17's model search used.</p>
<h2 id="two-webgpu-walls-and-a-branchless-ray-march" tabindex="-1">Two WebGPU walls, and a branchless ray-march <a class="header-anchor" href="#two-webgpu-walls-and-a-branchless-ray-march" aria-label="Permalink to &quot;Two WebGPU walls, and a branchless ray-march&quot;"></a></h2>
<p>The textbook POM loop breaks out of the search on the first crossing. On r184 that doesn't work, for two separate reasons. <code>If(...).and(...)</code> compiled without errors but produced WGSL where the loop body never executed, so the post-loop refinement ran on garbage and the plane rendered nearly white. And <code>Break()</code> as a standalone node hadn't shipped to the r184 build at all, so even with a working <code>If</code> there was no way to express &quot;stop on first crossing.&quot; Both trace back to known three.js issues around TSL control flow over-optimizing across <code>If</code> and <code>Loop</code> boundaries in this version range.</p>
<p>The rewrite is branchless. Every iteration unconditionally samples the texture, which keeps texture access in uniform control flow as the WGSL spec wants, then folds the new state in through a <code>done</code> flag held as a float. Once <code>done</code> flips to 1, the per-iteration <code>mix</code> calls degenerate to &quot;keep state unchanged,&quot; which is the branchless equivalent of a break. The <code>done</code> flag is built with a <code>step</code> helper implemented as <code>0.5 + 0.5 × sign(x + ε)</code> because boolean-to-float coercion has been spotty across the r18x line and <code>sign()</code> is universally safe. The cost is that every fragment runs all 64 iterations regardless of where it actually crosses, but that's the right trade at fragment scale: the runtime gates on max steps anyway, and a real GPU would speculate past a &quot;real&quot; break too. A clean fallback matters here, a final <code>mix(baseUV, refined, done)</code>, so that at zero amplitude (the far end of the distance fade) no fragment crosses, <code>done</code> stays 0, and the POM material is bit-identical to flat. That's the whole point of the distance-LOD trick: collapse to flat cost where the effect is sub-pixel anyway.</p>
<h2 id="the-bug-was-a-discipline-failure-not-a-math-failure" tabindex="-1">The bug was a discipline failure, not a math failure <a class="header-anchor" href="#the-bug-was-a-discipline-failure-not-a-math-failure" aria-label="Permalink to &quot;The bug was a discipline failure, not a math failure&quot;"></a></h2>
<p>The branchless version ran but looked distorted, streaky horizontal artifacts at moderate amplitude and a subtly-wrong-but-not-crisp result at low amplitude. The fix came from a one-line prompt: go read the canonical reference. The LlamAcademy tutorial that inspired this is just a Unity ShaderGraph node, so the real implementation lives in Unity's <code>PerPixelDisplacement.hlsl</code>. Reading it line by line surfaced three semantic differences I'd unwittingly introduced: an off-by-one in the ray-height baseline (Unity does an initial advance before the loop, so my frame of reference was a whole step out of phase, landing crossings in the wrong layer about half the time), a sign convention on the max offset that the refinement step depends on, and a cumulative-offset versus cumulative-UV bookkeeping choice that made my refinement math work harder and tangled the sign.</p>
<p>The root cause wasn't any single error, it was mixing two references. I'd taken the LearnOpenGL POM tutorial as my guide, which uses similar but different sign conventions and a different refinement formula, and ended up in a mongrel state where two-thirds of the math matched one source and one-third matched the other. The rewrite is a near-verbatim port of Unity's HLSL into TSL, same variable names, same initial advance, same refinement, with the branchless <code>done</code> flag kept on top. The lesson is worth carrying: when you port a known-good shader from another stack, port it line-for-line with the same names first, then refactor for local style. Don't re-derive against a second reference mid-port.</p>
<h2 id="a-reference-plane-that-can-t-lie" tabindex="-1">A reference plane that can't lie <a class="header-anchor" href="#a-reference-plane-that-can-t-lie" aria-label="Permalink to &quot;A reference plane that can't lie&quot;"></a></h2>
<p>The side-by-side was missing the obvious thing: a real-geometry plane. Without it, &quot;POM looks pretty good&quot; is unfalsifiable. Pretty good compared to what? So the spike added a fourth plane, the same heightmap pushed through actual vertex positions. WebGPU has no hardware tessellation (it's simply not in the spec, cut for Metal compatibility), so the substitute is a densely subdivided plane (256×256 segments, 131,072 triangles) with vertex displacement in the vertex stage. The same amplitude uniform drives both POM and the geometry plane, so they fade together and the comparison stays apples-to-apples at every distance.</p>
<p>With ground truth on screen, the qualitative claims became measurable. At a 16° orbit looking down, POM and the tessellated plane agree on internal shading. At grazing angles they diverge exactly where they must: POM clamps to the geometry's perfectly straight rectangular edge, while the real mesh shows a bumpy horizon profile of actual peaks and valleys catching the light. So POM's edge &quot;swimming&quot; is now provably intrinsic to the algorithm, not an artifact of the texture or lighting. The two cost shapes are also clear: POM is fragment-bound (cost scales with covered pixels), the tessellated plane is vertex-bound (cost scales with mesh density regardless of coverage). For a terrain chunk, which already pays the vertex cost of a heightmap-driven plane, POM is the right answer for sub-mesh detail.</p>
<p>The reference plane also caught a subtle UX bug. The user noticed the surface appeared to sink as amplitude increased. That traced to Unity's convention treating the geometric plane as the top of the heightfield, so peaks anchor flush and everything else parallaxes downward, dragging the average surface below the flat baseline by <code>(1 − mean_h) × amplitude</code>. The fix re-centers the convention so <code>h = 0.5</code> is the plane, peaks rise toward the camera and valleys recess. The algorithm runs exactly as Unity prescribes; the spike just post-processes the output by half an offset to match what &quot;amplitude&quot; should mean to a person dragging a slider.</p>
<p>One more thing the reference plane settled. A &quot;Steps&quot; slider seemed to do nothing, which read like a plumbing bug but wasn't. The three-iteration secant refinement after the linear search is so good (Tatarchuk's 2006 POM paper notes a 4-step search plus 3-step secant is visually indistinguishable from a 64-step search) that on a smooth heightmap every step count from 4 to 64 converges to the same sub-texel UV. The fix was a toggle, not a re-plumbing: turn the secant off and the step slider becomes the only control over crossing precision, so dropping to 4 visibly stair-steps the cobblestone and cranking to 64 smooths it back. The toggle is a 0/1 uniform that <code>mix</code>es every secant state update to a no-op when off, so toggling never rebuilds the material and never stutters.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Parallax occlusion mapping in TSL.</strong> POM ray-marches the view direction through a heightfield in tangent space, finds the first layer where the ray drops below the surface, and refines the crossing, producing recessed-mortar depth on a flat quad with no extra geometry. A final <code>mix(baseUV, refined, done)</code> makes the material bit-identical to flat when no fragment crosses, which is what lets distance-LOD amplitude attenuation collapse the cost to flat-texture cost at range. See <a href="/guides/landscape-generation-browser.html#terrain-materials">terrain materials</a>.</p>
<p><strong>Branchless loops for WebGPU control flow.</strong> On Three.js r184, TSL <code>If(...).and(...)</code> can compile to WGSL whose loop body never runs, and standalone <code>Break()</code> isn't available. The portable pattern is an unconditional texture sample per iteration (keeping texture access in uniform control flow per the WGSL spec) plus a <code>done</code> flag held as a float that <code>mix</code>es each state update to a no-op once set. A <code>step</code> helper built from <code>sign(x + ε)</code> avoids unreliable boolean-to-float coercion. Cost is constant max iterations regardless of early-out point, the correct trade at fragment scale.</p>
<p><strong>Verbatim shader porting.</strong> Porting a known-good shader from another engine should be line-for-line with the original's variable names first, refactor for local style second. Mixing two references (Unity's <code>PerPixelDisplacement.hlsl</code> and the LearnOpenGL tutorial) produced a mongrel with an off-by-one ray baseline, an inverted offset sign, and a refinement formula whose clamp masked out-of-range weights as spatial discontinuities. One canonical ground truth, not a re-derivation.</p>
<p><strong>Vertex-displaced ground-truth reference.</strong> With no hardware tessellation in WebGPU, a densely subdivided plane (256² segments) displaced in the vertex stage stands in as real geometry to validate a fragment-stage fake. Driving both with the same amplitude uniform keeps the comparison honest across distance. POM is fragment-bound (scales with covered pixels) and the geometry plane is vertex-bound (scales with mesh density), so they diverge exactly at silhouette edges, proving POM's edge swim is intrinsic, not an artifact.</p>
<hr>
<p>Part 20 of 29.
Previous: <a href="/blog/2026-05-04-open-world-browser-part-19-imposters.html">Part 19 - The imposter that has to survive a forest</a>
Next: <a href="/blog/2026-05-06-open-world-browser-part-21-visibility-buffer.html">Part 21 - A faster renderer that wasn't faster</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 19: The imposter that has to survive a forest]]></title>
            <link>https://app.cinevva.com/blog/2026-05-04-open-world-browser-part-19-imposters</link>
            <guid>https://app.cinevva.com/blog/2026-05-04-open-world-browser-part-19-imposters</guid>
            <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 38 baked a tree down to two textures on a billboard, then a KISS rewrite and an odd-N fix made it match. Spikes 41-42 killed the snap with hemi-octahedral blending and pushed a million trees onto the GPU.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-19-the-imposter-that-has-to-survive-a-forest" tabindex="-1">Building an open world in the browser, part 19: The imposter that has to survive a forest <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-19-the-imposter-that-has-to-survive-a-forest" aria-label="Permalink to &quot;Building an open world in the browser, part 19: The imposter that has to survive a forest&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-02-open-world-browser-part-18-ai-scattering.html">Part 18</a> gave creators a brush that fills a hillside with trees. The catch is what those trees cost when there are tens of thousands of them on screen. A distant tree doesn't need 2,000 triangles to contribute four pixels. This part is the deepest LOD: the imposter, a flat quad wearing a photo of the tree, and the road from &quot;one tree that looks right&quot; to a million of them on the GPU.</p>
<h2 id="a-tree-is-two-textures-on-a-billboard" tabindex="-1">A tree is two textures on a billboard <a class="header-anchor" href="#a-tree-is-two-textures-on-a-billboard" aria-label="Permalink to &quot;A tree is two textures on a billboard&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/38-imposters/" title="Spike 38 Octahedral Imposters" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/38-imposters/" target="_blank">Open Spike 38 in a new tab ↗</a> · <a href="/blog/spike-source?spike=38-imposters">View source</a></p>
<p>An imposter pre-renders a prop from a grid of viewing angles into two texture atlases, one for color and one for world-space normals, then at runtime shows a single camera-facing quad sampling whichever tile matches the current view. The bake is two passes per tile: diffuse with an unlit material so no lighting bakes into the texture, and normals encoded as <code>normalWorld × 0.5 + 0.5</code> with alpha forwarded from the source so the silhouette matches pixel-for-pixel. The runtime material is a full <code>MeshStandardNodeMaterial</code>, so the imposter still takes the scene's sun and IBL like any other surface. The win is the geometry: one quad instead of thousands of triangles, with the detail living in a 1 MB texture.</p>
<p>Pulling this into its own spike was itself a lesson. The imposter started as the deepest LOD inside spike 37's scatter system, and every iteration on the bake had to be tested through the full scatter pipeline, with bake correctness tangled up in instance-matrix migration and LOD swaps. Splitting it out to one prop, one quad, side-by-side with the original, dropped iteration time from minutes to seconds.</p>
<h2 id="when-the-textbook-answer-is-the-wrong-answer" tabindex="-1">When the textbook answer is the wrong answer <a class="header-anchor" href="#when-the-textbook-answer-is-the-wrong-answer" aria-label="Permalink to &quot;When the textbook answer is the wrong answer&quot;"></a></h2>
<p>The first implementation used octahedral encoding, the textbook mapping for packing sphere directions into a square. It passed numerical roundtrip tests, and yet the user kept screenshotting the imposter snapped to a tile that looked like the tree from slightly above instead of straight on. Six rounds of fixes followed (a per-quad view-direction uniform, a square bake aspect, debug materials, static billboarding) and every one was genuinely needed but none was the actual bug. The fix only came with &quot;rethink it from scratch, KISS, no band-aids.&quot;</p>
<p>The rewrite threw out octahedral folding for plain azimuth by elevation: <code>az = atan2(dir.x, dir.z)</code>, <code>el = asin(dir.y)</code>, <code>uv = (az/2π, el/π + ½)</code>. That's the whole encoding, no L1 normalize, no sign-of-zero corner cases. The reason it's better here isn't that it's more accurate (it samples a less uniform sphere), it's that the CPU cell-picker and the GPU shader use the same primitives, so they can't disagree at a boundary direction the way the octahedral pair quietly did. The atlas reads like a contact sheet: column is the angle around the prop, row is elevation, obvious at a glance in the overlay.</p>
<p>Even after that, the &quot;looks from slightly above&quot; complaint survived, and the cause was a quantization choice, not the encoding. With a 4×4 grid the row centers land at ±22.5° and ±67.5°, so there is no row at exactly 0° elevation. A viewer looking horizontally, the overwhelmingly common case, always falls into a row baked at a tilt. The fix is odd N: a 5×5 grid puts row centers at 0° and ±36° and ±72°, so the horizontal viewer gets a tile baked at exactly horizontal. The same family of &quot;off-by-half-a-cell&quot; mistake shows up again in the next part's parallax work, and the cure is the same question: does my discrete sample point actually land where I think it does for the canonical input?</p>
<p>Two more pieces mattered. The billboard has to be piecewise-static, not continuously facing the camera. The imposter is a flat photo taken from a specific bake direction, so the runtime quad's image plane must match that bake camera's plane, which means it holds orientation across the arc where one cell stays selected, then snaps at the boundary. And the budget should follow where players actually look: a later pass dropped the tilted top-down rows entirely in favor of 24 horizontal ring slots at 15° apart plus one straight-down tile, because trees are viewed from eye height roughly all the time.</p>
<h2 id="the-snap-and-how-blending-erased-it" tabindex="-1">The snap, and how blending erased it <a class="header-anchor" href="#the-snap-and-how-blending-erased-it" aria-label="Permalink to &quot;The snap, and how blending erased it&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/42-imposter-flicker/" title="Spike 42 Imposter Flicker Comparison" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/42-imposter-flicker/" target="_blank">Open Spike 42 in a new tab ↗</a> · <a href="/blog/spike-source?spike=42-imposter-flicker">View source</a></p>
<p>The piecewise-static billboard is invisible at distance and pops at close range, which is fine until you orbit. Spike 42 put four variants side by side (the original prop, the az/el 5×5 baseline, and two hemi-octahedral grids) to isolate the flicker and kill it. Two artifacts drive the snap. The cell pop happens because the fragment shader quantizes the view direction into one of 25 cells, so crossing a boundary swaps the sampled tile and re-aims the quad on the same frame. The pole degeneracy is the top-down view, where every azimuth collapses onto one point and the bridge between the ring and the top tile is the worst transition in the atlas.</p>
<p>Hemi-octahedral mapping fixes both. It maps the upper hemisphere onto the unit square continuously, so adjacent 3D directions land at adjacent UVs and there's no pole singularity and no need for a special top-down tile. The flicker cure is bilinear cell blending: instead of snapping to the nearest tile, find the 2×2 group of tiles bracketing the encoded direction and blend all four, 8 texture taps total (4 diffuse, 4 normal). Adjacent views now cross-fade instead of popping. The normal blend of two unit vectors isn't itself unit length, so it gets renormalized, which behaves like a slerp for the small angles between neighboring tiles. The cost is real (a 12×12 atlas is about 9 MB versus the az/el 1.6 MB, and the bake runs roughly 5× longer at 288 render-target passes) but the bake is a one-shot at load and the blend buys a pop-free result, which is what makes imposters usable while the camera is actually moving.</p>
<h2 id="a-million-trees-one-camera-position-copy-per-frame" tabindex="-1">A million trees, one camera-position copy per frame <a class="header-anchor" href="#a-million-trees-one-camera-position-copy-per-frame" aria-label="Permalink to &quot;A million trees, one camera-position copy per frame&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/41-imposter-forest/" title="Spike 41 Imposter Forest" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/41-imposter-forest/" target="_blank">Open Spike 41 in a new tab ↗</a> · <a href="/blog/spike-source?spike=41-imposter-forest">View source</a></p>
<p>Spike 38's runtime does one CPU <code>lookAt</code> per quad per frame, which is fine for one tree and fatal for a forest. At a million trees the per-frame matrix updates and instance-buffer upload would dominate everything. Spike 41 moves the entire per-frame pipeline onto the GPU. Each instance's center, yaw, and scale upload once at build time as instanced attributes and never change. The vertex shader builds the billboard basis from the world-space view direction <code>camPos − center</code> and expands a shared unit quad into world space. The fragment shader does the hemi-octahedral encode and the bilinear blend per pixel. The only per-frame CPU work for the whole forest is one <code>Vector3.copy</code> to update the camera-position uniform, which doesn't scale with tree count at all.</p>
<p>A nice piece of the math is that the per-instance yaw cancels out of the normal decode. The bake stores normals in the bake camera's frame, and because a yaw rotation about world up preserves +Y and the cross product is rotation-equivariant, the runtime basis built with a world-up reference already equals the rotated bake basis. So the shader decodes normals straight through the runtime basis varyings without ever touching the per-instance yaw. Placement uses a jittered grid rather than pure random scatter: partition the area into cells, drop one tree per cell at the center plus a bounded offset, which guarantees a minimum spacing (no two trees on top of each other) while still reading as a natural forest. One detail that's easy to miss is the bounding sphere. The geometry template is just a unit quad, so three.js would frustum-cull the whole forest the moment the camera looked away from the origin. Setting an explicit bounding sphere covering the full area plus a quad's margin keeps the corner trees from getting chopped at glancing angles.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Octahedral and azimuth-elevation imposter atlases.</strong> An imposter bakes a prop from a grid of view directions into a diffuse atlas and a world-space normal atlas, then renders a single billboard sampling the matching tile, replacing thousands of triangles with two textures. The textbook octahedral mapping gives uniform sphere coverage but is prone to CPU/GPU divergence at fold boundaries; a plain azimuth-by-elevation grid samples a less uniform sphere but guarantees the cell-picker and shader agree by construction. Use odd N so a row lands at exactly 0° elevation, and spend the tile budget on the horizontal ring since props are mostly viewed from eye height.</p>
<p><strong>Piecewise-static billboard orientation.</strong> An imposter is a photo from a specific bake direction, so the runtime quad's image plane must match the bake camera's plane, not continuously face the runtime camera. The quad holds orientation across the arc where one cell stays selected, then snaps at the boundary, which is invisible at imposter distance and only pops up close where imposters aren't used.</p>
<p><strong>Hemi-octahedral atlas with bilinear cell blend.</strong> Mapping the upper hemisphere onto the unit square continuously removes the pole singularity and the special top-down tile. The snap is killed by sampling the 2×2 tile group bracketing the encoded view direction and bilinear-blending all four tiles (8 taps), so adjacent views cross-fade. Blended normals are renormalized, approximating a slerp over the small inter-tile angle. Cost is a larger atlas (about 9 MB at 12×12) and a longer one-shot bake, traded for pop-free shading under camera motion.</p>
<p><strong>GPU-driven instanced imposters.</strong> Per-instance center, yaw, and scale upload once as instanced attributes; the vertex shader builds the billboard basis and expands a shared unit quad, and the fragment shader does the encode and blend per pixel. Per-frame CPU cost for the whole forest is a single camera-position uniform copy, independent of instance count. Per-instance yaw cancels out of the normal decode because yaw about world up is preserved by the cross-product basis construction. An explicit forest-wide bounding sphere prevents three.js from frustum-culling the entire instanced mesh when the camera looks away from the unit-quad template's origin. See <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">GPU-driven LOD</a>.</p>
<hr>
<p>Part 19 of 29.
Previous: <a href="/blog/2026-05-02-open-world-browser-part-18-ai-scattering.html">Part 18 - A scatter brush that feels AI-placed</a>
Next: <a href="/blog/2026-05-05-open-world-browser-part-20-parallax-occlusion.html">Part 20 - Faking depth on a flat plane</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Volumetric clouds and weather effects in modern games]]></title>
            <link>https://app.cinevva.com/blog/2026-05-04-volumetric-clouds-and-weather</link>
            <guid>https://app.cinevva.com/blog/2026-05-04-volumetric-clouds-and-weather</guid>
            <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How modern games render volumetric clouds, atmospheric scattering, fog, rain, snow, and dynamic weather, with the foundational papers and engine talks behind each technique.]]></description>
            <content:encoded><![CDATA[<h1 id="volumetric-clouds-and-weather-effects-in-modern-games" tabindex="-1">Volumetric clouds and weather effects in modern games <a class="header-anchor" href="#volumetric-clouds-and-weather-effects-in-modern-games" aria-label="Permalink to &quot;Volumetric clouds and weather effects in modern games&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<img src="/img/blog/clouds_weather_hero.webp" alt="Stylized AAA scene with towering volumetric storm clouds, lightning, rain shafts, and a wet stone road reflecting the sky" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>A few weeks ago I wrote about <a href="/blog/2026-05-03-aaa-rendering-techniques.html">the rendering techniques modern AAA games actually ship</a>. One area I left thin was sky and weather, because it deserves its own list. Clouds, fog, rain, and snow are the systems that turn a terrain demo into a place. They also share more code than they look like they do. Volumetric clouds, ground fog, and god rays are all the same ray-march. Wet roads, snow accumulation, and footprint trails are all the same displacement plus PBR trick. Wind is one direction vector that everything in the scene reads from.</p>
<p>Here's a short, opinionated tour of how the bigger studios build this stuff in 2026, with the papers and engine talks behind each piece.</p>
<h2 id="_1-physically-based-sky-and-atmosphere" tabindex="-1">1. Physically based sky and atmosphere <a class="header-anchor" href="#_1-physically-based-sky-and-atmosphere" aria-label="Permalink to &quot;1. Physically based sky and atmosphere&quot;"></a></h2>
<p>Atmospheric scattering is the foundation. The sky color, the horizon haze, the way distant mountains turn blue, the orange of sunset, all come from light scattering through the air. Modern engines compute this from physics: Rayleigh scattering for the blue, Mie scattering for the haze around the sun, ozone absorption for the deep violet at the zenith.</p>
<p>The original 2008 Bruneton method baked everything into 4D lookup tables, which limited dynamic time-of-day and added LUT artifacts at low sun angles. Sébastien Hillaire's 2020 update, which is what UE5's Sky Atmosphere component ships, replaces the high-dimensional LUT with a few 2D textures and a multiple-scattering approximation that updates per frame. It runs from a phone to a high-end PC.</p>
<img src="/img/blog/clouds_weather_atmosphere.webp" alt="Physically based sky at golden hour with smooth orange-to-blue gradient over a distant mountain silhouette" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Hillaire, <a href="https://sebh.github.io/publications/egsr2020.pdf" target="_blank" rel="noreferrer">A Scalable and Production Ready Sky and Atmosphere Rendering Technique</a> (EGSR 2020, the modern standard, used in UE5).</li>
<li>Bruneton and Neyret, <a href="https://hal.inria.fr/inria-00288758/document" target="_blank" rel="noreferrer">Precomputed Atmospheric Scattering</a> (EGSR 2008, the original LUT approach).</li>
<li>Bruneton, <a href="https://ebruneton.github.io/precomputed_atmospheric_scattering/" target="_blank" rel="noreferrer">Precomputed Atmospheric Scattering: a New Implementation</a> (open-source reference, ozone and multi-planet support).</li>
<li>Epic Games, <a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/sky-atmosphere-component-in-unreal-engine" target="_blank" rel="noreferrer">Sky Atmosphere Component</a> (UE5 docs).</li>
</ul>
<h2 id="_2-volumetric-clouds-with-perlin-worley-noise" tabindex="-1">2. Volumetric clouds with Perlin-Worley noise <a class="header-anchor" href="#_2-volumetric-clouds-with-perlin-worley-noise" aria-label="Permalink to &quot;2. Volumetric clouds with Perlin-Worley noise&quot;"></a></h2>
<p>The bedrock cloud technique in modern games started in Andrew Schneider's 2015 Horizon Zero Dawn talk. Clouds are not meshes. They are a 3D density function defined by layered noise: a low-frequency Perlin-Worley mix gives the overall cloud shape, and a higher-frequency Worley noise erodes the silhouette into wispy edges. A weather map (a 2D texture sampled by world XZ) controls coverage, cloud type, and precipitation per region. A height-based gradient blends between cumulus, stratus, and cirrus profiles by altitude.</p>
<p>The renderer marches a ray from the camera through the cloud volume, accumulating density and scattering. The &quot;Nubis&quot; iteration in 2017 added regional-scale authoring and animation, and the original PS4 implementation ran in about 2 ms for the entire sky. Most studios that ship volumetric clouds today still trace their lineage to this paper.</p>
<img src="/img/blog/clouds_weather_noise.webp" alt="A 3D cloud shape decomposed into stacked Perlin and Worley noise patterns showing how detail erodes the silhouette" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Schneider, <a href="https://www.guerrilla-games.com/read/the-real-time-volumetric-cloudscapes-of-horizon-zero-dawn" target="_blank" rel="noreferrer">The Real-Time Volumetric Cloudscapes of Horizon Zero Dawn</a> (SIGGRAPH 2015, the canonical reference).</li>
<li>Schneider, <a href="https://advances.realtimerendering.com/s2017/Nubis%20-%20Authoring%20Realtime%20Volumetric%20Cloudscapes%20with%20the%20Decima%20Engine%20-%20Final%20.pdf" target="_blank" rel="noreferrer">Nubis: Authoring Real-Time Volumetric Cloudscapes with the Decima Engine</a> (SIGGRAPH 2017, the regional-scale follow-up).</li>
<li>Hillaire, <a href="https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdf" target="_blank" rel="noreferrer">Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite</a> (SIGGRAPH 2016, the Frostbite version).</li>
<li>Häggström, <a href="https://www.diva-portal.org/smash/get/diva2:1223894/FULLTEXT01.pdf" target="_blank" rel="noreferrer">Real-time rendering of volumetric clouds</a> (a clean, approachable thesis with full shader code).</li>
</ul>
<h2 id="_3-voxel-clouds-and-nubis3" tabindex="-1">3. Voxel clouds and Nubis³ <a class="header-anchor" href="#_3-voxel-clouds-and-nubis3" aria-label="Permalink to &quot;3. Voxel clouds and Nubis³&quot;"></a></h2>
<p>The 2023 evolution of Nubis abandoned the 2.5D shape representation entirely in favor of true 3D voxels. Each voxel stores cloud density directly, which lets artists carve and animate cloud shapes the way they sculpt terrain. The cost of moving to a denser representation is paid back by ray-march acceleration with compressed signed distance fields and clever up-rezzing of sparse voxel data.</p>
<p>The result is the kind of cloudscape you can fly <em>through</em> without seeing the underlying tricks fall apart. It's overkill for most studios, but it's the direction the high end is moving.</p>
<img src="/img/blog/clouds_weather_voxel_clouds.webp" alt="A cumulus cloud shown decomposed into a 3D voxel grid, with smoothed wispy edges blending the chunky interior" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Schneider, <a href="https://www.guerrilla-games.com/read/nubis-cubed" target="_blank" rel="noreferrer">Nubis³: Methods (and madness) to model and render immersive real-time voxel-based clouds</a> (SIGGRAPH 2023, the voxel-cloud talk).</li>
<li>Schneider, <a href="https://www.guerrilla-games.com/read/nubis-evolved" target="_blank" rel="noreferrer">Nubis, Evolved</a> (SIGGRAPH 2022, the bridge between 2.5D and full 3D).</li>
<li>Schneider, <a href="https://www.schneidervfx.com/" target="_blank" rel="noreferrer">Real-time Volumetrics and VFX</a> (Andrew's personal site, with course notes and breakdowns).</li>
</ul>
<h2 id="_4-layered-cloud-rendering-and-2d-backdrops" tabindex="-1">4. Layered cloud rendering and 2D backdrops <a class="header-anchor" href="#_4-layered-cloud-rendering-and-2d-backdrops" aria-label="Permalink to &quot;4. Layered cloud rendering and 2D backdrops&quot;"></a></h2>
<p>Not every studio can afford full-volume clouds, and not every camera angle needs them. A lot of games combine techniques: high-altitude cirrus rendered as a scrolling 2D layer, mid-altitude cumulus as volumetric rays, and low-altitude stratus as a thin participating-media slab. The horizon often gets a pre-baked sky cubemap that the volumetric pass blends into beyond a fade distance.</p>
<p>This layering is what keeps the cloud budget honest. A single cloud type at full quality can eat 4-6 ms; layering different qualities for different cloud altitudes can hold the same look at half the cost.</p>
<img src="/img/blog/clouds_weather_cloud_layers.webp" alt="Sunset sky split into three cloud layers: high cirrus wisps, mid cumulus puffs, and low stratus haze near the horizon" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Vos, <a href="https://www.gdcvault.com/play/1023327/The-Real-Time-Volumetric-Cloudscapes" target="_blank" rel="noreferrer">The Real-Time Volumetric Cloudscapes of Horizon Zero Dawn</a> (GDC 2016 video, the layering breakdown is in the second half).</li>
<li>Bauer, <a href="https://www.youtube.com/watch?v=IYxtqlD1-Ac" target="_blank" rel="noreferrer">Creating the Atmospheric World of Red Dead Redemption 2</a> (SIGGRAPH 2019, RDR2's combined sky/clouds/volumetrics pipeline).</li>
<li>Hillaire, <a href="https://www.gdcvault.com/play/1029105/Volumetric-Clouds-and" target="_blank" rel="noreferrer">Volumetric clouds and mega particles in REDengine 4</a> (GDC 2025, Cyberpunk 2077's cloud pipeline).</li>
</ul>
<h2 id="_5-volumetric-fog-with-froxel-grids" tabindex="-1">5. Volumetric fog with froxel grids <a class="header-anchor" href="#_5-volumetric-fog-with-froxel-grids" aria-label="Permalink to &quot;5. Volumetric fog with froxel grids&quot;"></a></h2>
<p>Fog is a 3D field, not a 2D screen effect. The standard modern approach is the froxel grid: a 3D texture aligned to the camera's view frustum, with each cell (&quot;froxel&quot; = frustum + voxel) storing density and lit color. A compute shader injects scattering from every light source into the grid, accumulates extinction along the view ray, and applies the result as a fullscreen pass.</p>
<p>This is what gives you light shafts through windows, colored fog around point lights, and visible volumes around explosions. It's also the underlying machinery for &quot;atmospheric perspective&quot; that fades distant objects into the air. The technique was introduced by Bart Wronski for Assassin's Creed 4 and standardized by Sébastien Hillaire in Frostbite.</p>
<img src="/img/blog/clouds_weather_froxel_fog.webp" alt="Camera frustum visualized as a 3D froxel grid with smaller cells near the camera and bigger cells far away, fog particles inside" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Wronski, <a href="https://bartwronski.com/wp-content/uploads/2014/08/bwronski_volumetric_fog_siggraph2014.pdf" target="_blank" rel="noreferrer">Volumetric Fog: Unified Compute Shader Based Solution to Atmospheric Scattering</a> (SIGGRAPH 2014, the original froxel-grid paper).</li>
<li>Hillaire, <a href="http://www.frostbite.com/2015/08/physically-based-unified-volumetric-rendering-in-frostbite/" target="_blank" rel="noreferrer">Physically Based and Unified Volumetric Rendering in Frostbite</a> (SIGGRAPH 2015, the production-grade implementation).</li>
<li>Kovalovs, <a href="https://history.siggraph.org/wp-content/uploads/2022/08/2020-Talks-Kovalovs_Volumetric-Effects-of-The-Last-of-Us-Part-Two.pdf" target="_blank" rel="noreferrer">Volumetric Effects of The Last of Us Part Two</a> (SIGGRAPH 2020, with notes on temporal jitter and depth-correct compositing).</li>
<li>Wright et al., <a href="https://advances.realtimerendering.com/s2022/SIGGRAPH2022-Advances-Lumen-Wright%20et%20al.pdf" target="_blank" rel="noreferrer">Lumen: Real-Time Global Illumination in Unreal Engine 5</a> (SIGGRAPH 2022, includes Lumen's interaction with volumetric fog).</li>
</ul>
<h2 id="_6-god-rays-and-crepuscular-shafts" tabindex="-1">6. God rays and crepuscular shafts <a class="header-anchor" href="#_6-god-rays-and-crepuscular-shafts" aria-label="Permalink to &quot;6. God rays and crepuscular shafts&quot;"></a></h2>
<p>Visible rays of sunlight in misty air are not a separate effect. They fall out of the same fog system, as long as the fog density and the shadow map are both available to the same compute shader. When the shader injects light into a froxel, it samples the shadow map at that froxel's world position. Cells in shadow stay dark, cells in light pick up the sun color. March the camera ray through the result and the bright cells form continuous shafts.</p>
<p>Cheaper screen-space variants exist (radial blur from the sun position into the depth buffer) and are still the right pick on mobile or low-end hardware. They miss off-screen sun positions but cost almost nothing.</p>
<img src="/img/blog/clouds_weather_god_rays.webp" alt="Dawn forest with sun rays slicing through tree trunks and ground fog, forming clear god ray shafts" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Mitchell, <a href="https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-13-volumetric-light-scattering-post-process" target="_blank" rel="noreferrer">Volumetric Light Scattering as a Post-Process</a> (GPU Gems 3, the screen-space radial-blur approach).</li>
<li>Engelhardt and Dachsbacher, <a href="https://cg.ivd.kit.edu/publications/2010/epipolar/EpipolarSampling.pdf" target="_blank" rel="noreferrer">Epipolar Sampling for Shadows and Crepuscular Rays in Participating Media</a> (I3D 2010, the more accurate GPU technique).</li>
<li>Vos, <a href="https://www.guerrilla-games.com/read/volumetric-light-effects-in-killzone-shadow-fall" target="_blank" rel="noreferrer">Volumetric Light Effects in Killzone: Shadow Fall</a> (SIGGRAPH 2014, the production version with shadow integration).</li>
</ul>
<h2 id="_7-lightning-and-stochastic-weather-events" tabindex="-1">7. Lightning and stochastic weather events <a class="header-anchor" href="#_7-lightning-and-stochastic-weather-events" aria-label="Permalink to &quot;7. Lightning and stochastic weather events&quot;"></a></h2>
<p>Lightning is a one-frame effect with two parts: the bolt mesh, and the scene-wide tonemap and lighting response. The bolt itself is usually a procedural billboard mesh built from a recursive line-segment subdivision algorithm, jittered for chaos and tapered toward the ground. Some engines render it as a screen-space additive flash, others as a fully lit emissive geometry that casts light on the world via a one-frame point-light injection.</p>
<p>The interesting part is everything <em>else</em>: cloud bottoms light from below, the ground brightens for two frames, the auto-exposure metering takes a few frames to recover, and a delayed thunder cue plays based on distance. Done well, this turns a 16 ms flash into a five-second sequence that sells weather as something happening in the world, not just over it.</p>
<img src="/img/blog/clouds_weather_lightning.webp" alt="Lightning fork striking from a thundercloud at dusk, lighting the cloud bottoms and a small village silhouette" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Reed and Wyvill, <a href="https://dl.acm.org/doi/10.1145/192161.192256" target="_blank" rel="noreferrer">Visual Simulation of Lightning</a> (SIGGRAPH 1994, the recursive bolt algorithm everybody still uses).</li>
<li>Kim and Lin, <a href="https://gamma.cs.unc.edu/POWER/" target="_blank" rel="noreferrer">Fast Animation of Lightning Using an Adaptive Mesh</a> (IEEE TVCG 2007, more physically grounded).</li>
<li>Bauer, <a href="https://www.youtube.com/watch?v=IYxtqlD1-Ac" target="_blank" rel="noreferrer">Creating the Atmospheric World of Red Dead Redemption 2</a> (SIGGRAPH 2019, RDR2's storm pipeline including lightning timing).</li>
</ul>
<h2 id="_8-rain-particles-rain-meshes-and-screen-space-drops" tabindex="-1">8. Rain particles, rain meshes, and screen-space drops <a class="header-anchor" href="#_8-rain-particles-rain-meshes-and-screen-space-drops" aria-label="Permalink to &quot;8. Rain particles, rain meshes, and screen-space drops&quot;"></a></h2>
<p>Falling rain in modern games is rarely just particles. The cheap and convincing solution is a small set of scrolling textures stretched across vertical or screen-aligned quads, lit by the same sun and sky probes as everything else. Closer to the camera, individual streak particles add detail. On the camera lens itself, droplet textures, sliding trails, and impact ripples sell the &quot;you are inside the storm&quot; feel.</p>
<p>Wind affects the rain direction. The same wind vector pushes the cloud weather map, bends grass, and tilts the rain quads. One scene-wide vector, dozens of consumers.</p>
<img src="/img/blog/clouds_weather_rain_storm.webp" alt="Heavy night rainstorm with sheets of rain illuminated by distant headlights and ripples on a wet road" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Tatarchuk, <a href="https://gpuopen.com/wp-content/uploads/2018/04/Tatarchuk-Rain-Rendering-EGSR2006.pdf" target="_blank" rel="noreferrer">Artist-Directable Real-Time Rain Rendering in City Environments</a> (EGSR 2006, the canonical layered-rain reference).</li>
<li>Garg and Nayar, <a href="https://www1.cs.columbia.edu/CAVE/projects/rain_ren/rain_ren.php" target="_blank" rel="noreferrer">Photorealistic Rendering of Rain Streaks</a> (SIGGRAPH 2006, the physics of light through raindrops).</li>
<li>Wojciechowski, <a href="https://www.gdcvault.com/play/1027382/Rain-In-Cyberpunk-2077" target="_blank" rel="noreferrer">Rain in Cyberpunk 2077</a> (GDC 2021, a modern production walkthrough).</li>
</ul>
<h2 id="_9-wet-surfaces-puddles-and-ripples" tabindex="-1">9. Wet surfaces, puddles, and ripples <a class="header-anchor" href="#_9-wet-surfaces-puddles-and-ripples" aria-label="Permalink to &quot;9. Wet surfaces, puddles, and ripples&quot;"></a></h2>
<p>Rain that doesn't change the ground looks fake immediately. Wet surfaces respond by darkening their albedo (water absorbs incoming light), flattening their normals (the water film smooths microsurface), and dropping their roughness (water is a near-perfect mirror at glancing angles). The shader change is small, the visual change is huge.</p>
<p>Puddles are mask-driven: a height-based or vertex-painted mask defines low spots that fill with water as a &quot;wetness&quot; parameter rises. Ripples are flipbook normal-map textures triggered by raindrop impacts. The really nice implementations build the wetness state up over time, so a long rainstorm slowly soaks the world and a brief shower only darkens the high spots.</p>
<img src="/img/blog/clouds_weather_wet_surfaces.webp" alt="Wet cobblestone street at night with shallow puddles reflecting neon signs and ripple rings from raindrop impacts" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Lagarde and de Rousiers, <a href="https://seblagarde.wordpress.com/2015/07/14/siggraph-2014-moving-frostbite-to-physically-based-rendering/" target="_blank" rel="noreferrer">Moving Frostbite to Physically Based Rendering 3.0, section 5.5</a> (SIGGRAPH 2014, the canonical wet-surface PBR adjustments).</li>
<li>Lagarde, <a href="https://seblagarde.wordpress.com/2011/08/17/feeding-a-physical-based-lighting-mode/" target="_blank" rel="noreferrer">Adopting a Physically Based Shading Model</a> (the original blog series with the wet-PBR math).</li>
<li>Cyanilux, <a href="https://www.cyanilux.com/tutorials/rain-effects-breakdown/" target="_blank" rel="noreferrer">Rain Effects Breakdown</a> (an approachable Shader Graph walkthrough of puddles, ripples, and droplets).</li>
</ul>
<h2 id="_10-snow-accumulation-and-deformation" tabindex="-1">10. Snow accumulation and deformation <a class="header-anchor" href="#_10-snow-accumulation-and-deformation" aria-label="Permalink to &quot;10. Snow accumulation and deformation&quot;"></a></h2>
<p>Snow is the symmetrical problem to rain: the world has to remember it, not just receive it. The standard approach uses a top-down &quot;snow accumulation&quot; texture that builds up over time wherever the sky is visible (computed against a top-down depth or shadow map). The terrain shader samples this mask and blends in the snow material in shaded regions and snow-deep displacement in exposed areas.</p>
<p>Footprints and tire tracks are rendered into a sliding deformation map centered on the player. As the camera moves, old footprints scroll out and the texture wraps around. The terrain or snow shader samples this deformation map and pushes vertices down where it has been written. Battlefield 5's snow does this with hardware tessellation; cheaper approaches use a high-density terrain mesh with vertex displacement only.</p>
<img src="/img/blog/clouds_weather_snow_deformation.webp" alt="Stylized snowy landscape with fresh footprints, drifts piled against rocks, falling snowflakes, and hazy distant mountains" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Barré-Brisebois, <a href="https://www.digitalfoundry.net/articles/digitalfoundry-2018-hands-on-with-battlefield-5s-closed-alpha" target="_blank" rel="noreferrer">Hands-on with Battlefield 5: how the small things matter</a> (the production write-up of Frostbite snow).</li>
<li>St-Amour, <a href="https://is.muni.cz/th/m2v6i/real-time_snow_deformation.txt?lang=cs" target="_blank" rel="noreferrer">Real-time snow deformation</a> (a thesis with full GPU implementation details).</li>
<li>Andersson, <a href="https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/chapter5-andersson-terrain-rendering-in-frostbite.pdf" target="_blank" rel="noreferrer">Terrain Rendering in Frostbite Using Procedural Shader Splatting</a> (the splat-system foundation that snow accumulation rides on top of).</li>
</ul>
<h2 id="_11-wind-as-a-global-system" tabindex="-1">11. Wind as a global system <a class="header-anchor" href="#_11-wind-as-a-global-system" aria-label="Permalink to &quot;11. Wind as a global system&quot;"></a></h2>
<p>Wind is not a particle effect. In production engines it's a single global vector (sometimes a low-resolution 3D field) that <em>every</em> dynamic system reads from in its vertex shader. Grass blades bend, tree branches sway, cloth flaps, leaves drift, rain tilts, smoke advects, cloud weather maps scroll. One uniform updated per frame, dozens of consumers.</p>
<p>The richer version is a &quot;wind grid&quot; that stores direction and strength sampled by world position, allowing for storms with localized gusts, sheltered valleys, and wakes behind buildings. Foliage also typically gets a per-vertex offset baked at authoring time so identical trees don't sway in lockstep. The result is a world that breathes at the same rate.</p>
<img src="/img/blog/clouds_weather_wind_foliage.webp" alt="Strong wind blowing grass and trees sideways with leaves swirling in the foreground under stormy clouds" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>McAuley, <a href="https://www.gdcvault.com/play/1022235/Rendering-the-World-of-Far" target="_blank" rel="noreferrer">Rendering the World of Far Cry 4</a> (GDC 2015, includes Far Cry 4's wind grid for vegetation).</li>
<li>Habel and Wimmer, <a href="https://www.cg.tuwien.ac.at/research/publications/2007/HABEL-2007-IIM/" target="_blank" rel="noreferrer">Realistic Real-Time Rendering of Landscapes Using Billboard Clouds</a> (older, but the wind-on-vegetation math is unchanged).</li>
<li>Frostbite, <a href="https://www.guerrilla-games.com/read/the-vegetation-of-horizon-zero-dawn" target="_blank" rel="noreferrer">The Vegetation of Horizon Zero Dawn</a> (SIGGRAPH 2017, with the wind-system architecture).</li>
</ul>
<h2 id="_12-sandstorms-blizzards-and-dense-weather" tabindex="-1">12. Sandstorms, blizzards, and dense weather <a class="header-anchor" href="#_12-sandstorms-blizzards-and-dense-weather" aria-label="Permalink to &quot;12. Sandstorms, blizzards, and dense weather&quot;"></a></h2>
<p>Severe weather is its own rendering category. A sandstorm is a thick, opaque, ground-aligned fog with a strong directional bias and aggressive distance fog. A blizzard adds a near-camera particle blast and reduced visibility. Volcanic ash and smoke are the same architecture with different colors.</p>
<p>The thing that sells these is not the particles, it's the <em>coupling</em>: the sun darkens, the sky tints, the post-process color grading shifts, ambient audio swaps, footstep sounds change, the player's voice gets muffled if they have one. The renderer is the messenger; the immersion comes from every system in the game responding at the same time.</p>
<img src="/img/blog/clouds_weather_sandstorm.webp" alt="A wall of orange dust and sand rolling across a desert plain with bruised sky above and clear blue behind" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Burley, <a href="https://www.gdcvault.com/play/1024954/The-Real-time-Sky-and" target="_blank" rel="noreferrer">The Real-Time Sky and Atmosphere of Uncharted: The Lost Legacy</a> (GDC 2018, with notes on dense-weather composition).</li>
<li>Khalifa, <a href="https://www.gdcvault.com/play/1027587/Atmospheric-Weather-Effects-in-Forza" target="_blank" rel="noreferrer">Atmospheric Weather Effects in Forza Horizon 5</a> (GDC 2022, modern open-world weather).</li>
<li>Karis, <a href="https://advances.realtimerendering.com/s2021/" target="_blank" rel="noreferrer">The Technology Behind the Unreal Engine 5 &quot;Lumen in the Land of Nanite&quot; Demo</a> (SIGGRAPH 2021, the cave dust scene's volumetric stack).</li>
</ul>
<h2 id="_13-time-of-day-and-dynamic-skies" tabindex="-1">13. Time of day and dynamic skies <a class="header-anchor" href="#_13-time-of-day-and-dynamic-skies" aria-label="Permalink to &quot;13. Time of day and dynamic skies&quot;"></a></h2>
<p>Real-time time of day is the multiplier that makes every other system in this list worth shipping. The sun direction and color update over a 24-minute or 24-hour cycle. The atmosphere LUT updates with the sun angle. The cloud lighting recomputes per frame. The shadow cascades repoint. The reflection probes refresh. The ambient color shifts. The post-process exposure adapts.</p>
<p>Doing this without visible artifacts is mostly a story of texture caching and temporal stability. Fast techniques precompute the sky at fixed sun angles and interpolate; slower ones recompute every frame. The Hillaire 2020 atmosphere model is fast enough to recompute, which is why UE5 ships it. The cloud weather map scrolls with wind, so coverage shifts naturally without anyone authoring keyframes.</p>
<img src="/img/blog/clouds_weather_time_of_day.webp" alt="Three vertical bands across one landscape: dawn pink, noon blue, sunset red, all sharing the same hill silhouette" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Hillaire, <a href="https://sebh.github.io/publications/egsr2020.pdf" target="_blank" rel="noreferrer">A Scalable and Production Ready Sky and Atmosphere Rendering Technique</a> (EGSR 2020, with the dynamic time-of-day discussion).</li>
<li>Pesce, <a href="https://www.realtimerendering.com/raytracinggems/rtg2/index.html" target="_blank" rel="noreferrer">Real-Time Sky Rendering: Techniques and Tradeoffs</a> (Ray Tracing Gems II chapter, modern survey).</li>
<li>Bauer, <a href="https://www.youtube.com/watch?v=IYxtqlD1-Ac" target="_blank" rel="noreferrer">Creating the Atmospheric World of Red Dead Redemption 2</a> (SIGGRAPH 2019, with the 24-hour pipeline).</li>
</ul>
<h2 id="_14-the-weather-state-machine" tabindex="-1">14. The weather state machine <a class="header-anchor" href="#_14-the-weather-state-machine" aria-label="Permalink to &quot;14. The weather state machine&quot;"></a></h2>
<p>Underneath all of this is a tiny state machine. Most games ship somewhere between 4 and 12 weather states (clear, partly cloudy, overcast, light rain, heavy rain, thunderstorm, fog, snow, blizzard, sandstorm), each defined by a set of parameters: cloud coverage and type, wind speed and direction, precipitation type and intensity, ambient color tints, audio profile, post-process grade.</p>
<p>Transitions are linear interpolations between parameter sets over 30 to 120 seconds. The transition isn't a special case, it's just two states being lerped, with each rendering subsystem reading the current parameter values that frame. Weather can be scripted (cutscene needs a storm), seeded (deterministic per region per in-game day so that two players in the same world see the same weather), or fully authored on a region grid. The cleanest pipelines treat all three as different schedulers writing into the same parameter buffer.</p>
<img src="/img/blog/clouds_weather_state_machine.webp" alt="Side-by-side of the same scene under clear sun on the left and stormy rain on the right with wet glistening surfaces" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Bauer, <a href="https://www.youtube.com/watch?v=IYxtqlD1-Ac" target="_blank" rel="noreferrer">Creating the Atmospheric World of Red Dead Redemption 2</a> (SIGGRAPH 2019, with the weather-state pipeline).</li>
<li>Khalifa, <a href="https://www.gdcvault.com/play/1027587/Atmospheric-Weather-Effects-in-Forza" target="_blank" rel="noreferrer">Atmospheric Weather Effects in Forza Horizon 5</a> (GDC 2022, with the parameter-blend approach).</li>
<li>Schneider, <a href="https://www.guerrilla-games.com/read/the-real-time-volumetric-cloudscapes-of-horizon-zero-dawn" target="_blank" rel="noreferrer">The Real-Time Volumetric Cloudscapes of Horizon Zero Dawn</a> (SIGGRAPH 2015, with weather-map-driven cloud evolution).</li>
</ul>
<h2 id="_15-the-cinematic-moments" tabindex="-1">15. The cinematic moments <a class="header-anchor" href="#_15-the-cinematic-moments" aria-label="Permalink to &quot;15. The cinematic moments&quot;"></a></h2>
<p>The whole stack exists for a few signature moments. Standing on a ridge as a storm front rolls in. Watching a sun shaft burn through a clearing in the canopy. Walking out of a cave into snow. Flying through a cumulus cloud and seeing the light on the inside.</p>
<p>These are the moments players take screenshots of. They're also the moments where every system above has to be working at the same time: cloud volumetrics, atmospheric scattering, fog with shadow integration, wet PBR, wind on the foliage, time-of-day color grading, and a transition between weather states all composing into one frame. Get any one of them wrong and the magic snaps.</p>
<img src="/img/blog/clouds_weather_cloud_sea.webp" alt="Looking down across a sea of clouds onto a single mountain peak rising through, with sunlit cloud tops and shadowed valleys below" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<h2 id="what-this-means-for-the-browser" tabindex="-1">What this means for the browser <a class="header-anchor" href="#what-this-means-for-the-browser" aria-label="Permalink to &quot;What this means for the browser&quot;"></a></h2>
<p>Most of these techniques map cleanly onto WebGPU. We've shipped basic atmospheric fog, equirectangular skybox blending, and screen-space distance haze in the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">open-world browser engine</a>. The harder pieces (full volumetric clouds, froxel grid fog, wet PBR with dynamic puddles, snow deformation maps) are the obvious next step now that the terrain pipeline is stable. Spike 24's per-fragment fog color sampling from the skybox is one piece of this puzzle. A compute-shader cloud raymarcher feeding into the same atmosphere LUT is the next.</p>
<p>The good news is that the browser hardware floor is now high enough. WebGPU compute, 3D textures, indirect dispatch, and timestamp queries all exist. The Hillaire 2020 atmosphere has been ported to WebGL multiple times. Schneider's Nubis has open-source reference implementations in GLSL that translate to WGSL with mechanical edits. There is no longer a rendering reason that a browser game can't have the same sky as a console one. There are just engineering reasons, and engineering reasons are the kind we like.</p>
<h2 id="further-reading-across-the-whole-stack" tabindex="-1">Further reading across the whole stack <a class="header-anchor" href="#further-reading-across-the-whole-stack" aria-label="Permalink to &quot;Further reading across the whole stack&quot;"></a></h2>
<p>If you want one source that pulls all of this together, the SIGGRAPH &quot;Advances in Real-Time Rendering in Games&quot; archive (<a href="https://advances.realtimerendering.com/" target="_blank" rel="noreferrer">advances.realtimerendering.com</a>) has the canonical weather and atmosphere talks going back to 2014. For production tear-downs of how specific games render their sky and weather, <a href="http://www.adriancourreges.com/blog/" target="_blank" rel="noreferrer">Adrian Courrèges' GPU profiling articles</a> include detailed frame-by-frame breakdowns of GTA V, Horizon Zero Dawn, and Doom Eternal. For the sky and atmosphere math specifically, <a href="https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html" target="_blank" rel="noreferrer">scratchapixel.com's volume rendering chapter</a> is the gentlest introduction, and Hillaire's open-source <a href="https://github.com/sebh/UnrealEngineSkyAtmosphere" target="_blank" rel="noreferrer">implementation repository</a> is the production-quality reference.</p>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/blog/clouds_weather_hero.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[A short list of rendering techniques used in modern AAA games]]></title>
            <link>https://app.cinevva.com/blog/2026-05-03-aaa-rendering-techniques</link>
            <guid>https://app.cinevva.com/blog/2026-05-03-aaa-rendering-techniques</guid>
            <pubDate>Sun, 03 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The core rendering techniques behind today's AAA visuals, from Nanite virtualized geometry to ReSTIR path tracing and ML-based upscaling, with deep technical references for each.]]></description>
            <content:encoded><![CDATA[<h1 id="a-short-list-of-rendering-techniques-used-in-modern-aaa-games" tabindex="-1">A short list of rendering techniques used in modern AAA games <a class="header-anchor" href="#a-short-list-of-rendering-techniques-used-in-modern-aaa-games" aria-label="Permalink to &quot;A short list of rendering techniques used in modern AAA games&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<img src="/img/blog/aaa_rendering_hero.webp" alt="Composite scene showing PBR materials, virtualized geometry, and ray-traced lighting in a modern AAA game" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>If you crack open the rendering pipeline of a 2026 AAA game, most of what you'll find traces back to a small set of techniques that ship across studios. The names change between engines, but the ideas are the same. Here's a short list of what's actually doing the work on screen, with a figure and a few deep technical references for each.</p>
<h2 id="_1-physically-based-rendering-pbr" tabindex="-1">1. Physically based rendering (PBR) <a class="header-anchor" href="#_1-physically-based-rendering-pbr" aria-label="Permalink to &quot;1. Physically based rendering (PBR)&quot;"></a></h2>
<p>Materials are described by albedo, roughness, metallic, normal, and ambient occlusion textures, and lit by energy-conserving shaders (Cook-Torrance specular, Lambertian or Disney diffuse). It's the baseline every modern AAA engine assumes. If a surface looks consistent under sunlight, lamp light, and a flashlight, PBR is why.</p>
<img src="/img/blog/aaa_pbr_materials.webp" alt="Iron helmet rendered with PBR materials, with floating texture swatches showing albedo, roughness, metallic, and normal maps" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Burley, <a href="https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf" target="_blank" rel="noreferrer">Physically-Based Shading at Disney</a> (the original &quot;Disney BRDF&quot; course notes, SIGGRAPH 2012).</li>
<li>Karis, <a href="https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf" target="_blank" rel="noreferrer">Real Shading in Unreal Engine 4</a> (SIGGRAPH 2013, the canonical UE4 PBR talk).</li>
<li>Lagarde and de Rousiers, <a href="https://seblagarde.wordpress.com/2015/07/14/siggraph-2014-moving-frostbite-to-physically-based-rendering/" target="_blank" rel="noreferrer">Moving Frostbite to Physically Based Rendering 3.0</a> (SIGGRAPH 2014, full Frostbite pipeline).</li>
<li><a href="https://www.realtimerendering.com/" target="_blank" rel="noreferrer">Real-Time Rendering, 4th edition, chapter 9</a> (the textbook reference).</li>
</ul>
<h2 id="_2-deferred-and-visibility-buffer-shading" tabindex="-1">2. Deferred and visibility-buffer shading <a class="header-anchor" href="#_2-deferred-and-visibility-buffer-shading" aria-label="Permalink to &quot;2. Deferred and visibility-buffer shading&quot;"></a></h2>
<p>Geometry first writes attributes (normals, material IDs, depth) into a G-buffer or visibility buffer. Lighting runs as a fullscreen pass that reads those buffers and shades each pixel once. Visibility buffers (used by Nanite and similar) push this further by storing only triangle IDs and resolving material parameters per pixel later, which keeps overdraw cheap on dense geometry.</p>
<img src="/img/blog/aaa_deferred_gbuffer.webp" alt="Deferred shading G-buffer split into final color, world normals, depth, and material ID panels" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Engel, <a href="https://download.nvidia.com/developer/presentations/2004/6800_Leagues/6800_Leagues_Deferred_Shading.pdf" target="_blank" rel="noreferrer">Deferred Shading</a> (NVIDIA, the foundational talk).</li>
<li>Burns and Hunt, <a href="http://jcgt.org/published/0002/02/04/" target="_blank" rel="noreferrer">The Visibility Buffer: A Cache-Friendly Approach to Deferred Shading</a> (JCGT 2013, the original visibility buffer paper).</li>
<li>Wihlidal, <a href="http://www.frostbite.com/2016/03/optimizing-the-graphics-pipeline-with-compute/" target="_blank" rel="noreferrer">&quot;Optimizing the Graphics Pipeline with Compute&quot;</a> (GDC 2016, Frostbite's compute-based deferred path).</li>
<li>Karis, Stubbe, Wihlidal, <a href="https://advances.realtimerendering.com/s2021/Karis_Nanite_SIGGRAPH_Advances_2021_final.pdf" target="_blank" rel="noreferrer">A Deep Dive into Nanite Virtualized Geometry</a> (SIGGRAPH 2021, includes the Nanite visibility buffer in detail).</li>
</ul>
<h2 id="_3-virtualized-geometry-nanite-style" tabindex="-1">3. Virtualized geometry (Nanite-style) <a class="header-anchor" href="#_3-virtualized-geometry-nanite-style" aria-label="Permalink to &quot;3. Virtualized geometry (Nanite-style)&quot;"></a></h2>
<p>Meshes are pre-built into a hierarchy of clusters. At runtime the GPU streams and selects clusters at the resolution that matches each pixel, so you get sub-pixel-accurate detail without manual LODs. Unreal's Nanite is the most visible example. Other engines now ship their own variants. The practical result is film-quality assets in real time without LOD authoring.</p>
<img src="/img/blog/aaa_virtualized_geometry.webp" alt="Smooth render on the left, sub-pixel meshlet clusters in vivid colors on the right" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Karis, Stubbe, Wihlidal, <a href="https://advances.realtimerendering.com/s2021/Karis_Nanite_SIGGRAPH_Advances_2021_final.pdf" target="_blank" rel="noreferrer">A Deep Dive into Nanite Virtualized Geometry</a> (SIGGRAPH 2021).</li>
<li>Brian Karis, <a href="https://www.youtube.com/watch?v=eviSykqSUUw" target="_blank" rel="noreferrer">Nanite GDC 2021 talk</a> (an accessible video walkthrough).</li>
<li>Liktor, <a href="https://research.activision.com/publications/2021/09/geometry-rendering-pipeline-architecture-at-activision" target="_blank" rel="noreferrer">Geometry Rendering Pipeline Architecture at Activision</a> (cluster-based rendering, 2021).</li>
<li>Schied et al., <a href="https://research.nvidia.com/publication/2017-07_spatiotemporal-variance-guided-filtering-real-time-reconstruction-path-traced" target="_blank" rel="noreferrer">Spatiotemporal Variance-Guided Filtering</a> (related cluster culling techniques).</li>
</ul>
<h2 id="_4-real-time-ray-tracing-for-shadows-reflections-and-ao" tabindex="-1">4. Real-time ray tracing for shadows, reflections, and AO <a class="header-anchor" href="#_4-real-time-ray-tracing-for-shadows-reflections-and-ao" aria-label="Permalink to &quot;4. Real-time ray tracing for shadows, reflections, and AO&quot;"></a></h2>
<p>Hardware ray tracing (DXR, Vulkan RT) traces shadow rays, mirror and glossy reflection rays, and ambient occlusion rays against a BVH built each frame. Even a few rays per pixel beat what screen-space techniques can do, especially for off-screen reflections and contact shadows. Most games use it surgically, not for everything.</p>
<img src="/img/blog/aaa_hardware_raytracing.webp" alt="Glossy red sports car in a luxury showroom with accurate ray-traced reflections and contact shadows" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Microsoft, <a href="https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html" target="_blank" rel="noreferrer">DirectX Raytracing (DXR) Functional Spec</a> (the API reference).</li>
<li>Wyman, <a href="http://intro-to-dxr.cwyman.org/" target="_blank" rel="noreferrer">Introduction to DirectX Raytracing</a> (the SIGGRAPH course notes, very approachable).</li>
<li>Boksansky and Marrs, <a href="https://www.realtimerendering.com/raytracinggems/rtg2/index.html" target="_blank" rel="noreferrer">Ray Tracing Gems II, chapters 17-19</a> (free PDF, modern DXR techniques).</li>
<li>Stachowiak, <a href="https://www.ea.com/frostbite/news/stochastic-screen-space-reflections" target="_blank" rel="noreferrer">Stochastic Screen-Space Reflections</a> (Frostbite, the bridge from SSR to RT).</li>
</ul>
<h2 id="_5-software-ray-tracing-lumen-style" tabindex="-1">5. Software ray tracing (Lumen-style) <a class="header-anchor" href="#_5-software-ray-tracing-lumen-style" aria-label="Permalink to &quot;5. Software ray tracing (Lumen-style)&quot;"></a></h2>
<p>Not every player has an RTX card, so engines also ship distance-field or surface-cache fallbacks. Unreal's Lumen, for instance, traces against signed distance fields and surface caches for cheap diffuse GI, and only escalates to hardware rays when needed. It's how AAA games hit &quot;ray-traced look&quot; on consoles.</p>
<img src="/img/blog/aaa_software_rt_lumen.webp" alt="Cathedral interior with stained-glass color bleed and an SDF wireframe overlay revealing approximated geometry" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Wright et al., <a href="https://advances.realtimerendering.com/s2022/SIGGRAPH2022-Advances-Lumen-Wright%20et%20al.pdf" target="_blank" rel="noreferrer">Lumen: Real-Time Global Illumination in Unreal Engine 5</a> (SIGGRAPH 2022, the Lumen paper).</li>
<li>Epic Games, <a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/lumen-technical-details-in-unreal-engine" target="_blank" rel="noreferrer">Lumen Technical Details</a> (official engine docs).</li>
<li>Wright, <a href="https://advances.realtimerendering.com/s2021/Wright%20et%20al%20-%20Radiance%20Caching%20for%20Real-Time%20Global%20Illumination%20-%20SIGGRAPH%202021.pdf" target="_blank" rel="noreferrer">Radiance Caching for Real-Time Global Illumination</a> (SIGGRAPH 2021).</li>
<li>Wright, <a href="https://www.youtube.com/watch?v=2GYXuM10riw" target="_blank" rel="noreferrer">Lumen GDC 2022 talk</a> (video version).</li>
</ul>
<h2 id="_6-restir-and-reservoir-sampling" tabindex="-1">6. ReSTIR and reservoir sampling <a class="header-anchor" href="#_6-restir-and-reservoir-sampling" aria-label="Permalink to &quot;6. ReSTIR and reservoir sampling&quot;"></a></h2>
<p>For direct and indirect lighting with thousands of light sources, ReSTIR (Reservoir Spatio-Temporal Importance Resampling) reuses light samples across pixels and frames. It's how games like Cyberpunk 2077 with Path Tracing keep noise low at one or two rays per pixel. Expect to see it in more engines as path tracing becomes the high-end target.</p>
<img src="/img/blog/aaa_ray_tracing_restir.webp" alt="Cyberpunk street with ray-traced reflections in wet pavement and visible bounced light paths" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Bitterli et al., <a href="https://research.nvidia.com/sites/default/files/pubs/2020-07_Spatiotemporal-reservoir-resampling/ReSTIR.pdf" target="_blank" rel="noreferrer">Spatiotemporal Reservoir Resampling for Real-Time Ray Tracing with Dynamic Direct Lighting</a> (SIGGRAPH 2020, the original ReSTIR paper).</li>
<li>Ouyang et al., <a href="https://research.nvidia.com/publication/2021-06_restir-gi-path-resampling-real-time-path-tracing" target="_blank" rel="noreferrer">ReSTIR GI: Path Resampling for Real-Time Path Tracing</a> (HPG 2021, indirect-illumination ReSTIR).</li>
<li>Lin et al., <a href="https://research.nvidia.com/publication/2022-07_generalized-resampled-importance-sampling-foundations-restir" target="_blank" rel="noreferrer">Generalized Resampled Importance Sampling</a> (SIGGRAPH 2022, the math foundations).</li>
<li>NVIDIA, <a href="https://www.nvidia.com/en-us/geforce/news/cyberpunk-2077-ray-tracing-overdrive-technology-preview-on-rtx-4090/" target="_blank" rel="noreferrer">Cyberpunk 2077 Path Tracing Tech Deep Dive</a> (engineering blog).</li>
</ul>
<h2 id="_7-volumetric-clouds-fog-and-atmosphere" tabindex="-1">7. Volumetric clouds, fog, and atmosphere <a class="header-anchor" href="#_7-volumetric-clouds-fog-and-atmosphere" aria-label="Permalink to &quot;7. Volumetric clouds, fog, and atmosphere&quot;"></a></h2>
<p>Skies are ray-marched through 3D noise and density volumes. Atmosphere uses precomputed scattering tables (Bruneton-style) for sun and moon transitions. Fog is a froxel grid (think a 3D texture aligned to the view frustum) that captures local lighting. Together they give you &quot;weather as a system&quot; instead of a skybox.</p>
<img src="/img/blog/aaa_volumetrics.webp" alt="Mountain valley at sunset with volumetric clouds and god rays piercing through misty air" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Schneider, <a href="https://www.guerrilla-games.com/read/the-real-time-volumetric-cloudscapes-of-horizon-zero-dawn" target="_blank" rel="noreferrer">The Real-Time Volumetric Cloudscapes of Horizon Zero Dawn</a> (SIGGRAPH 2015, the canonical clouds reference).</li>
<li>Hillaire, <a href="https://sebh.github.io/publications/egsr2020.pdf" target="_blank" rel="noreferrer">A Scalable and Production Ready Sky and Atmosphere Rendering Technique</a> (EGSR 2020, the modern Bruneton successor used in UE5).</li>
<li>Wronski, <a href="https://bartwronski.com/2014/08/22/volumetric-fog-siggraph-2014/" target="_blank" rel="noreferrer">Volumetric Fog: Unified Compute Shader Based Solution to Atmospheric Scattering</a> (SIGGRAPH 2014, Assassin's Creed 4 froxel fog).</li>
<li>Hillaire, <a href="https://www.ea.com/frostbite/news/physically-based-unified-volumetric-rendering-in-frostbite" target="_blank" rel="noreferrer">Physically Based and Unified Volumetric Rendering in Frostbite</a> (SIGGRAPH 2015).</li>
</ul>
<h2 id="_8-cascaded-shadow-maps-and-virtual-shadow-maps" tabindex="-1">8. Cascaded shadow maps and virtual shadow maps <a class="header-anchor" href="#_8-cascaded-shadow-maps-and-virtual-shadow-maps" aria-label="Permalink to &quot;8. Cascaded shadow maps and virtual shadow maps&quot;"></a></h2>
<p>For sun shadows, cascaded shadow maps split the frustum into ranges and render each at appropriate resolution. Virtual shadow maps go further: a single huge shadow map is split into pages and only the pages visible from the camera get rendered. It's how AAA games keep crisp shadows near the player without a giant memory bill.</p>
<img src="/img/blog/aaa_cascaded_shadows.webp" alt="Outdoor scene with three colored cascade frustums overlaid showing shadow detail near the camera" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Dimitrov, <a href="https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf" target="_blank" rel="noreferrer">Cascaded Shadow Maps</a> (NVIDIA whitepaper, the standard reference).</li>
<li>Microsoft, <a href="https://learn.microsoft.com/en-us/windows/win32/dxtecharts/cascaded-shadow-maps" target="_blank" rel="noreferrer">Common Techniques to Improve Shadow Depth Maps</a> (DirectX docs).</li>
<li>Wright, <a href="https://advances.realtimerendering.com/s2023/index.html#VirtualShadowMaps" target="_blank" rel="noreferrer">Virtual Shadow Maps in Fortnite Battle Royale Chapter 4</a> (SIGGRAPH 2023, the UE5 VSM talk).</li>
<li>Epic Games, <a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/virtual-shadow-maps-in-unreal-engine" target="_blank" rel="noreferrer">Virtual Shadow Maps documentation</a>.</li>
</ul>
<h2 id="_9-screen-space-effects-ssao-ssr-ssgi-sssss" tabindex="-1">9. Screen-space effects (SSAO, SSR, SSGI, SSSSS) <a class="header-anchor" href="#_9-screen-space-effects-ssao-ssr-ssgi-sssss" aria-label="Permalink to &quot;9. Screen-space effects (SSAO, SSR, SSGI, SSSSS)&quot;"></a></h2>
<p>Reading the depth and normal buffers cheaply gives you ambient occlusion (SSAO), reflections (SSR), one-bounce global illumination (SSGI), and subsurface scattering for skin (SSSSS). They miss off-screen detail, which is why ray tracing is taking over, but they're still everywhere as a fast baseline.</p>
<img src="/img/blog/aaa_screen_space_effects.webp" alt="Side-by-side kitchen scene comparing flat lighting on the left with SSAO, SSR, and SSGI on the right" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Mittring, <a href="https://www.crytek.com/wp-content/uploads/Finding%20Next%20Gen%20-%20CryEngine%202.pdf" target="_blank" rel="noreferrer">Finding Next Gen: CryENGINE 2</a> (SIGGRAPH 2007, the original SSAO paper).</li>
<li>McGuire et al., <a href="https://research.nvidia.com/publication/scalable-ambient-obscurance" target="_blank" rel="noreferrer">Scalable Ambient Obscurance</a> (HPG 2012, modern SSAO).</li>
<li>Stachowiak and Uludag, <a href="https://www.ea.com/frostbite/news/stochastic-screen-space-reflections" target="_blank" rel="noreferrer">Stochastic Screen-Space Reflections</a> (Frostbite, the SSR reference).</li>
<li>Jimenez, <a href="https://www.iryoku.com/separable-sss/" target="_blank" rel="noreferrer">Separable Subsurface Scattering</a> (the technique used for skin in most AAA engines).</li>
<li>Mara et al., <a href="https://research.activision.com/publications/2016/09/deep-g-buffers-for-stable-global-illumination-approximation" target="_blank" rel="noreferrer">Deep Screen Space</a> (Activision, an SSGI lineage).</li>
</ul>
<h2 id="_10-temporal-anti-aliasing-and-ml-upscaling-dlss-fsr-xess" tabindex="-1">10. Temporal anti-aliasing and ML upscaling (DLSS, FSR, XeSS) <a class="header-anchor" href="#_10-temporal-anti-aliasing-and-ml-upscaling-dlss-fsr-xess" aria-label="Permalink to &quot;10. Temporal anti-aliasing and ML upscaling (DLSS, FSR, XeSS)&quot;"></a></h2>
<p>The frame is rendered at a lower internal resolution and reconstructed using motion vectors, depth, and history. ML-based upscalers (DLSS 3/4, FSR 3, XeSS) add frame generation on top, interpolating intermediate frames from optical flow. Most AAA titles now ship assuming an upscaler is on, which changes how you budget the rest of the frame.</p>
<img src="/img/blog/aaa_ml_upscaling.webp" alt="Side-by-side of a low-resolution input and a sharp ML-reconstructed output of the same character" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Karis, <a href="http://advances.realtimerendering.com/s2014/index.html#_HIGH-QUALITY_TEMPORAL_SUPERSAMPLING" target="_blank" rel="noreferrer">High Quality Temporal Supersampling</a> (SIGGRAPH 2014, the canonical TAA talk).</li>
<li>Salvi, <a href="https://research.nvidia.com/sites/default/files/pubs/2016-03_An-Excursion-in/dlss_DTAA-2-2.pdf" target="_blank" rel="noreferrer">An Excursion in Temporal Supersampling</a> (NVIDIA, on the path to DLSS).</li>
<li>Edelsten, <a href="https://www.gdcvault.com/play/1026184/" target="_blank" rel="noreferrer">Truly Next-Gen: Adding Deep Learning to Games and Graphics</a> (GDC 2019, DLSS architecture).</li>
<li>AMD, <a href="https://gpuopen.com/fidelityfx-super-resolution-3/" target="_blank" rel="noreferrer">FidelityFX Super Resolution 3 technical details</a> and Intel, <a href="https://www.intel.com/content/www/us/en/developer/articles/technical/intel-xess-technical-paper.html" target="_blank" rel="noreferrer">XeSS technical paper</a>.</li>
</ul>
<h2 id="_11-gpu-driven-rendering-and-mesh-shaders" tabindex="-1">11. GPU-driven rendering and mesh shaders <a class="header-anchor" href="#_11-gpu-driven-rendering-and-mesh-shaders" aria-label="Permalink to &quot;11. GPU-driven rendering and mesh shaders&quot;"></a></h2>
<p>Culling, LOD selection, and draw submission all run on the GPU. Mesh shaders replace the vertex/geometry/tessellation pipeline with a more flexible compute-style stage that emits meshlets. Combined with multi-draw indirect, this keeps the CPU out of the per-object hot loop entirely.</p>
<img src="/img/blog/aaa_gpu_driven_meshlets.webp" alt="Alien spaceport scene with colored meshlet cluster overlay showing GPU-driven rendering" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Haar and Aaltonen, <a href="https://advances.realtimerendering.com/s2015/aaltonenhaar_siggraph2015_combined_final_footer_220dpi.pdf" target="_blank" rel="noreferrer">GPU-Driven Rendering Pipelines</a> (SIGGRAPH 2015, the foundational Assassin's Creed Unity talk).</li>
<li>Wihlidal, <a href="https://www.frostbite.com/2016/03/optimizing-the-graphics-pipeline-with-compute/" target="_blank" rel="noreferrer">Optimizing the Graphics Pipeline with Compute</a> (GDC 2016, Frostbite GPU-driven culling).</li>
<li>Kubisch, <a href="https://developer.nvidia.com/blog/introduction-turing-mesh-shaders/" target="_blank" rel="noreferrer">Introduction to Turing Mesh Shaders</a> (NVIDIA, the mesh shader primer).</li>
<li>Pesce, <a href="https://www.bartwronski.com/" target="_blank" rel="noreferrer">A Whirlwind Tour of Mesh Shaders</a> (and similar engineering blogs collected on <a href="http://www.adriancourreges.com/blog/" target="_blank" rel="noreferrer">Adrian Courrèges' RenderDoc tear-downs</a>).</li>
</ul>
<h2 id="_12-hair-cloth-and-skin-rendering" tabindex="-1">12. Hair, cloth, and skin rendering <a class="header-anchor" href="#_12-hair-cloth-and-skin-rendering" aria-label="Permalink to &quot;12. Hair, cloth, and skin rendering&quot;"></a></h2>
<p>Hair uses Marschner-style anisotropic shading with strand-based geometry (NVIDIA HairWorks, AMD TressFX, or engine-native systems). Cloth is simulated on the GPU with position-based dynamics and rendered with anisotropic specular. Skin uses screen-space subsurface scattering plus pre-integrated wrap lighting. These three are usually where you spot the budget gap between AAA and indie.</p>
<img src="/img/blog/aaa_hair_cloth_skin.webp" alt="Close-up of a warrior with strand-based hair, cloth cloak, and subsurface-scattered skin" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Marschner et al., <a href="https://www.cs.cornell.edu/~srm/publications/SG03-hair.pdf" target="_blank" rel="noreferrer">Light Scattering from Human Hair Fibers</a> (SIGGRAPH 2003, the foundational hair model).</li>
<li>Chiang et al., <a href="https://benedikt-bitterli.me/pchfm/pchfm.pdf" target="_blank" rel="noreferrer">A Practical and Controllable Hair and Fur Model for Production Path Tracing</a> (Disney 2016, used widely in real-time approximations).</li>
<li>Müller et al., <a href="https://matthias-research.github.io/pages/publications/posBasedDyn.pdf" target="_blank" rel="noreferrer">Position Based Dynamics</a> (the standard cloth simulation reference).</li>
<li>Jimenez et al., <a href="https://www.iryoku.com/separable-sss/" target="_blank" rel="noreferrer">Separable Subsurface Scattering</a> and <a href="https://www.iryoku.com/translucency/" target="_blank" rel="noreferrer">Real-Time Realistic Skin Translucency</a>.</li>
</ul>
<h2 id="_13-decals-virtual-texturing-and-material-layering" tabindex="-1">13. Decals, virtual texturing, and material layering <a class="header-anchor" href="#_13-decals-virtual-texturing-and-material-layering" aria-label="Permalink to &quot;13. Decals, virtual texturing, and material layering&quot;"></a></h2>
<p>Surface variation comes from layered decals (bullet holes, dirt, blood, grime) projected onto the depth buffer, plus virtual textures that stream high-resolution detail just-in-time. Material layering blends multiple PBR sets per pixel using masks and triplanar projection, which is how a single rock looks like five rocks.</p>
<img src="/img/blog/aaa_decals_virtual_textures.webp" alt="Weathered concrete bunker wall with bullet decals, graffiti, and a virtual texture page atlas inset" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Pranckevičius, <a href="https://aras-p.info/blog/2009/02/27/deferred-decals/" target="_blank" rel="noreferrer">Deferred Decals</a> and follow-ups (Aras' classic blog series).</li>
<li>Mittring, <a href="http://advances.realtimerendering.com/s2012/Epic/UnrealEngine4_Mittring_SIGGRAPH2012_Final_Cleaned.pdf" target="_blank" rel="noreferrer">The Technology Behind the &quot;Unreal Engine 4 Elemental Demo&quot;</a> (SIGGRAPH 2012, includes virtual texturing details).</li>
<li>van Waveren, <a href="https://www.realtimerendering.com/blog/id-tech-5-challenges-from-texture-virtualization-to-massive-parallelization/" target="_blank" rel="noreferrer">id Tech 5 Challenges: From Texture Virtualization to Massive Parallelization</a> (SIGGRAPH 2009, the MegaTexture talk).</li>
<li>Williams, <a href="https://www.gdcvault.com/play/1022144/Crafting-the-World-of-The" target="_blank" rel="noreferrer">Material Layering in The Order: 1886</a> (GDC 2014, layered PBR materials).</li>
</ul>
<h2 id="_14-order-independent-transparency" tabindex="-1">14. Order-independent transparency <a class="header-anchor" href="#_14-order-independent-transparency" aria-label="Permalink to &quot;14. Order-independent transparency&quot;"></a></h2>
<p>Hair, foliage, particles, and glass don't sort cleanly. AAA engines use techniques like weighted blended OIT, depth peeling, or per-pixel linked lists to render them correctly without a CPU sort step. It's quietly one of the most expensive parts of the frame on a foliage-heavy scene.</p>
<img src="/img/blog/aaa_oit_transparency.webp" alt="Forest scene with overlapping translucent foliage, smoke, glass droplets, and translucent hair" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>McGuire and Bavoil, <a href="http://jcgt.org/published/0002/02/09/" target="_blank" rel="noreferrer">Weighted Blended Order-Independent Transparency</a> (JCGT 2013, the WBOIT paper).</li>
<li>Bavoil and Myers, <a href="https://developer.download.nvidia.com/SDK/10/opengl/src/dual_depth_peeling/doc/DualDepthPeeling.pdf" target="_blank" rel="noreferrer">Order Independent Transparency with Dual Depth Peeling</a> (NVIDIA whitepaper).</li>
<li>Yang et al., <a href="https://www.cse.chalmers.se/edu/year/2017/course/TDA362/Per-Pixel%20Linked%20List.pdf" target="_blank" rel="noreferrer">Real-Time Concurrent Linked List Construction on the GPU</a> (the per-pixel linked list reference).</li>
<li>Wyman, <a href="https://research.nvidia.com/publication/2016-06_exploring-and-expanding-continuum-oit-algorithms" target="_blank" rel="noreferrer">Exploring and Expanding the Continuum of OIT Algorithms</a> (HPG 2016, comparison survey).</li>
</ul>
<h2 id="_15-neural-radiance-caching-and-ml-denoisers" tabindex="-1">15. Neural radiance caching and ML denoisers <a class="header-anchor" href="#_15-neural-radiance-caching-and-ml-denoisers" aria-label="Permalink to &quot;15. Neural radiance caching and ML denoisers&quot;"></a></h2>
<p>The newest layer. NVIDIA's neural radiance cache learns indirect lighting per-scene and queries it instead of tracing more rays. ML denoisers (OptiX, Intel Open Image Denoise, custom in-house) clean up sparse ray-traced signals in milliseconds. Expect this category to grow fast over the next two years.</p>
<img src="/img/blog/aaa_neural_radiance_denoise.webp" alt="Noisy 1-spp ray-traced cathedral on the left, clean ML-denoised result on the right" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Deep dives:</p>
<ul>
<li>Müller et al., <a href="https://research.nvidia.com/publication/2021-06_real-time-neural-radiance-caching-path-tracing" target="_blank" rel="noreferrer">Real-Time Neural Radiance Caching for Path Tracing</a> (SIGGRAPH 2021, the NRC paper).</li>
<li>Schied et al., <a href="https://research.nvidia.com/publication/2017-07_spatiotemporal-variance-guided-filtering-real-time-reconstruction-path-traced" target="_blank" rel="noreferrer">Spatiotemporal Variance-Guided Filtering: Real-Time Reconstruction for Path-Traced Global Illumination</a> (HPG 2017, SVGF).</li>
<li>Chaitanya et al., <a href="https://research.nvidia.com/publication/interactive-reconstruction-monte-carlo-image-sequences-using-recurrent-denoising" target="_blank" rel="noreferrer">Interactive Reconstruction of Monte Carlo Image Sequences using a Recurrent Denoising Autoencoder</a> (SIGGRAPH 2017, the first recurrent ML denoiser).</li>
<li>Intel, <a href="https://www.openimagedenoise.org/documentation.html" target="_blank" rel="noreferrer">Open Image Denoise documentation</a> (open-source production denoiser).</li>
</ul>
<h2 id="what-this-means-for-the-browser" tabindex="-1">What this means for the browser <a class="header-anchor" href="#what-this-means-for-the-browser" aria-label="Permalink to &quot;What this means for the browser&quot;"></a></h2>
<p>We've shipped several of these techniques in WebGPU for our <a href="/blog/2026-02-25-open-world-browser-series-guide.html">open-world browser engine</a>. Cascaded shadow maps, GPU-driven instancing, triplanar PBR, screen-space fog, and clipmap-based virtualized terrain all run at 120 FPS in a tab. The rest (hardware ray tracing, mesh shaders, ML upscaling) is coming to the web as the WebGPU spec catches up. For deeper dives, see our guides on <a href="/guides/browser-3d-open-world-tech.html">browser open-world tech</a> and <a href="/guides/landscape-generation-browser.html">landscape generation</a>.</p>
<h2 id="further-reading-across-the-whole-stack" tabindex="-1">Further reading across the whole stack <a class="header-anchor" href="#further-reading-across-the-whole-stack" aria-label="Permalink to &quot;Further reading across the whole stack&quot;"></a></h2>
<p>If you want to read one book, <a href="https://www.realtimerendering.com/" target="_blank" rel="noreferrer">Real-Time Rendering, 4th edition</a> is the standard reference covering most of the topics above. For ongoing research, the SIGGRAPH &quot;Advances in Real-Time Rendering in Games&quot; course archive (<a href="https://advances.realtimerendering.com/" target="_blank" rel="noreferrer">advances.realtimerendering.com</a>) has free PDFs of the AAA-engine deep dives going back to 2006. For production tear-downs of how specific games render their frame, <a href="http://www.adriancourreges.com/blog/" target="_blank" rel="noreferrer">Adrian Courrèges' GPU profiling articles</a> are required reading.</p>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/blog/aaa_rendering_hero.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 18: A scatter brush that feels AI-placed]]></title>
            <link>https://app.cinevva.com/blog/2026-05-02-open-world-browser-part-18-ai-scattering</link>
            <guid>https://app.cinevva.com/blog/2026-05-02-open-world-browser-part-18-ai-scattering</guid>
            <pubDate>Sat, 02 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 37 built a heuristic scatter brush that keeps trees off cliffs and pebbles at the waterline with no LLM, then chased it through instancing, distance LOD, and a stack of bugs back to a 4 ms paint stroke.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-18-a-scatter-brush-that-feels-ai-placed" tabindex="-1">Building an open world in the browser, part 18: A scatter brush that feels AI-placed <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-18-a-scatter-brush-that-feels-ai-placed" aria-label="Permalink to &quot;Building an open world in the browser, part 18: A scatter brush that feels AI-placed&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-05-01-open-world-browser-part-17-animations-and-search.html">Part 17</a> gave the player a combat-grade animation set and a way to pull any CC0 model into the world. This part goes back to the creator's side. Spike 34's palette places one prop per click, which is fine for staging a hero object and useless for a forest. Spike 37 is the brush: drag across the terrain and trees fill in where trees belong.</p>
<h2 id="ai-placed-without-an-ai" tabindex="-1">&quot;AI-placed&quot; without an AI <a class="header-anchor" href="#ai-placed-without-an-ai" aria-label="Permalink to &quot;&quot;AI-placed&quot; without an AI&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/37-ai-scattering/" title="Spike 37 AI-Assisted Prop Scattering" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/37-ai-scattering/" target="_blank">Open Spike 37 in a new tab ↗</a> · <a href="/blog/spike-source?spike=37-ai-scattering">View source</a></p>
<p>The question this spike answers is whether a purely heuristic brush feels intelligent enough to skip the LLM. The test of &quot;AI-placed&quot; is concrete: trees stay off cliffs, rocks tilt into the slope, beach pebbles stop at the waterline, all on the first stroke. We got there with slope and altitude predicates, weighted draws, and per-family spacing, and not a single model call.</p>
<p>The brush works on a 257×257 CPU heightmap with hand-tuned features so every preset has somewhere to land: northern mountains for the mixed-slope picks, an eastern cliff strip for scree, a southern coastal plain for beach and meadow, a lake bowl in the south-west. The terrain bakes vertex colors from an <code>(altitude, slope)</code> biome classifier, so before you paint a single tree you can see where a preset will fire. Five presets ship as flat data, each a list of picks like <code>{ category, weight, slopeMin, slopeMax, altMin, altMax, minSpacing, alignToSlope }</code>. Cliff and Scree sets <code>slopeMin: 0.3</code> so rocks only land on actual slopes and <code>alignToSlope: true</code> so each boulder's up vector follows the surface normal.</p>
<p>For each stroke the scatter engine samples <code>densityPerM2 × area</code> candidate points inside the brush disc, reads height and slope per candidate, filters the preset's picks to those whose predicates pass, weighted-draws one, then runs a spacing check against an in-radius spatial hash. The whole thing is deterministic: a seedable Mulberry32 RNG owns every draw, so <code>(seed, brush events)</code> reproduces any session exactly. On the boot terrain a Mixed Forest stroke on flat meadow placed 139 of 158 candidates in 5 ms, while the same preset on a cliff placed only 106 of 226 and the HUD reported 81 of those rejected by slope. That rejection breakdown is the whole UX: you can see <em>why</em> the cliff only took a few trees instead of guessing.</p>
<p>The point of keeping presets as flat data is that the LLM version, when it lands, is a JSON swap rather than a rewrite. <code>paint({ preset })</code> doesn't care whether <code>preset.picks</code> came from a hand-tuned recipe or a worker that expanded &quot;deciduous forest with mossy boulders&quot; into weights. The engine never hardcodes a prop id either, so dropping in a different catalog needs no engine changes.</p>
<h2 id="from-300-draw-calls-to-49" tabindex="-1">From 300 draw calls to 49 <a class="header-anchor" href="#from-300-draw-calls-to-49" aria-label="Permalink to &quot;From 300 draw calls to 49&quot;"></a></h2>
<p>The first cut rendered each placement as a <code>clone(true)</code> of a multi-mesh group, which is fine at a few hundred props and a wall at the 2,500 cap, where draw calls climb into the thousands. We swapped to <code>InstancedMesh</code> before that hit, with one bucket per <code>(propId, partIndex)</code>. Each bucket grows by doubling: allocate a bigger <code>InstancedMesh</code>, copy the live matrices, swap the scene parent, dispose the old attribute. Erase is a swap-remove, so removing one instance is O(1) regardless of bucket size. Determinism, spacing, and the rejection HUD all carry through unchanged because the swap lives entirely below the placement record.</p>
<p>A diagnostic on the MegaKit pack settled a real architecture question. A multi-primitive glTF mesh (trunk plus leaves) can reach three.js as either one mesh with an array material and <code>geometry.groups</code>, or as separate sibling meshes with one material each. The loader takes the second path for this pack: every part is a single-material mesh with empty groups. That's the better shape for scatter, because separate buckets per primitive let the trunk bucket grow independently from the leaf bucket if their counts diverge. Same draw-call count either way, better memory shape with the split. The measured win held up: a forest stroke that was ~300 draw calls became 49, and a full multi-stroke session reached 3,221 instances at 75 FPS in 51 draw calls, a cap the clone path could never reach before frame budget collapsed.</p>
<h2 id="distance-lod-and-four-bugs-hiding-in-it" tabindex="-1">Distance LOD, and four bugs hiding in it <a class="header-anchor" href="#distance-lod-and-four-bugs-hiding-in-it" aria-label="Permalink to &quot;Distance LOD, and four bugs hiding in it&quot;"></a></h2>
<p>Instancing cut draw calls but every instance still drew its full triangle count, even the trees 90 m out contributing two pixels of leaf detail. So we baked three LOD levels per prop part with meshoptimizer (full, 50%, 15%), extended the bucket key to <code>(propId, partIndex, lod)</code>, and added a <code>move()</code> that shuttles a placement between sibling buckets with no allocation. Distance bands are 0 to 30 m, 30 to 90 m, and beyond, with ±4 m of hysteresis around each boundary so a camera hovering near a band edge doesn't thrash a placement back and forth re-uploading its matrix every frame. Re-evaluation is capped at 4 Hz and gated on the camera actually having moved, so a still camera costs one squared-distance compare per frame.</p>
<p>That LOD path is where the instructive bugs lived. The first showed up as placements vanishing or duplicating as the camera orbited, worse as the scene filled up. The cause was a shared scratch matrix: <code>move()</code> read a placement's transform into the module-scoped <code>_tmpMat</code>, but the source bucket's swap-remove used that same <code>_tmpMat</code> for its own internal shuffle, clobbering the carried matrix before the destination wrote it. The bug only spared the case where the moved slot was already last in its bucket, roughly a <code>1/count</code> chance, which is exactly the &quot;rare flicker that gets worse as the scene grows&quot; the playtest saw. Fix was a dedicated <code>_carryMat</code> reserved for <code>move()</code> alone. Stress-tested at 1,274 cumulative moves, the cluster stayed pixel-identical.</p>
<p>The second bug was subtler: every LOD transition felt smooth <em>except</em> the first one. Trees crossing into LOD1 visibly shifted shading even though their silhouette barely changed, while bigger triangle drops later in the ladder looked fine. The simplifier with <code>LockBorder</code> never moves or invents vertices, so surviving vertices keep their normals exactly, but we were calling <code>computeVertexNormals()</code> after every simplification anyway. LOD0 returns the original artist-authored normals untouched; LOD1 and up got three.js's generic face-average recompute. The 0-to-1 boundary was the only place in the ladder where the normal regime changed, so that's where the pop lived. Dropping the one defensive line fixed the shading and, as a bonus, cut per-prop bake time roughly in half because we stopped recomputing normals on four LODs per part.</p>
<p>Auditing what the simplifier produced surfaced a third win. Each LOD was an <code>original.clone()</code> with a fresh index, and <code>BufferGeometry.clone()</code> deep-copies every attribute, so five LODs held five independent copies of position, normal, UV, and color buffers whose values were bit-identical across all of them. We refactored to share attribute references and only own a private index buffer per LOD, dropping a typical tree part from 20 distinct attribute identities to 9 and uploading each vertex buffer to the GPU once. Two contracts come with aliased storage: don't mutate attribute data through any single LOD, and don't <code>dispose()</code> a single LOD geometry, since both would hit every sibling that shares the buffer.</p>
<p>The fourth bug had nothing to do with painting. Just waving the cursor over the terrain dropped the frame rate, with no button held. The <code>pointermove</code> handler raycast against the terrain mesh, a 131,072-triangle plane with no spatial structure, so three.js walked the entire index buffer per event at up to 1,000 events per second. We didn't need the mesh for that lookup at all, because the terrain is a parametric heightmap. An adaptive ray-march against <code>sampleHeight</code> (big strides high above the surface, a 0.4 m floor near it, then 12 bisections on the sign flip) costs roughly 8 to 30 samples per ray instead of 131,072 triangle tests, about three orders of magnitude cheaper, and hover holds the frame cap again.</p>
<h2 id="the-cost-just-moves-make-sure-it-moves-off-the-click" tabindex="-1">The cost just moves; make sure it moves off the click <a class="header-anchor" href="#the-cost-just-moves-make-sure-it-moves-off-the-click" aria-label="Permalink to &quot;The cost just moves; make sure it moves off the click&quot;"></a></h2>
<p>After swapping the spike to <code>WebGPURenderer</code> on three r184 (the production target), a DevTools profile showed the very first paint blocking for 265 ms, 79% of it inside the meshoptimizer WASM. The bake was real work, around 180 simplify calls for a cold preset, but it was running inside the click handler because <code>preloadProps</code> only fetched and parsed scenes, never triggered the LOD bake. The fix was to make preset selection do the full bake in the background: <code>preloadProps</code> now calls the part-resolution path, caches the in-flight promise so a fast click joins it instead of forking a duplicate, and memoizes the per-geometry preprocessing the simplifier was redoing four times per part. First paint dropped from 209 ms to 4 ms in the HUD. The WASM time didn't vanish, it just left the user's critical path and runs while they're looking at the terrain deciding where to paint.</p>
<p>That's the recurring lesson of this spike. Almost none of these fixes changed what the brush <em>does</em>. They changed <em>when</em> the cost lands: off the click, off the hover, off the boundary the camera is hovering near. A scatter tool that feels instant isn't doing less work, it's doing the work where the user isn't waiting on it.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Heuristic suitability scatter.</strong> A brush samples candidate points in a disc, reads <code>(height, slope)</code> per point from a CPU heightmap, filters a preset's picks by slope and altitude predicates, weighted-draws one, and rejects it if it violates per-family minimum spacing tracked in a spatial hash. Slope-aligned picks rotate their up vector to the surface normal. This produces placement that reads as intentional (trees off cliffs, rocks tilted into slopes, pebbles stopping at the waterline) with no learned weights, and keeps the preset as flat data so an LLM-generated pick list is a drop-in swap.</p>
<p><strong>Deterministic placement under async loads.</strong> A seedable Mulberry32 RNG owns every draw, so <code>(seed, brush events)</code> reproduces a session exactly. RNG draws happen before any <code>await</code>, and spacing reservations are inserted into the spatial index before the glTF clone resolves, so concurrent candidates respect each other and async asset loading can't perturb the sequence.</p>
<p><strong>Bucketed InstancedMesh with O(1) edits.</strong> One <code>InstancedMesh</code> per <code>(propId, partIndex, lod)</code>, capacity doubled on demand by copying live matrices into a larger buffer. Erase and FIFO-evict are swap-remove with a back-reference array patching the moved instance's index, so a removal is O(1) regardless of bucket size. A diagnostic confirmed glTF parts arrive as single-material meshes, making one-bucket-per-primitive the active path and giving each primitive an independently growable bucket.</p>
<p><strong>Distance LOD with hysteresis and shared attribute buffers.</strong> Three meshopt-simplified levels per part, selected by distance bands with ±4 m hysteresis so a camera near a boundary doesn't thrash, re-evaluated at a capped rate and gated on real camera motion. Because <code>LockBorder</code> simplification never moves vertices, all LODs share one set of position/normal/UV/color buffers and differ only in their private index buffer, cutting distinct GPU vertex buffers by roughly half. Skipping a defensive <code>computeVertexNormals</code> keeps artist normals identical across LODs and removes the only shading discontinuity in the ladder. See <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">LOD and meshoptimizer</a>.</p>
<p><strong>Analytic heightmap raycast for high-frequency lookups.</strong> A <code>pointermove</code>-rate cursor lookup against a 131k-triangle plane mesh walks the whole index buffer per event. Replacing it with an adaptive ray-march against the analytic height function (large strides far from the surface, a small floor near it, bisection on the sign flip of <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.869ex;" xmlns="http://www.w3.org/2000/svg" width="16.358ex" height="2.566ex" role="img" focusable="false" viewBox="0 -750 7230.4 1134.3" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(389,0)"><g data-mml-node="mtext"><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(392,0)" style="stroke-width: 3;"/><path data-c="79" d="M69 -66Q91 -66 104 -80T118 -116Q118 -134 109 -145T91 -160Q84 -163 97 -166Q104 -168 111 -168Q131 -168 148 -159T175 -138T197 -106T213 -75T225 -43L242 0L170 183Q150 233 125 297Q101 358 96 368T80 381Q79 382 78 382Q66 385 34 385H19V431H26L46 430Q65 430 88 429T122 428Q129 428 142 428T171 429T200 430T224 430L233 431H241V385H232Q183 385 185 366L286 112Q286 113 332 227L376 341V350Q376 365 366 373T348 383T334 385H331V431H337H344Q351 431 361 431T382 430T405 429T422 429Q477 429 503 431H508V385H497Q441 380 422 345Q420 343 378 235T289 9T227 -131Q180 -204 113 -204Q69 -204 44 -177T19 -116Q19 -89 35 -78T69 -66Z" transform="translate(892,0)" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1453,-239.4) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2460.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3460.9,0)"><g data-mml-node="mtext"><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(389,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(1225,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(1617,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(2117,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(2395,0)" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2984,-150) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(6841.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><msub><mtext>ray</mtext><mi>y</mi></msub><mo>−</mo><msub><mtext>terrain</mtext><mi>y</mi></msub><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>) costs tens of samples instead of tens of thousands of triangle tests, about three orders of magnitude cheaper, and a pre-allocated output vector keeps the hot path allocation-free.</p>
<p><strong>Move work off the interaction's critical path.</strong> Expensive one-time work (meshopt LOD bakes, WGSL pipeline compiles) should run during idle gaps, not inside the click handler. Preloading the active preset's full bake on preset selection, caching the in-flight promise so a fast click joins rather than forks it, and memoizing per-geometry preprocessing dropped first-paint latency from 209 ms to 4 ms without doing any less total work.</p>
<hr>
<p>Part 18 of 29.
Previous: <a href="/blog/2026-05-01-open-world-browser-part-17-animations-and-search.html">Part 17 - Animations that didn't need retargeting, and a live asset search</a>
Next: <a href="/blog/2026-05-04-open-world-browser-part-19-imposters.html">Part 19 - The imposter that has to survive a forest</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 17: Animations that didn't need retargeting, and a live asset search]]></title>
            <link>https://app.cinevva.com/blog/2026-05-01-open-world-browser-part-17-animations-and-search</link>
            <guid>https://app.cinevva.com/blog/2026-05-01-open-world-browser-part-17-animations-and-search</guid>
            <pubDate>Fri, 01 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 35 retargeted 262 combat and locomotion clips onto the player rig, and built an offline parity test to keep them honest. Spike 36 wired a live Polyhaven model search with no build step.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-17-animations-that-didn-t-need-retargeting-and-a-live-asset-search" tabindex="-1">Building an open world in the browser, part 17: Animations that didn't need retargeting, and a live asset search <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-17-animations-that-didn-t-need-retargeting-and-a-live-asset-search" aria-label="Permalink to &quot;Building an open world in the browser, part 17: Animations that didn't need retargeting, and a live asset search&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p><a href="/blog/2026-04-28-open-world-browser-part-16-structure-and-authoring.html">Part 16</a> gave us a world you can place objects in. This part gives the player something better to do in it: a combat-grade animation set, and a way to pull any of a thousand CC0 models into the world by typing a search.</p>
<h2 id="_262-clips-one-skeleton-three-retargeting-fixes" tabindex="-1">262 clips, one skeleton, three retargeting fixes <a class="header-anchor" href="#_262-clips-one-skeleton-three-retargeting-fixes" aria-label="Permalink to &quot;262 clips, one skeleton, three retargeting fixes&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/35-combat-animations/" title="Spike 35 Combat Animations" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/35-combat-animations/" target="_blank">Open Spike 35 in a new tab ↗</a> · <a href="/blog/spike-source?spike=35-combat-animations">View source</a></p>
<p>The player rig is a CC4-style <code>3MIKE</code> skeleton with 213 bones, including an 80-plus-bone facial rig and full finger joints. The animation sources don't match it. The first cut retargeted hand-picked Mixamo combat clips, but the source pool was thin and locomotion was split awkwardly between Mixamo and a few Kimodo BVH idles. The real win came when we pivoted the source library to Quaternius's two Universal Animation Library packs, 262 clips total on one consistent UE5-style 65-bone mannequin: sword combos, bow archery, climbs, wallruns, dodges, hit reactions, emotes, and a much cleaner locomotion set.</p>
<p>That's a 65-bone source to a 213-bone target, with no shared bone names, T-pose orientations, or limb proportions. Every clip gets remapped in three steps: a bone-name map, a bind-pose alignment so both rigs share a reference orientation, and position-track scaling so the shorter source rig doesn't put the character knee-deep in the floor. Getting that right meant chasing a sequence of bugs that each taught something specific.</p>
<p>The character came out over-twisted, every shoulder and elbow rotated about 30° too far. The cause was a pose mismatch: the UAL source ships a true T-pose, CC4's bind is an A-pose, and the retargeter assumed both rigs sat in matching reference poses, so the A-to-T difference got added into every per-frame delta. The fix forces CC4's arm chain into a true T-pose to capture the bind, then retargets, so the deltas stay small.</p>
<p>Then the body stopped translating. Knockbacks recoiled the upper body but left the feet planted. UAL puts displacement on the <code>root</code> bone, not the pelvis, so reading pelvis position gave near-zero motion. The fix sums <code>root.position</code> and <code>pelvis.position</code>, scales the horizontal part, and writes a single hip-position track. While doing this we found vertical offsets were off by about 12%, because we'd used the global limb-proportion ratio for the Y axis when pelvis height needs the hip-to-floor ratio. Two ratios, two axes: <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="40.854ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 18057.6 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mtext"><path data-c="76" d="M338 431Q344 429 422 429Q479 429 503 431H508V385H497Q439 381 423 345Q421 341 356 172T288 -2Q283 -11 263 -11Q244 -11 239 -2Q99 359 98 364Q93 378 82 381T43 385H19V431H25L33 430Q41 430 53 430T79 430T104 429T122 428Q217 428 232 431H240V385H226Q187 384 184 370Q184 366 235 234L286 102L377 341V349Q377 363 367 372T349 383T335 385H331V431H338Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(528,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(972,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(1364,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(1753,0)" style="stroke-width: 3;"/><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" transform="translate(2031,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(2475,0)" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(2975,0)" style="stroke-width: 3;"/><path data-c="52" d="M130 622Q123 629 119 631T103 634T60 637H27V683H202H236H300Q376 683 417 677T500 648Q595 600 609 517Q610 512 610 501Q610 468 594 439T556 392T511 361T472 343L456 338Q459 335 467 332Q497 316 516 298T545 254T559 211T568 155T578 94Q588 46 602 31T640 16H645Q660 16 674 32T692 87Q692 98 696 101T712 105T728 103T732 90Q732 59 716 27T672 -16Q656 -22 630 -22Q481 -16 458 90Q456 101 456 163T449 246Q430 304 373 320L363 322L297 323H231V192L232 61Q238 51 249 49T301 46H334V0H323Q302 3 181 3Q59 3 38 0H27V46H60Q102 47 111 49T130 61V622ZM491 499V509Q491 527 490 539T481 570T462 601T424 623T362 636Q360 636 340 636T304 637H283Q238 637 234 628Q231 624 231 492V360H289Q390 360 434 378T489 456Q491 467 491 499Z" transform="translate(3253,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(3989,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(4489,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(4878,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(5156,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(5933.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6989.6,0) translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(7267.6,0)"><g data-mml-node="mtext"><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(389,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(889,0)" style="stroke-width: 3;"/><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" transform="translate(1281,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1781,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(2225,0)" style="stroke-width: 3;"/><path data-c="48" d="M128 622Q121 629 117 631T101 634T58 637H25V683H36Q57 680 180 680Q315 680 324 683H335V637H302Q262 636 251 634T233 622L232 500V378H517V622Q510 629 506 631T490 634T447 637H414V683H425Q446 680 569 680Q704 680 713 683H724V637H691Q651 636 640 634T622 622V61Q628 51 639 49T691 46H724V0H713Q692 3 569 3Q434 3 425 0H414V46H447Q489 47 498 49T517 61V332H232V197L233 61Q239 51 250 49T302 46H335V0H324Q303 3 180 3Q45 3 36 0H25V46H58Q100 47 109 49T128 61V622Z" transform="translate(2614,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(3364,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(3642,0)" style="stroke-width: 3;"/><path data-c="59" d="M518 0Q497 3 374 3Q253 3 232 0H221V46H254Q313 47 321 58Q324 62 324 167V273L221 446Q117 620 114 623Q106 631 91 634T31 637H11V683H20Q29 680 148 680Q273 680 294 683H305V637H287Q239 636 236 621Q236 619 321 475L407 332L483 460Q502 492 527 534Q563 594 563 604Q563 632 517 637H508V683H517H525Q533 683 545 683T571 682T600 681T626 681Q695 681 731 683H738V637H723Q640 633 613 588Q612 587 517 427L425 273V169V95Q425 66 428 59T444 49Q459 46 506 46H528V0H518Z" transform="translate(4198,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(12215.6,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z" style="stroke-width: 3;"/></g></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(12715.6,0)"><g data-mml-node="mtext"><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(394,0)" style="stroke-width: 3;"/><path data-c="75" d="M383 58Q327 -10 256 -10H249Q124 -10 105 89Q104 96 103 226Q102 335 102 348T96 369Q86 385 36 385H25V408Q25 431 27 431L38 432Q48 433 67 434T105 436Q122 437 142 438T172 441T184 442H187V261Q188 77 190 64Q193 49 204 40Q224 26 264 26Q290 26 311 35T343 58T363 90T375 120T379 144Q379 145 379 161T380 201T380 248V315Q380 361 370 372T320 385H302V431Q304 431 378 436T457 442H464V264Q464 84 465 81Q468 61 479 55T524 46H542V0Q540 0 467 -5T390 -11H383V58Z" transform="translate(894,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(1450,0)" style="stroke-width: 3;"/><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" transform="translate(1842,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(2286,0)" style="stroke-width: 3;"/><path data-c="48" d="M128 622Q121 629 117 631T101 634T58 637H25V683H36Q57 680 180 680Q315 680 324 683H335V637H302Q262 636 251 634T233 622L232 500V378H517V622Q510 629 506 631T490 634T447 637H414V683H425Q446 680 569 680Q704 680 713 683H724V637H691Q651 636 640 634T622 622V61Q628 51 639 49T691 46H724V0H713Q692 3 569 3Q434 3 425 0H414V46H447Q489 47 498 49T517 61V332H232V197L233 61Q239 51 250 49T302 46H335V0H324Q303 3 180 3Q45 3 36 0H25V46H58Q100 47 109 49T128 61V622Z" transform="translate(2730,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(3480,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(3758,0)" style="stroke-width: 3;"/><path data-c="59" d="M518 0Q497 3 374 3Q253 3 232 0H221V46H254Q313 47 321 58Q324 62 324 167V273L221 446Q117 620 114 623Q106 631 91 634T31 637H11V683H20Q29 680 148 680Q273 680 294 683H305V637H287Q239 636 236 621Q236 619 321 475L407 332L483 460Q502 492 527 534Q563 594 563 604Q563 632 517 637H508V683H517H525Q533 683 545 683T571 682T600 681T626 681Q695 681 731 683H738V637H723Q640 633 613 588Q612 587 517 427L425 273V169V95Q425 66 428 59T444 49Q459 46 506 46H528V0H518Z" transform="translate(4314,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(17779.6,0) translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mtext>verticalRatio</mtext><mo>=</mo><mo data-mjx-texclass="ORD" stretchy="false">|</mo><mrow data-mjx-texclass="ORD"><mtext>targetHipY</mtext></mrow><mrow data-mjx-texclass="ORD"><mo>/</mo></mrow><mrow data-mjx-texclass="ORD"><mtext>sourceHipY</mtext></mrow><mo data-mjx-texclass="ORD" stretchy="false">|</mo></math></mjx-assistive-mml></mjx-container> for Y, the proportion ratio for the horizontal. Overshoot gone. And a stream of <code>RL_BoneRoot.position</code> NaN warnings traced to mapping UAL's <code>root</code> (at <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.875ex" height="1.731ex" role="img" focusable="false" viewBox="0 -683 2596.6 765" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D44C" d="M66 637Q54 637 49 637T39 638T32 641T30 647T33 664T42 682Q44 683 56 683Q104 680 165 680Q288 680 306 683H316Q322 677 322 674T320 656Q316 643 310 637H298Q242 637 242 624Q242 619 292 477T343 333L346 336Q350 340 358 349T379 373T411 410T454 461Q546 568 561 587T577 618Q577 634 545 637Q528 637 528 647Q528 649 530 661Q533 676 535 679T549 683Q551 683 578 682T657 680Q684 680 713 681T746 682Q763 682 763 673Q763 669 760 657T755 643Q753 637 734 637Q662 632 617 587Q608 578 477 424L348 273L322 169Q295 62 295 57Q295 46 363 46Q379 46 384 45T390 35Q390 33 388 23Q384 6 382 4T366 1Q361 1 324 1T232 2Q170 2 138 2T102 1Q84 1 84 9Q84 14 87 24Q88 27 89 30T90 35T91 39T93 42T96 44T101 45T107 45T116 46T129 46Q168 47 180 50T198 63Q201 68 227 171L252 274L129 623Q128 624 127 625T125 627T122 629T118 631T113 633T105 634T96 635T83 636T66 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1040.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2096.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>Y</mi><mo>=</mo><mn>0</mn></math></mjx-assistive-mml></mjx-container>) onto a target bone whose position track gets normalized by dividing by its bind Y. Divide by zero, NaN, silently dropped track. The fix was to drop the root mapping entirely, since its translation already lives in the hip track.</p>
<p>The smallest fix was the most satisfying to watch. The character gripped the sword with limp bind-pose fingers because the bone map had no finger entries, and the retargeter only writes tracks for mapped bones. Adding 30 finger bones (five fingers, three segments, two hands, dropping UAL's fourth tip helper that doesn't deform) made the hand close on the grip and open on the release.</p>
<h2 id="proving-262-clips-without-watching-262-clips" tabindex="-1">Proving 262 clips without watching 262 clips <a class="header-anchor" href="#proving-262-clips-without-watching-262-clips" aria-label="Permalink to &quot;Proving 262 clips without watching 262 clips&quot;"></a></h2>
<p>You can't eyeball-verify a 262-clip library, so we built an offline trajectory parity test: a headless Node script that loads each pack, samples the source skeleton at 60Hz, runs the retargeting pipeline, and compares per-bone world positions and rotations against the source after scaling. Pelvis Y drift came in at 0.003 m max. The hands showed a constant 2.5° offset that first read as &quot;fingers aren't tracking,&quot; but a constant offset is the bind delta between the flat UAL hand and CC4's slightly cupped bind, and it's invariant across the clip. Real animation errors show up as drift that varies frame to frame. Once that was clear, the test became a one-shot regression check: if a clip's drift stops holding that constant baseline, a recent change broke retargeting.</p>
<p>A side-by-side reference mannequin made the visual half of debugging decisive. Pressing backslash shows whichever source rig owns the current clip next to the player, so a &quot;twisted shoulder&quot; question becomes &quot;is the twist in the source, or did retargeting add it?&quot; Numerical tests catch regressions, the visual reference catches bind-pose mistakes the numbers don't surface, and together they retired the guessing game.</p>
<p>With the pipeline solid, switching the WASD/jump/swim baseline from Mixamo to UAL was a thin alias map: the FSM still speaks generic state names like <code>idle</code> and <code>walk</code>, resolved to UAL clip names at playback. Swimming needed per-clip rig offsets because the freestyle and tread-water poses anchor the pelvis at different anatomical heights, so we submerge the rig half a meter for active swimming and deeper for treading, blending between them at a smooth 5Hz.</p>
<h2 id="type-a-word-get-a-model" tabindex="-1">Type a word, get a model <a class="header-anchor" href="#type-a-word-get-a-model" aria-label="Permalink to &quot;Type a word, get a model&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/36-polyhaven-models/" title="Spike 36 Polyhaven Models" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/36-polyhaven-models/" target="_blank">Open Spike 36 in a new tab ↗</a> · <a href="/blog/spike-source?spike=36-polyhaven-models">View source</a></p>
<p>Spike 34 ate a day hand-curating a CC0 pack. The long-term answer is a search box. Polyhaven publishes about 1,100 CC0 models behind a permissive JSON API and a deterministic CDN, and this spike wires the whole path (query, thumbnails, load, render) from a static page with no build step, in roughly 300 lines of vanilla JS plus three.js.</p>
<p>On boot it fetches the full catalog once, about 600 KB. Search is pure client-side scoring (name beats id beats category beats tag) with a 120 ms debounce, rendering the top 60 cards. Thumbnails lazy-load through an <code>IntersectionObserver</code> so typing doesn't fire 60 requests at once. The interesting piece is loading. Polyhaven's file endpoint exposes multi-file glTF, not GLB, with textures shared across resolutions and split into separate files, and it hands back an <code>include</code> map of relative path to absolute CDN URL. Rather than download and patch the JSON ourselves, we feed that map through <code>LoadingManager.setURLModifier</code>, which fires for every dependency the loader needs (the <code>.bin</code>, each texture) and resolves it through the CDN. One click, one apparent file. Both API and CDN set permissive CORS, verified with <code>curl</code> before any client code, so no proxy. PBR materials render correctly with <code>RoomEnvironment</code> and ACES tone mapping defaults with no per-asset fix-ups, and 1k textures keep a typical model at 2 to 5 MB instead of 20 to 40 MB at 4k.</p>
<p>A meshoptimizer WASM pass rounds it out with a non-destructive reducer: each mesh stashes a clone of its original geometry, and changing the ratio rebuilds an index buffer from that clone rather than simplifying cumulatively. Multi-material geometry simplifies per group and rebuilds <code>geometry.groups</code> so material slots don't collapse. An armchair goes from 5,626 triangles at full to 2,812 at half.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Skeleton retargeting with bind-pose alignment.</strong> Mapping animation from one skeleton to another with different bone names, proportions, and reference poses requires three corrections: a bone-name map, a bind-pose alignment so both rigs share a reference orientation (forcing the target's A-pose arm chain into the source's T-pose), and position-track scaling. A pose mismatch adds the A-to-T rotation into every per-frame delta, doubling joint rotation. Unmapped helper bones must be dropped, since a root bone at <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.875ex" height="1.731ex" role="img" focusable="false" viewBox="0 -683 2596.6 765" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D44C" d="M66 637Q54 637 49 637T39 638T32 641T30 647T33 664T42 682Q44 683 56 683Q104 680 165 680Q288 680 306 683H316Q322 677 322 674T320 656Q316 643 310 637H298Q242 637 242 624Q242 619 292 477T343 333L346 336Q350 340 358 349T379 373T411 410T454 461Q546 568 561 587T577 618Q577 634 545 637Q528 637 528 647Q528 649 530 661Q533 676 535 679T549 683Q551 683 578 682T657 680Q684 680 713 681T746 682Q763 682 763 673Q763 669 760 657T755 643Q753 637 734 637Q662 632 617 587Q608 578 477 424L348 273L322 169Q295 62 295 57Q295 46 363 46Q379 46 384 45T390 35Q390 33 388 23Q384 6 382 4T366 1Q361 1 324 1T232 2Q170 2 138 2T102 1Q84 1 84 9Q84 14 87 24Q88 27 89 30T90 35T91 39T93 42T96 44T101 45T107 45T116 46T129 46Q168 47 180 50T198 63Q201 68 227 171L252 274L129 623Q128 624 127 625T125 627T122 629T118 631T113 633T105 634T96 635T83 636T66 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1040.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2096.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>Y</mi><mo>=</mo><mn>0</mn></math></mjx-assistive-mml></mjx-container> triggers a silent divide-by-zero in position-track normalization.</p>
<p><strong>Two scale ratios for one rig.</strong> Horizontal displacement uses the global limb-proportion ratio (overall skeleton size), but vertical pelvis offset uses the hip-to-floor ratio <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="25.041ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 11068 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(278,0)"><g data-mml-node="mtext"><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(389,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(889,0)" style="stroke-width: 3;"/><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" transform="translate(1281,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1781,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(2225,0)" style="stroke-width: 3;"/><path data-c="48" d="M128 622Q121 629 117 631T101 634T58 637H25V683H36Q57 680 180 680Q315 680 324 683H335V637H302Q262 636 251 634T233 622L232 500V378H517V622Q510 629 506 631T490 634T447 637H414V683H425Q446 680 569 680Q704 680 713 683H724V637H691Q651 636 640 634T622 622V61Q628 51 639 49T691 46H724V0H713Q692 3 569 3Q434 3 425 0H414V46H447Q489 47 498 49T517 61V332H232V197L233 61Q239 51 250 49T302 46H335V0H324Q303 3 180 3Q45 3 36 0H25V46H58Q100 47 109 49T128 61V622Z" transform="translate(2614,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(3364,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(3642,0)" style="stroke-width: 3;"/><path data-c="59" d="M518 0Q497 3 374 3Q253 3 232 0H221V46H254Q313 47 321 58Q324 62 324 167V273L221 446Q117 620 114 623Q106 631 91 634T31 637H11V683H20Q29 680 148 680Q273 680 294 683H305V637H287Q239 636 236 621Q236 619 321 475L407 332L483 460Q502 492 527 534Q563 594 563 604Q563 632 517 637H508V683H517H525Q533 683 545 683T571 682T600 681T626 681Q695 681 731 683H738V637H723Q640 633 613 588Q612 587 517 427L425 273V169V95Q425 66 428 59T444 49Q459 46 506 46H528V0H518Z" transform="translate(4198,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(5226,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z" style="stroke-width: 3;"/></g></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(5726,0)"><g data-mml-node="mtext"><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(394,0)" style="stroke-width: 3;"/><path data-c="75" d="M383 58Q327 -10 256 -10H249Q124 -10 105 89Q104 96 103 226Q102 335 102 348T96 369Q86 385 36 385H25V408Q25 431 27 431L38 432Q48 433 67 434T105 436Q122 437 142 438T172 441T184 442H187V261Q188 77 190 64Q193 49 204 40Q224 26 264 26Q290 26 311 35T343 58T363 90T375 120T379 144Q379 145 379 161T380 201T380 248V315Q380 361 370 372T320 385H302V431Q304 431 378 436T457 442H464V264Q464 84 465 81Q468 61 479 55T524 46H542V0Q540 0 467 -5T390 -11H383V58Z" transform="translate(894,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(1450,0)" style="stroke-width: 3;"/><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" transform="translate(1842,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(2286,0)" style="stroke-width: 3;"/><path data-c="48" d="M128 622Q121 629 117 631T101 634T58 637H25V683H36Q57 680 180 680Q315 680 324 683H335V637H302Q262 636 251 634T233 622L232 500V378H517V622Q510 629 506 631T490 634T447 637H414V683H425Q446 680 569 680Q704 680 713 683H724V637H691Q651 636 640 634T622 622V61Q628 51 639 49T691 46H724V0H713Q692 3 569 3Q434 3 425 0H414V46H447Q489 47 498 49T517 61V332H232V197L233 61Q239 51 250 49T302 46H335V0H324Q303 3 180 3Q45 3 36 0H25V46H58Q100 47 109 49T128 61V622Z" transform="translate(2730,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(3480,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(3758,0)" style="stroke-width: 3;"/><path data-c="59" d="M518 0Q497 3 374 3Q253 3 232 0H221V46H254Q313 47 321 58Q324 62 324 167V273L221 446Q117 620 114 623Q106 631 91 634T31 637H11V683H20Q29 680 148 680Q273 680 294 683H305V637H287Q239 636 236 621Q236 619 321 475L407 332L483 460Q502 492 527 534Q563 594 563 604Q563 632 517 637H508V683H517H525Q533 683 545 683T571 682T600 681T626 681Q695 681 731 683H738V637H723Q640 633 613 588Q612 587 517 427L425 273V169V95Q425 66 428 59T444 49Q459 46 506 46H528V0H518Z" transform="translate(4314,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(10790,0) translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo data-mjx-texclass="ORD" stretchy="false">|</mo><mrow data-mjx-texclass="ORD"><mtext>targetHipY</mtext></mrow><mrow data-mjx-texclass="ORD"><mo>/</mo></mrow><mrow data-mjx-texclass="ORD"><mtext>sourceHipY</mtext></mrow><mo data-mjx-texclass="ORD" stretchy="false">|</mo></math></mjx-assistive-mml></mjx-container>, because the leg-to-torso proportion differs between rigs. Using one ratio for both axes leaves climbs overshooting and recoveries sinking below the floor. UAL displacement also lives on the <code>root</code> bone, not the pelvis, so both must be summed into a single hip-position track to preserve motion across knockback, climb, and locomotion clips.</p>
<p><strong>Offline trajectory parity testing.</strong> A headless script samples the source skeleton at 60Hz, runs the retargeting pipeline, and compares per-bone world transforms against the source. A constant per-frame offset is the harmless bind delta, while drift that varies frame to frame is a real error, so the test becomes a regression check that fires when a change drops or distorts a track. Per-pack GLB isolation (fresh load, freed before the next) avoids cache contention across the sweep.</p>
<p><strong>LoadingManager.setURLModifier for CDN glTF graphs.</strong> When a CDN ships glTF as a relative-URI graph with an accompanying include map (relative path to absolute URL), <code>setURLModifier</code> resolves every dependency the loader requests through the CDN without rewriting the JSON. This collapses a multi-file, multi-resolution distribution into a single-click load. Disposing each prior model's geometry, materials, and texture maps before the next load prevents hundreds of MB of GPU memory accumulating across a browsing session.</p>
<p><strong>Non-destructive mesh simplification.</strong> Storing a clone of each mesh's original geometry and rebuilding only the index buffer per simplification ratio keeps changes fast and avoids cumulative damage from repeated simplification. Running per <code>geometry.groups</code> slice and reconstructing the groups preserves multi-material assignments. See <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">LOD and meshoptimizer</a> for how this feeds distance-based LOD.</p>
<hr>
<p>Part 17 of 29.
Previous: <a href="/blog/2026-04-28-open-world-browser-part-16-structure-and-authoring.html">Part 16 - Structure for a world that keeps growing</a>
Next: <a href="/blog/2026-05-02-open-world-browser-part-18-ai-scattering.html">Part 18 - A scatter brush that feels AI-placed</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Anatomy of an AI game engine: what's actually inside the prompt]]></title>
            <link>https://app.cinevva.com/blog/2026-04-30-anatomy-of-an-ai-game-engine</link>
            <guid>https://app.cinevva.com/blog/2026-04-30-anatomy-of-an-ai-game-engine</guid>
            <pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cinevva looks like a single text box. Behind it sits an orchestrator, 26 typed tools, eleven asset providers, a live game iframe the agent can play, and a storefront wired to gameplay reels. Here's the whole machine, piece by piece.]]></description>
            <content:encoded><![CDATA[<h1 id="anatomy-of-an-ai-game-engine-what-s-actually-inside-the-prompt" tabindex="-1">Anatomy of an AI game engine: what's actually inside the prompt <a class="header-anchor" href="#anatomy-of-an-ai-game-engine-what-s-actually-inside-the-prompt" aria-label="Permalink to &quot;Anatomy of an AI game engine: what's actually inside the prompt&quot;"></a></h1>
<p><em>By <a href="/about.html">Mariana Muntean</a>, CEO of Cinevva</em></p>
<p>The most common thing strangers say about Cinevva is that it's an AI wrapper. They land on the homepage, see a single input box that reads &quot;Describe your game&quot;, and decide they've understood the product. They've understood the front door. The product is everything you don't see when you type that sentence, plus, as of this week, an open world your players walk into to find your game in the first place.</p>
<p>Behind that input is a full game engine, an orchestrator with 26 typed tools, a fleet of generative models, an aggregated asset search across eleven providers, a live game iframe the agent can read and write to, and a storefront where the same agent ships your finished game as a swipeable gameplay reel. None of that is exotic. All of it is plumbing. The interesting work was deciding what to wire to what.</p>
<p>This post walks through the machine top to bottom. It's the explanation we give technical investors and the explanation we give developers thinking about contributing. They want the same answer.</p>
<h2 id="_1-the-prompt-is-the-surface-not-the-system" tabindex="-1">1. The prompt is the surface, not the system <a class="header-anchor" href="#_1-the-prompt-is-the-surface-not-the-system" aria-label="Permalink to &quot;1. The prompt is the surface, not the system&quot;"></a></h2>
<img src="/img/blog/engine-anatomy/cinevva-engine-prompt.png" alt="The Cinevva prompt input on the homepage. A single text field reads 'Make a neon space shooter with asteroids, power-ups, and a synth soundtrack.' Below it are three suggestion chips and the Cinevva logo." style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>The first design decision was hiding everything until the user has skin in the game. The homepage has one job, which is to take a sentence and turn it into a project. We split the experience into a public side (the prompt, the showcases, the storefront feed) and a creator side (the IDE, the asset tools, the publish flow) so that the people who just want to play don't have to look at the cockpit.</p>
<p>That's why the placeholder rotates through things like &quot;Make a snake game with neon graphics&quot; and &quot;Top-down zombie shooter&quot;. They aren't tutorials. They're a permission slip. The user sees that &quot;weird arcade idea&quot; is a legal input.</p>
<p>The moment you submit, three things happen. The system creates a fresh game project with a stable ID. It seeds an empty file tree (<code>index.html</code>, <code>game.js</code>, <code>style.css</code>, <code>assets/</code>, <code>GDD.md</code>). And it routes your sentence into the orchestrator with a project context window that points at those empty files. From here on you're talking to an agent that has hands.</p>
<h2 id="_2-behind-the-input-a-full-browser-ide" tabindex="-1">2. Behind the input, a full browser IDE <a class="header-anchor" href="#_2-behind-the-input-a-full-browser-ide" aria-label="Permalink to &quot;2. Behind the input, a full browser IDE&quot;"></a></h2>
<img src="/img/blog/engine-anatomy/cinevva-engine-ide.png" alt="The Cinevva creator IDE in dark mode. Three panels: a file tree on the left with GDD.md, index.html, game.js, style.css and an assets folder; a syntax-highlighted code editor in the middle showing a Three.js renderer setup; and a live game preview on the right showing a top-down neon space shooter with a SCORE and LIVES HUD." style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>The creator workspace is a three-panel IDE that runs entirely in the browser. File tree on the left, code editor in the middle, live game iframe on the right, and a chat input running along the bottom. We picked this layout on purpose. People who already write code recognize it instantly. People who don't can ignore the middle panel and only ever talk to the chat.</p>
<p>The file tree isn't a metaphor. Every project really is a small static site of HTML, JS, CSS, generated images, GLB models, and audio files, served straight from R2 through a Cloudflare Worker. There's no build step. The reason matters. It means the agent can read and write the files you'd actually ship, and it means the same artifact runs in the iframe, on a phone wrapped in Capacitor, on Steam wrapped in Tauri, and inside Discord as an Embedded Activity. One filesystem, six distribution targets.</p>
<p>Every game we generate is required to expose two functions on <code>window</code>. <code>getGameState()</code> returns a plain object describing the running game (player position, score, lives, enemies, timers). <code>setGameState(patch)</code> applies a partial update back to the live game. That tiny API is the contract that lets the agent inspect and modify a running game without reloading it. We'll come back to why in section 6.</p>
<h2 id="_3-the-orchestrator-where-the-actual-engine-lives" tabindex="-1">3. The orchestrator, where the actual engine lives <a class="header-anchor" href="#_3-the-orchestrator-where-the-actual-engine-lives" aria-label="Permalink to &quot;3. The orchestrator, where the actual engine lives&quot;"></a></h2>
<img src="/img/blog/engine-anatomy/cinevva-engine-orchestrator.png" alt="A diagram of the Cinevva orchestrator. A central node labeled 'Orchestrator (LLM)' is connected to eight tool nodes arranged around it: list_game_files, read_files, edit_files, write_files, generate_image (Flux), generate_skybox (Blockade), generate_music (ElevenLabs), and rig_model (Tripo). A dotted line connects the orchestrator to a 'Game iframe' node labeled 'screenshots + console.'" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>The piece people miss when they call this &quot;an AI wrapper&quot; is that the AI doesn't write code so much as it conducts a small orchestra. The orchestrator is a tool-calling loop running against a frontier model, with a system prompt that's about a thousand lines long and a typed tool surface of 26 functions. The tool definitions are the engine. The model is the conductor.</p>
<p>The 26 tools fall into five families. Filesystem tools (<code>list_game_files</code>, <code>read_files</code>, <code>search_game_file</code>, <code>edit_files</code>, <code>write_files</code>, <code>delete_files</code>, <code>rename_files</code>) let the agent treat your game like any other codebase. Project tools (<code>create_new_game</code>, <code>list_games</code>, <code>open_game</code>, <code>ask_user</code>, <code>game_ready</code>) handle the lifecycle of a project and the handoff back to the human. Generative tools (<code>generate_image</code>, <code>generate_skybox</code>, <code>generate_music</code>, <code>generate_sfx</code>, <code>rig_model</code>, <code>apply_animation</code>, <code>list_animations</code>, <code>list_skybox_styles</code>) call out to specialized models that we'll cover next. Knowledge tools (<code>search_fonts</code>, <code>list_library_docs</code>, <code>search_library_docs</code>, <code>list_generations</code>) give the agent reliable, indexed sources of truth instead of hallucinated APIs. And runtime tools (<code>read_console_logs</code>, <code>execute_js</code>) let the agent see and touch the live game.</p>
<p>A typical first turn looks something like this. The user types &quot;Make a neon space shooter&quot;. The model emits a <code>write_files</code> call with <code>GDD.md</code> describing the design, then <code>write_files</code> again with <code>index.html</code>, <code>game.js</code>, and <code>style.css</code>. It calls <code>generate_music</code> with <code>prompt=&quot;synthwave space shooter, 120 BPM, driving bassline&quot;</code> which kicks off a job against ElevenLabs Music and returns an MP3 URL inside R2. It calls <code>generate_skybox</code> with a Blockade Labs model 3 style for the starfield. It calls <code>game_ready(title, message)</code> to flip the iframe to the new build, and finally <code>read_console_logs</code> to confirm the game booted without errors. If it sees a stack trace, it loops back to <code>edit_files</code> and fixes it before the user reports anything.</p>
<p>The interesting property of this design is that the system prompt and the tool schemas are the only thing that's product-specific. Swap the model and everything still works. We've migrated between three frontier models in the last year without changing the surface, because the surface is ours.</p>
<h2 id="_4-the-asset-side-a-model-fan-out" tabindex="-1">4. The asset side: a model fan-out <a class="header-anchor" href="#_4-the-asset-side-a-model-fan-out" aria-label="Permalink to &quot;4. The asset side: a model fan-out&quot;"></a></h2>
<img src="/img/blog/engine-anatomy/cinevva-engine-assets.png" alt="A three-panel asset generation dashboard. The left panel is a music generator with a prompt 'synthwave space shooter, 120 BPM, driving bassline,' a 60-second duration slider, and a Generate button, plus a playing waveform card titled 'Asteroid Drift Theme.' The middle panel is a skybox generator with the prompt 'deep space, distant nebula, two moons' and a grid of four 360 degree environment thumbnails. The right panel shows a rotating 3D model preview of a low-poly purple spaceship with a 'Rig + Walk' button and a status indicator that reads 'Rigging... 1m 42s left.'" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>The agent doesn't generate images, music, sound, or 3D. It calls specialists. The asset side of Cinevva is a fan-out across best-in-class models, glued together so that the output of one is a valid input to the next.</p>
<p>For images and sprites, <code>generate_image</code> calls Flux Pro 1.1 with a width and height that's always a multiple of 32 and an output format that flips to PNG when the prompt asks for transparency. For full songs the agent calls ElevenLabs Music with a stylistic prompt and a duration in seconds. For one-off effects, ElevenLabs SFX produces a clip from text like &quot;laser gun firing, sci-fi blaster&quot; in five to fifteen seconds. For 360 degree environments, <code>generate_skybox</code> calls Blockade Labs with one of about ninety styles, defaulting to model 3 Digital Painting. For 3D characters, <code>rig_model</code> sends a GLB to Tripo to skin and rig as a biped, quadruped, hexapod, octopod, serpentine, or aquatic creature, then <code>apply_animation</code> plays back any of fifteen presets (idle, walk, run, jump, climb, dive, slash, shoot, hurt, fall, turn, plus the non-biped marches).</p>
<p>Each one of these is a job, not a sync call. They take seconds to minutes. The orchestrator launches them, returns immediately with a <code>jobId</code>, and a separate channel pipes the result back into the conversation when the asset is ready. That changes the feel completely. The agent can keep building game logic while a 3D model finishes rigging in the background. The user never has to wait on a single critical path.</p>
<p>The artifacts all land in the same place. R2, behind <code>cdn.cinevva.com</code>, addressable as plain URLs. Which means the agent's next <code>write_files</code> call can simply embed <code>&lt;audio src=&quot;https://cdn.cinevva.com/audio/abc.mp3&quot;&gt;</code> and the game just works. No SDK, no asset bundler, no manifest.</p>
<h2 id="_5-asset-search-across-eleven-providers" tabindex="-1">5. Asset search across eleven providers <a class="header-anchor" href="#_5-asset-search-across-eleven-providers" aria-label="Permalink to &quot;5. Asset search across eleven providers&quot;"></a></h2>
<img src="/img/blog/engine-anatomy/cinevva-engine-asset-search.png" alt="The Cinevva asset search page in dark mode. A search bar at the top contains the query 'asteroid rock' and a row of provider filter pills shows All, PolyHaven, Sketchfab, Kenney, Quaternius, AmbientCG, OpenGameArt, Freesound, Smithsonian, Synty, and Jamendo, with Sketchfab highlighted. Below is a four-column grid of asteroid model thumbnails, each card showing its provider badge, a CC0 or CC-BY license tag, and a 'Use in game' button. A small caption in the upper right corner shows a chat bubble that reads 'ChatGPT: one of the best free asset tools for game devs.'" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Generation is great when you need something specific. Search is faster when something already exists. The <a href="/assets.html">Asset Library</a> is a federated search across eleven free and licensed providers, returned in a single ranked feed.</p>
<p>The provider registry sits in the worker and runs each query in parallel against PolyHaven, Sketchfab, Freesound, Kenney, AmbientCG, Quaternius, OpenGameArt, the Smithsonian 3D collection, TurboSquid, Synty, and Jamendo. Each provider implements a common interface (<code>search</code>, <code>getMetadata</code>, <code>isBrowsable</code>, <code>getSupportedTypes</code>), so adding a new one is a single TypeScript file. The aggregation layer handles cursor-based pagination across providers that paginate differently, deduplicates licenses, normalizes asset types into a common taxonomy (model, texture, audio, hdri, sprite), and tags each asset with its license so the agent never picks something it can't redistribute.</p>
<p>When ChatGPT recommended this tool last quarter as one of the best free asset finders for game devs, we went and read the recommendation. It was right about the part that matters. The point isn't that we have a search bar over a single library. The point is that an indie developer searching for &quot;stone wall texture&quot; gets results from PolyHaven and AmbientCG side by side with Kenney's stylized version and a photogrammetry option from Smithsonian, with one query, with consistent license metadata. The agent can hit the same endpoint and pick assets the same way a human does.</p>
<p>This is the part of the engine that often gets undersold. Most game-AI demos generate everything from scratch and end up with a uniform, glossy, slightly uncanny look. The interesting games on Cinevva mix generated assets with real CC0 photogrammetry from Smithsonian and a hand-made low-poly tree from Quaternius. The orchestrator can do that in one turn.</p>
<h2 id="_6-the-agent-plays-the-game" tabindex="-1">6. The agent plays the game <a class="header-anchor" href="#_6-the-agent-plays-the-game" aria-label="Permalink to &quot;6. The agent plays the game&quot;"></a></h2>
<img src="/img/blog/engine-anatomy/cinevva-engine-debug.png" alt="A live debugging view. On the left, a running neon space shooter showing the player ship dodging asteroids with a SCORE of 4820 and 2 lives remaining. On the right, two stacked panels: an execute_js panel showing the call JSON.stringify(getGameState()) and a returned JSON object with player position, velocity, health, score, lives, and asteroid count, and a read_console_logs panel showing the most recent log lines including spawn asteroid, powerup picked, a texture load warning, and an FPS reading of 58." style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>This is the part that surprises most engineers when we show it. The agent doesn't just write code into files. It runs the game in a sandboxed iframe, reads its console, takes screenshots layered for UI versus 3D scene, and executes JavaScript inside the live runtime to inspect or change state.</p>
<p><code>read_console_logs</code> returns the last fifty lines of <code>console.log</code>, <code>console.warn</code>, <code>console.error</code>, and uncaught exceptions from the iframe. After every <code>game_ready</code> call, the agent is required by the system prompt to call <code>read_console_logs</code> and fix any errors before handing back to the user. That single rule eliminates most of the &quot;the AI said it was done but the screen is black&quot; failures that other tools have.</p>
<p><code>execute_js</code> is the more powerful one. It runs arbitrary JavaScript inside the game's global scope. Because every game is required to implement <code>getGameState</code> and <code>setGameState</code>, the agent can run <code>JSON.stringify(getGameState())</code> to read the entire state of the running game, then <code>setGameState({player: {x: 500, y: 100}})</code> to teleport the player, <code>setGameState({lives: 99})</code> to grant invincibility, or <code>setGameState({level: 3})</code> to skip ahead. It can also poke at lower levels, like <code>scene.children.map(c =&gt; c.type)</code> to inspect the Three.js scene graph or <code>document.querySelectorAll('canvas').length</code> to confirm the renderer mounted.</p>
<p>You can see why this matters. When a user says &quot;the player gets stuck in the wall on level two&quot;, the agent doesn't have to guess. It opens level two, runs <code>getGameState()</code>, examines collision data, runs an <code>execute_js</code> patch to test a fix, and only then writes the fix to disk. It debugs the way you debug. We treat the iframe as a peer, not a target.</p>
<h2 id="_7-library-docs-the-boring-source-of-truth" tabindex="-1">7. Library docs, the boring source of truth <a class="header-anchor" href="#_7-library-docs-the-boring-source-of-truth" aria-label="Permalink to &quot;7. Library docs, the boring source of truth&quot;"></a></h2>
<p>We index the official documentation for the libraries games actually use (Three.js, Tone.js, Cannon, and growing) into a structured, searchable JSON store. The agent reaches for <code>search_library_docs(&quot;threejs&quot;, &quot;Mesh&quot;)</code> before it reaches for the open web. It's faster, it's versioned, and it never hallucinates a method that was renamed two minor releases ago.</p>
<p>This isn't glamorous. It's the difference between an AI that produces working code and an AI that produces convincing-looking code that crashes at line forty. Most of the visible quality of Cinevva-built games comes from this one decision.</p>
<h2 id="_8-from-playable-to-discoverable-reels-and-the-storefront" tabindex="-1">8. From playable to discoverable: reels and the storefront <a class="header-anchor" href="#_8-from-playable-to-discoverable-reels-and-the-storefront" aria-label="Permalink to &quot;8. From playable to discoverable: reels and the storefront&quot;"></a></h2>
<img src="/img/blog/engine-anatomy/cinevva-engine-reels.png" alt="A short-form gameplay reels storefront. In the center, a phone-shaped card plays a portrait video of a neon space shooter mid-combat with social UI icons stacked on the right (a heart with 14k, a chat bubble with 3.2k, a share arrow with 1.8k, and a 'Play Now' button). The reel label at the bottom reads 'Asteroid Drift • by @indiedev • 3.1M views.' To the left, a small chat snippet titled 'From the creator's chat' shows the assistant message 'Game is live. Reel auto-published to your storefront.' To the right, a Storefront panel lists three trending games: Asteroid Drift, Roadhawk Runner, and Birb Clicker, each with a view count and a 'Trending' badge." style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>A game that nobody plays is a private hobby. The hardest unsolved problem in indie development isn't building games anymore. It's distribution. So the engine doesn't stop at &quot;playable&quot;. It stops at &quot;in front of players&quot;.</p>
<p>Every project on Cinevva carries a stable share URL like <code>play.cinevva.com/{game-id}</code> that opens the latest build of the game in a full-page iframe with a tiny overlay for likes and shares. When the agent calls <code>game_ready</code>, it doesn't only flip the in-IDE preview. It also offers to publish a 15-second gameplay reel to the <a href="/play.html">Cinevva storefront</a>. The reel is an actual recording of the game being played, captured directly from the iframe (we ship a small recorder script that the agent injects into the game runtime), so what the viewer scrolls past is real gameplay, not a marketing edit.</p>
<p>That changes the loop a developer lives in. The old loop was code, build, package, upload, write a Steam page, fight an algorithm, watch a wishlist counter. The new loop is type a sentence, watch the agent build, hit publish, watch your game appear in the same swipe feed alongside everything else. Discovery happens in the same product where creation happens, on the same URL, with the same login.</p>
<p>For dev community readers, this is the part to inspect. Distribution has always been the moat that game engines didn't ship with. Unity ships an editor, you ship the game on Steam. Unreal ships an editor, you ship on the Epic Store. Cinevva ships both halves. The engine and the storefront talk to each other through the same orchestrator that built the game.</p>
<h2 id="_9-top-performers-the-discovery-flywheel" tabindex="-1">9. Top Performers: the discovery flywheel <a class="header-anchor" href="#_9-top-performers-the-discovery-flywheel" aria-label="Permalink to &quot;9. Top Performers: the discovery flywheel&quot;"></a></h2>
<img src="/img/blog/engine-anatomy/cinevva-engine-charts.png" alt="The Cinevva Top Performers chart. Three tabs at the top read Trending, Recent, and Top, with Top selected. Below, a podium row of three featured games: number one The Breaker Belt with 42.1k plays, number two Roadhawk Runner with 27.7k plays, and number three Asteroid Drift with 37.1k plays. Below the podium, a denser grid of six smaller game cards including Pixel Platformer, Zombie Shooter, Fruit Ninja Clone, Tower Defense, Card Battler, and Zombie Builder, each with thumbnails, creator handles, and play counts." style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>The reels feed is one half of the flywheel. The <a href="/charts.html">Top Performers chart</a> is the other. It ranks games by trending, recent, and all-time top, weighted by completion rate (did the player finish a session) rather than raw clicks. Completion is harder to game than installs. A game that holds attention for sixty seconds beats a game that gets opened and closed.</p>
<p>Every signal feeds back into the orchestrator's next turn. When the agent says &quot;let's add a power-up&quot;, it's not pulling from a fixed pattern library. It's pulling from a model that has watched what mechanics actually retain players on Cinevva this month. The flywheel isn't a chart at the bottom of the page. It's the training signal for the whole engine.</p>
<p>For investors, this is the data moat. We don't just have generated games. We have generated games with real session-level engagement metrics, attached to the prompts that produced them, attached to the asset choices the agent made, attached to the players who liked them. Every loop tightens the next one.</p>
<h2 id="_10-the-engine-becomes-a-game" tabindex="-1">10. The engine becomes a game <a class="header-anchor" href="#_10-the-engine-becomes-a-game" aria-label="Permalink to &quot;10. The engine becomes a game&quot;"></a></h2>
<p>Here's the part I've been hinting at on social for months. Everything in sections 1 through 9 describes a system that builds games. The harder thing to describe, until you see it, is what happens when the system itself becomes a place.</p>
<p>Game creation is hard work, even with everything we've shipped. If you've never made a game before, you're suddenly responsible for the plot, the characters, the world, the mechanics, who does what and when, how the light behaves, how the special effects layer on, the music, the sound effects, the camera moves. It's a lot. And on top of all that you want the result to feel like yours, not like a template someone else's tool spat out. The prompt and the orchestrator do most of the mechanical work for you. What they can't do alone is give your finished thing a stage to stand on with other people watching.</p>
<p>So we built the stage. For most of this year <a href="/about.html">Oleg</a> and the engine team have been publishing the work in a <a href="/blog/2026-02-25-open-world-browser-series-guide.html">14-part series</a> about putting an open world in the browser. Sculpted terrain you can carve in real time. Biomes that paint themselves rock or grass based on slope and altitude. 80,000 grass blades that bend in the wind on top of compute-generated terrain. A character that walks, sprints, jumps, and glides across heightmap and volumetric terrain in the same step. None of it was a tech demo for its own sake. It was the substrate for this.</p>
<p>Today the open world is live in early form. You put on an avatar and walk in. Your published games show up as visitable spaces around you, the way the <a href="/play.html">storefront reels</a> and the <a href="/charts.html">Top Performers chart</a> show them now, except you arrive on foot instead of by swipe. You can drop into a stranger's game from the world without leaving the world. You can stand next to the person who made the thing you just played and tell them what you'd add next. You can sculpt a piece of terrain with someone you just met and turn it into the seed of a new project. Discovery stops being a feed. It becomes a walk.</p>
<p>Our engine has always been a game engine in the technical sense. Today it becomes one in the literal sense too. The thing that builds your game is now a game itself, and the same orchestrator from section 3, the same asset rails from section 4, the same <code>getGameState</code> contract from section 6, the same reels from section 8, and the same chart from section 9 all compose into the one place your players walk around in. The boundary between making and playing was always artificial. Now it's gone.</p>
<h2 id="what-we-think-we-got-right" tabindex="-1">What we think we got right <a class="header-anchor" href="#what-we-think-we-got-right" aria-label="Permalink to &quot;What we think we got right&quot;"></a></h2>
<p>The first thing we got right was treating the prompt as a UI affordance instead of a product. The product is the orchestrator, the tool surface, the asset rails, and the storefront. The prompt is the cheapest possible way to get a user into the system.</p>
<p>The second was insisting on a flat, browser-native filesystem with no build step. It's why a Cinevva game ships to mobile, desktop, Steam, and Discord from one source of truth. It's why the agent can read what it wrote.</p>
<p>The third was the contract that every generated game implements <code>getGameState</code> and <code>setGameState</code>. That single line in the system prompt is what makes the agent feel like a collaborator instead of a code generator. The agent can play, and a system that can play can debug.</p>
<p>The fourth was wiring the storefront into the same loop as the engine. We're not going to win on raw model quality. There's no defensible advantage in being two months ahead of an open-weights generator. We win when the same agent that builds your game also publishes it, measures it, and learns from how strangers played it.</p>
<p>If you want to see the whole machine in motion, the homepage prompt is still the front door. Type a sentence. Watch what happens behind it. Then come back here and tell us which piece of the engine to write up next.</p>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/blog/engine-anatomy/cinevva-engine-prompt.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 16: Structure for a world that keeps growing]]></title>
            <link>https://app.cinevva.com/blog/2026-04-28-open-world-browser-part-16-structure-and-authoring</link>
            <guid>https://app.cinevva.com/blog/2026-04-28-open-world-browser-part-16-structure-and-authoring</guid>
            <pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 33 broke a 6,285-line monolith into modules and exposed three hidden init-order bugs. Spike 34 used that structure to add 100+ placeable props without bloating anything.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-16-structure-for-a-world-that-keeps-growing" tabindex="-1">Building an open world in the browser, part 16: Structure for a world that keeps growing <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-16-structure-for-a-world-that-keeps-growing" aria-label="Permalink to &quot;Building an open world in the browser, part 16: Structure for a world that keeps growing&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>Every spike since <a href="/blog/2026-04-13-open-world-browser-part-13-terrain-sculpting.html">part 13</a> had followed the same recipe: copy the previous monolith, add one feature. By the end of <a href="/blog/2026-04-13-open-world-browser-part-15-multiplayer-and-water.html">spike 32</a> that monolith was 6,285 lines of <code>index.html</code> in a single <code>&lt;script type=&quot;module&quot;&gt;</code>. Code search was noisy, finding where to add a feature took longer than writing it, and any architectural change touched a file too big to diff in your head. Before adding another feature we paid down the structure.</p>
<h2 id="breaking-the-monolith-with-zero-behavior-change" tabindex="-1">Breaking the monolith with zero behavior change <a class="header-anchor" href="#breaking-the-monolith-with-zero-behavior-change" aria-label="Permalink to &quot;Breaking the monolith with zero behavior change&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/33-code-structure/" title="Spike 33 Code Structure Refactor" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/33-code-structure/" target="_blank">Open Spike 33 in a new tab ↗</a> · <a href="/blog/spike-source?spike=33-code-structure">View source</a></p>
<p>The constraint was strict: every split had to be a pure refactor, not a redesign. The monolith became 19 <code>.mjs</code> files plus a 151-line host shell. Top-level modules for scene, water, grass, character, physics, multiplayer, and UI, a <code>wgsl.mjs</code> that holds every WGSL source string as the single source of GPU truth, and a <code>terrain/</code> subtree for heightmap, SDF, chunks, GPU buffer wrappers, brush, LOD, and persistence. Total code came out at 6,555 lines, basically the monolith plus import boilerplate. No net change in volume, a large change in navigability.</p>
<p>Then the page loaded to a black screen. Two error messages, two unrelated root causes. The first was a WebGPU complaint about a zero-byte buffer binding. In the monolith, the SDF brush buffer was lazily allocated when the first marching-cubes chunk appeared, and the bind-group factory happened to run later, after the buffer existed. Splitting <code>terrain/gpu.mjs</code> from <code>terrain/brush.mjs</code> reordered module evaluation so the factory now ran first and tried to bind a <code>null</code> placeholder. The fix was to defer bind-group creation to the first dispatch with a <code>getOrCreateBindGroup(chunk)</code> helper. The &quot;create everything up front&quot; pattern was an artifact of the monolith's single init path.</p>
<p>The second message was a scary-looking FBX skeleton warning that turned out to be a red herring. It had printed since spike 25 and was harmless. The character was missing only because the first bug cascaded: every compute dispatch threw, the heightmap never got written, height samples returned 0, and the character spawned at the origin and fell through the world. Fix the buffer, the character animates fine, warning and all.</p>
<p>That's the real lesson of the refactor. The monolith hid every &quot;X must exist before Y is built&quot; relationship inside top-to-bottom script order. Modularizing shuffled that order and surfaced three more latent ordering bugs: grass scattering before the heightmap upload, the water plane added before the environment map finished decoding, and persistence load completing after the first frame. All three were one-line fixes, and none would have been caught without the split.</p>
<h2 id="a-hundred-props-without-growing-anything" tabindex="-1">A hundred props without growing anything <a class="header-anchor" href="#a-hundred-props-without-growing-anything" aria-label="Permalink to &quot;A hundred props without growing anything&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/34-world-authoring/" title="Spike 34 World Authoring" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/34-world-authoring/" target="_blank">Open Spike 34 in a new tab ↗</a> · <a href="/blog/spike-source?spike=34-world-authoring">View source</a></p>
<p>Spike 34 was the test of whether the structure paid off. The goal was a first-person palette to place trees, rocks, bushes, mushrooms, and paths from a CC0 model pack, with terrain-aware alignment, persistence, multiplayer sync, and physics colliders, all without leaving the controls. Every line of new code landed in five new files under <code>src/props/</code>, and no existing module grew by more than ten hook-up lines.</p>
<p>The asset arc took a detour worth recording, because it's the kind of thing that eats a day. We started on Quaternius's Ultimate Nature pack, an FBX library with no embedded textures. The FBX materials shipped as <code>MeshPhong</code> with no map, so we wired a manual material-name-to-PNG table, converted Phong to Standard, and set color spaces by hand. About 30% of materials had no matching PNG and several names were ambiguous between similar trees. A second FBX pack had the same gap. The fix wasn't more mapping tables, it was a better-authored pack: Quaternius's Stylized Nature MegaKit ships 116 complete glTFs with embedded PBR materials and baked normals. Swapping <code>FBXLoader</code> for <code>GLTFLoader</code> deleted the cm-to-meters scaling, the texture table, and the Phong conversion, and shrank <code>library.mjs</code> by about 80 lines. The takeaway: glTF with PBR is the right pipeline for CC0 packs that ship it, and FBX with manual texture mapping was twice the code for half the quality.</p>
<p>A few sharp edges came with the glTF path. The palette renders 116 thumbnails, and WebGPU's <code>canvas.toDataURL()</code> returns blank for a <code>GPUCanvasContext</code> surface, so thumbnails render into a <code>RenderTarget</code>, read back with <code>readRenderTargetPixelsAsync</code>, and blit into a 2D canvas, minding WebGPU's 256-byte row alignment. Ghost previews clone each material to tint them green, which broke on meshes whose <code>material</code> is an array, fixed with an <code>Array.isArray</code> branch. And the 16-bit normal maps that shipped at ~200 MB got a one-off <code>mogrify -depth 8</code> down to ~32 MB, visually identical since browsers downsample at upload anyway.</p>
<h2 id="when-the-rendered-geometry-only-exists-on-the-gpu" tabindex="-1">When the rendered geometry only exists on the GPU <a class="header-anchor" href="#when-the-rendered-geometry-only-exists-on-the-gpu" aria-label="Permalink to &quot;When the rendered geometry only exists on the GPU&quot;"></a></h2>
<p>The most instructive bug was the ghost preview snapping in 1 to 2 meter steps as the cursor moved. The terrain meshes store vertex positions in a <code>StorageBufferAttribute</code> because the compute pipeline writes them directly on the GPU, so three.js's CPU <code>Raycaster</code> can't see them and returns nothing. The fallback was a coarse 1.5 m ray-march against the analytic heightmap, and that fixed stride was the grid the user saw. We replaced it with an adaptive march: stride 2.5 m while high above the surface, shrink to 0.4 m within 5 m of it, then bisect 14 times when the sign of <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.869ex;" xmlns="http://www.w3.org/2000/svg" width="16.358ex" height="2.566ex" role="img" focusable="false" viewBox="0 -750 7230.4 1134.3" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(389,0)"><g data-mml-node="mtext"><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(392,0)" style="stroke-width: 3;"/><path data-c="79" d="M69 -66Q91 -66 104 -80T118 -116Q118 -134 109 -145T91 -160Q84 -163 97 -166Q104 -168 111 -168Q131 -168 148 -159T175 -138T197 -106T213 -75T225 -43L242 0L170 183Q150 233 125 297Q101 358 96 368T80 381Q79 382 78 382Q66 385 34 385H19V431H26L46 430Q65 430 88 429T122 428Q129 428 142 428T171 429T200 430T224 430L233 431H241V385H232Q183 385 185 366L286 112Q286 113 332 227L376 341V350Q376 365 366 373T348 383T334 385H331V431H337H344Q351 431 361 431T382 430T405 429T422 429Q477 429 503 431H508V385H497Q441 380 422 345Q420 343 378 235T289 9T227 -131Q180 -204 113 -204Q69 -204 44 -177T19 -116Q19 -89 35 -78T69 -66Z" transform="translate(892,0)" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1453,-239.4) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2460.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3460.9,0)"><g data-mml-node="mtext"><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(389,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(1225,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(1617,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(2117,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(2395,0)" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2984,-150) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(6841.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><msub><mtext>ray</mtext><mi>y</mi></msub><mo>−</mo><msub><mtext>terrain</mtext><mi>y</mi></msub><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container> flips. That lands sub-millimeter precision at roughly 30 wide steps plus 14 bisections per cast. When the rendered geometry lives only on the GPU, don't fight the raycaster, march against the analytic source.</p>
<h2 id="colliders-that-stay-honest-through-edits" tabindex="-1">Colliders that stay honest through edits <a class="header-anchor" href="#colliders-that-stay-honest-through-edits" aria-label="Permalink to &quot;Colliders that stay honest through edits&quot;"></a></h2>
<p>We chose primitive proxies over convex hulls or mesh colliders. Quaternius props are blobby and low-poly with no meaningful concavities, so hulls would be roughly 50 times the code and 10 times the runtime cost for the same gameplay. Each prop reduces to one shape derived from its bounding box: trees and cacti to a vertical capsule, rocks to a sphere, logs to a horizontal capsule along the long axis, and decorative bushes and flowers to nothing. Rocks and logs are walkable (vertical-only push so you can stand on them), trees and cacti are blocking (full 3D push so you can't climb a trunk). An 8 m spatial hash keeps the per-frame test to the player's 3×3 neighborhood, typically zero to six props.</p>
<p>Two design decisions kept the system coherent. Terrain alignment is a manifest flag, not a category enum hard-coded in code, so the ghost preview and the committed placement read the same <code>placement.alignToTerrain</code> value and can't disagree. And placed props react to terrain edits through one helper: after a brush stroke (local or replayed from a peer), <code>refreshPlacementsInRadius</code> resamples the ground under every prop in the affected disc, re-applies alignment, and re-derives the collider endpoints. Sculpt a hill under a tree and the tree rides up with it. Persistence and multiplayer reuse spike 31's pattern exactly, storing a flat list of <code>{uid, propId, x, y, z, rotY, scale}</code> and mirroring place, remove, and nudge events over BroadcastChannel.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>ES module decomposition under WebGPU.</strong> Splitting a monolithic <code>&lt;script type=&quot;module&quot;&gt;</code> into bare-path <code>.mjs</code> imports needs no bundler when modules are served as static assets, and three.js TSL plays fine across module boundaries. The hidden cost is initialization order: a monolith encodes &quot;build X before Y&quot; in top-to-bottom script order, while modules evaluate in import order, which can run a GPU bind-group factory before its buffer exists. The fix pattern is lazy initialization (<code>getOrCreate...</code> on first use) and awaiting the right promise rather than relying on declaration order.</p>
<p><strong>glTF with embedded PBR vs FBX with manual mapping.</strong> glTF ships in meters, references its own textures, and gives <code>MeshStandardMaterial</code> directly, so a CC0 pack authored as glTF drops straight into a PBR pipeline. FBX packs without texture-binding metadata require a hand-maintained material-name-to-PNG table that drifts on every pack update, plus a Phong-to-Standard conversion and manual color-space tagging. A foliage safety net promotes <code>transparent</code> materials with no <code>alphaTest</code> to <code>alphaTest: 0.5</code> cutout cards so they sort correctly behind opaque geometry.</p>
<p><strong>WebGPU offscreen thumbnails.</strong> <code>canvas.toDataURL()</code> returns blank for a <code>GPUCanvasContext</code>-backed canvas because there's no path from a presentation surface back to a 2D context. Rendering into a <code>RenderTarget</code>, reading pixels with <code>readRenderTargetPixelsAsync</code>, and blitting into a 2D canvas works, as long as the blit steps at WebGPU's 256-byte-aligned read-back stride. Results cache in <code>localStorage</code> under a version-bumped key so pack changes invalidate stale renders.</p>
<p><strong>Adaptive ray-march against an analytic heightmap.</strong> When terrain vertices live in a GPU <code>StorageBufferAttribute</code>, the CPU raycaster can't see them. Marching the analytic height function with a large stride far from the surface, a small stride near it, and a binary-search refinement on the sign flip of <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.869ex;" xmlns="http://www.w3.org/2000/svg" width="16.358ex" height="2.566ex" role="img" focusable="false" viewBox="0 -750 7230.4 1134.3" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(389,0)"><g data-mml-node="mtext"><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(392,0)" style="stroke-width: 3;"/><path data-c="79" d="M69 -66Q91 -66 104 -80T118 -116Q118 -134 109 -145T91 -160Q84 -163 97 -166Q104 -168 111 -168Q131 -168 148 -159T175 -138T197 -106T213 -75T225 -43L242 0L170 183Q150 233 125 297Q101 358 96 368T80 381Q79 382 78 382Q66 385 34 385H19V431H26L46 430Q65 430 88 429T122 428Q129 428 142 428T171 429T200 430T224 430L233 431H241V385H232Q183 385 185 366L286 112Q286 113 332 227L376 341V350Q376 365 366 373T348 383T334 385H331V431H337H344Q351 431 361 431T382 430T405 429T422 429Q477 429 503 431H508V385H497Q441 380 422 345Q420 343 378 235T289 9T227 -131Q180 -204 113 -204Q69 -204 44 -177T19 -116Q19 -89 35 -78T69 -66Z" transform="translate(892,0)" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1453,-239.4) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2460.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3460.9,0)"><g data-mml-node="mtext"><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(389,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(1225,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(1617,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(2117,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(2395,0)" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2984,-150) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(6841.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><msub><mtext>ray</mtext><mi>y</mi></msub><mo>−</mo><msub><mtext>terrain</mtext><mi>y</mi></msub><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container> gives sub-millimeter cursor precision in a bounded number of samples. The same primitive powers the brush cursor and the prop ghost.</p>
<p><strong>Primitive capsule colliders with a spatial hash.</strong> Each prop is reduced to a category-derived capsule or sphere from its bounding box, recorded as <code>{kind, walkable, radius, p1, p2}</code> and registered in every 8 m hash bucket it overlaps. Per frame the player tests only the props in their 3×3 bucket neighborhood, one capsule-vs-capsule resolution each. Walkable proxies (rocks, logs) get a vertical-only push, blocking proxies (trees) get the full 3D push. See <a href="/guides/landscape-generation-browser.html#physics-on-sdf-terrain">SDF terrain collisions</a> for the capsule math this builds on.</p>
<hr>
<p>Part 16 of 29.
Previous: <a href="/blog/2026-04-13-open-world-browser-part-15-multiplayer-and-water.html">Part 15 - Replace the baseline, then sync it</a>
Next: <a href="/blog/2026-05-01-open-world-browser-part-17-animations-and-search.html">Part 17 - Animations that didn't need retargeting, and a live asset search</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Four bets I made about gaming in 2022. How they're aging.]]></title>
            <link>https://app.cinevva.com/blog/2026-04-24-2022-predictions-aging-well</link>
            <guid>https://app.cinevva.com/blog/2026-04-24-2022-predictions-aging-well</guid>
            <pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In a 2022 interview I called WebGL game engines, GPT-3 for dialogue, real-time creative collab, and the indie takeover. Four years later, here's where each one landed.]]></description>
            <content:encoded><![CDATA[<h1 id="four-bets-i-made-about-gaming-in-2022-how-they-re-aging" tabindex="-1">Four bets I made about gaming in 2022. How they're aging. <a class="header-anchor" href="#four-bets-i-made-about-gaming-in-2022-how-they-re-aging" aria-label="Permalink to &quot;Four bets I made about gaming in 2022. How they're aging.&quot;"></a></h1>
<p><em>By <a href="/about.html">Mariana Muntean</a>, CEO of Cinevva</em></p>
<img src="/img/blog/tech-done-different.jpg" alt="Tech Done Different podcast interview, May 2022" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>In May 2022, I sat down for an interview with Ted Harrington's <em>Tech Done Different</em> podcast. The original write-up is <a href="https://medium.com/@vio-202020/interview-with-tech-done-different-3b9bf74d99c2" target="_blank" rel="noreferrer">still live on Medium</a>. I had been running Cinevva for about two years. We had five people, $200K raised, a Mozilla Builders award on the wall, and a lot of opinions about where game development was heading.</p>
<p>Most of those opinions were not fashionable in 2022. Web3 was loud, AAA studios owned the conversation, GPT-3 was a research toy, and WebGL was something hobbyists used to make jam games. Saying &quot;any creative will be a game developer&quot; out loud got polite nods and zero checks.</p>
<p>I went back and read that interview last week. Some of what I said reads like wishful thinking. Some of it reads like I had a time machine. Here are the four bets that aged the best, with what 2026 actually looks like next to each one.</p>
<h2 id="bet-1-game-engines-belong-in-the-browser" tabindex="-1">Bet 1: Game engines belong in the browser <a class="header-anchor" href="#bet-1-game-engines-belong-in-the-browser" aria-label="Permalink to &quot;Bet 1: Game engines belong in the browser&quot;"></a></h2>
<p>What I said in 2022:</p>
<blockquote>
<p>WebGL technology can be used across multiple platforms, including mobile devices, unlike other APIs that may be restricted to only PCs. So whether you're in the comfort of your home or on the move, you can still create and publish. WebGL games use only a fraction of the CPU and GPU power that a traditional PC game would require.</p>
</blockquote>
<p>In 2022 this was a contrarian bet. The big engines all assumed downloads, installs, and patch cycles. Browser games meant Flash nostalgia and Friv.</p>
<p>In 2026, our <a href="/blog/2026-02-25-open-world-browser-series-guide.html">open-world engine runs at 120 FPS in a browser tab</a>. Terrain, trees, physics, multiplayer sync, all in one URL. No download, no app store, no install.</p>
<img src="/img/blog/open-world-browser-120fps.png" alt="The Cinevva open-world running at 120 FPS in a browser tab, with terrain, trees, physics, and a player capsule rendered in a single tab" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>The browser caught up because three things landed at once. WebGPU shipped in every major browser. SharedArrayBuffer and cross-origin isolation became reliable. WebAssembly got fast enough to host real physics engines compiled from Rust. Our team <a href="/blog/2026-02-25-open-world-browser-series-guide.html">published a 12-part field journal</a> on what each of those steps actually cost. The summary is that the browser is now a real game runtime, not a place where you put the demo of the real game.</p>
<p>The bet I missed inside the bet: I underestimated how much of the work would be on the engineering culture side. Browser game engines are not held back by browsers anymore. They're held back by the assumption that a real game has a launcher.</p>
<h2 id="bet-2-ai-is-part-of-the-pipeline-not-a-side-feature" tabindex="-1">Bet 2: AI is part of the pipeline, not a side feature <a class="header-anchor" href="#bet-2-ai-is-part-of-the-pipeline-not-a-side-feature" aria-label="Permalink to &quot;Bet 2: AI is part of the pipeline, not a side feature&quot;"></a></h2>
<p>What I said in 2022:</p>
<blockquote>
<p>For our platform to work flawlessly we use open source game tools, but also develop our own technology using Natural Language Processing, Computer Vision, and Open AI solutions, such as GPT3 for dialogues and DALL:E for text to image generation.</p>
</blockquote>
<p>In May 2022, GPT-3 was a paid API most people had never used. DALL·E was barely a year old and wait-listed. Saying we were going to use them as production game-dev tools was, at best, ambitious cosplay.</p>
<p>In 2026, every serious creative pipeline has language models and image generation in it, including ours. Inside Cinevva today: ElevenLabs Music for soundtracks, ElevenLabs SFX for effects, Hunyuan3D and SAM3D for assets, Flux for visuals, Mubert for adaptive audio, AceStep for instrumentation. Prompts go in, usable assets come out, and the whole thing happens inside the editor.</p>
<img src="/img/blog/prompting-3d-games.png" alt="Prompt-driven 3D asset generation inside the Cinevva editor" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>The receipts on this one are in the GDC 2026 <em>State of the Game Industry</em> report. <a href="/signals/2026-03-28-the-52-52-split.html">52% of game companies say generative AI is being used in production</a>. The same 52% of developers say AI use is having a negative impact on the industry. Both numbers are true. The point is that the underlying technology became table stakes faster than anyone wanted to admit.</p>
<p>What I got wrong: I thought the resistance would come from engineers worried about quality. Most of it came from artists worried about jobs. That conversation is <a href="/signals/2026-03-16-when-ai-overrides-the-artist.html">much messier than I expected</a>, and the people who treat it as a clean win are not paying attention.</p>
<h2 id="bet-3-game-development-needs-google-docs-style-collaboration" tabindex="-1">Bet 3: Game development needs Google-Docs-style collaboration <a class="header-anchor" href="#bet-3-game-development-needs-google-docs-style-collaboration" aria-label="Permalink to &quot;Bet 3: Game development needs Google-Docs-style collaboration&quot;"></a></h2>
<p>What I said in 2022:</p>
<blockquote>
<p>Source control and work with app versions are often linked with remote collaboration. Today to do that you have to know where to go, what version of the application you need to download, be in sync with everyone, God forbid one member on the team is on the wrong version of the app, it becomes a problem. We aim for a Google doc near real-time kind of experience: simple, effortless but fun.</p>
</blockquote>
<p>Real-time collaboration in code tools was a fringe idea in 2022. Replit had it. VS Code Live Share existed. Most professional engines required Perforce, a VPN, and a senior engineer babysitting your branch.</p>
<p>In 2026, real-time collab is the default for any creative tool that wants to be taken seriously. Figma made it normal for design. Cursor and Bolt made it normal for code. Lovable made it normal for app prototyping. Game engines were the laggard, not the leader.</p>
<img src="/img/blog/open-world-chunk-streaming.png" alt="Streaming chunks of an open-world game between players in real time" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>Inside our own engine we now have multiplayer sync working as a <a href="/blog/2026-02-25-open-world-browser-part-04-streaming-before-fancy.html">first-class spike</a>, with chunk streaming, presence, and shared state in the browser. The experience is closer to &quot;open the link, jump in&quot; than &quot;set up Perforce, ask Slack for the right version, install the launcher&quot;.</p>
<p>The piece I underestimated: the hardest part of real-time collab in games is not networking. It's conflict resolution on creative intent. Two people editing the same level at the same time is a UX problem before it's a sync problem, and most tools haven't solved it yet.</p>
<h2 id="bet-4-the-future-is-indie" tabindex="-1">Bet 4: The future is indie <a class="header-anchor" href="#bet-4-the-future-is-indie" aria-label="Permalink to &quot;Bet 4: The future is indie&quot;"></a></h2>
<p>What I said in 2022:</p>
<blockquote>
<p>Creatives who were not able to build game content will be able to do so. We will discover abundant new ways of how people perceive and express creativity. This time the process will not be controlled by corporations or big AAA studios only. The future is indie.</p>
</blockquote>
<p>This was the most romantic line in the interview, and the easiest to dismiss in 2022. AAA budgets had never been higher. Indie projects had never had a harder time getting visibility. The math looked like it was going the other way.</p>
<p>In 2026, the math flipped. We made <a href="/blog/2026-02-18-a-breaker-belt.html">A Breaker Belt</a> in three days, two people, on web, mobile, and PC from one codebase. That used to take a small team a quarter. <a href="/signals/2026-03-23-solo-devs-shipping-not-vibing.html">Solo developers are shipping, not just vibing</a>, and Steam Next Fest 2026 had record entries from teams of one.</p>
<img src="/img/social/industry-solo-dev-shipping.png" alt="Solo developers shipping production-quality games using AI tooling and browser engines" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>The cost of finding out whether a weird idea works has collapsed. That's the real shift, and everything else, the AI in the pipeline, the browser as runtime, the real-time collab, is in service of it. A solo developer with the right tooling now has the production capacity that a 20-person studio had in 2018.</p>
<p>What I did not see coming clearly enough in 2022: the bottleneck moves from production to discovery. When everyone can ship, getting found becomes the hard problem. That's the next set of bets, and we're in the middle of making them.</p>
<h2 id="what-i-got-wrong-on-purpose" tabindex="-1">What I got wrong on purpose <a class="header-anchor" href="#what-i-got-wrong-on-purpose" aria-label="Permalink to &quot;What I got wrong on purpose&quot;"></a></h2>
<p>A few of the 2022 bets are still pending or need revision.</p>
<p>The Web3 angle was louder in that interview than it should have been. The encryption and zero-knowledge proof part holds up as a content-rights primitive. The token-and-marketplace framing aged poorly, and a lot of the energy I gave it in 2022 would have been better spent on AI tooling, where the real upside was.</p>
<p>I also underestimated how much of &quot;anyone can make a game&quot; is a discovery and distribution problem, not a tooling problem. Building is now easy. Getting found and getting paid are still hard, and they are where most of our work goes today.</p>
<p>That's the bet we're making right now. We just shipped a TikTok-style reels feed at <a href="/play.html">cinevva.com/play</a>. Vertical-swipe clips of actual gameplay, fifteen seconds each, no trailers and no marketing footage. If something catches your eye, one tap and you're playing it in your browser. No download, no install, no tutorial standing between you and the game.</p>
<p>The reason this matters for indie developers is brutal math. Steam Next Fest 2026 had a record number of entries, which sounds great until you do the per-game attention budget. Most demos got under a thousand impressions. The store-page funnel was already broken before AI made it worse. A scroll-and-swipe surface where the gameplay is the ad collapses the discovery loop into one motion. It puts solo developers in front of players the same way TikTok puts unknown musicians in front of listeners. That's the missing piece between &quot;anyone can ship&quot; and &quot;anyone can succeed&quot;.</p>
<p>If you want the original interview in full, <a href="https://medium.com/@vio-202020/interview-with-tech-done-different-3b9bf74d99c2" target="_blank" rel="noreferrer">it's here</a>. Reading it back was useful. Most of what we said out loud in 2022 turned out to be a working roadmap, just on a faster clock than we thought.</p>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/blog/tech-done-different.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 13: Terrain sculpting and the death of the math function]]></title>
            <link>https://app.cinevva.com/blog/2026-04-13-open-world-browser-part-13-terrain-sculpting</link>
            <guid>https://app.cinevva.com/blog/2026-04-13-open-world-browser-part-13-terrain-sculpting</guid>
            <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Three false starts, an exploding mesh, and a seam bug that forced us to rethink how terrain data works.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-13-terrain-sculpting-and-the-death-of-the-math-function" tabindex="-1">Building an open world in the browser, part 13: Terrain sculpting and the death of the math function <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-13-terrain-sculpting-and-the-death-of-the-math-function" aria-label="Permalink to &quot;Building an open world in the browser, part 13: Terrain sculpting and the death of the math function&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>For twelve parts we built a terrain engine that you could look at. Fly over it. Admire the seams not cracking. This time we wanted to touch it.</p>
<p>The goal sounded simple: let a player sculpt the terrain with a brush, in real time, without breaking any of the systems we spent 24 spikes building. It took three attempts, two bugs that looked like rendering failures but were data model failures, and a fundamental rethinking of how terrain data should work.</p>
<h2 id="three-false-starts" tabindex="-1">Three false starts <a class="header-anchor" href="#three-false-starts" aria-label="Permalink to &quot;Three false starts&quot;"></a></h2>
<p>Spike 25 was supposed to be the easy one. The production codebase already had a raycaster that hits terrain meshes. The placement tool uses it for dropping objects. A brush tool follows the same shape, it just modifies heightmap values instead of spawning a prefab. Simple.</p>
<p>Attempt one: I built it directly into the production <code>world/client/</code> TypeScript code. New <code>terrain-brush.ts</code>, modifications to <code>chunk.ts</code>, protocol changes, Vue component updates. Within an hour I had a brush that sort of worked, but chunk boundaries showed visible normal discontinuities. I couldn't tell if the bug was my brush code, the existing chunk stitching, or some interaction with the full render loop. That's exactly the situation the spike methodology exists to prevent. I'd skipped the rule and paid for it immediately. Reverted everything.</p>
<p>Attempt two: standalone spike, but I reached for Three.js 0.170.0 and WebGL. The production code uses WebGL, so it felt natural. But Spikes 13-24 had all moved to WebGPU. Building a WebGL brush spike would prove it works on the legacy renderer, not the one we're migrating toward. Wrong direction. Started over.</p>
<p>Attempt three: WebGPU, <code>WebGPURenderer</code>, compute shader for vertex generation, matching the Spike 22 stack. This time the architecture was right. Five brush operations working: raise, lower, smooth, flatten, noise. Brush cycle P95 under 4ms on M1.</p>
<p>And the seam bug was still there.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/25-heightmap-brush/" title="Spike 25 Heightmap Brush" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/25-heightmap-brush/" target="_blank">Open Spike 25 in a new tab ↗</a> · <a href="/blog/spike-source?spike=25-heightmap-brush">View source</a></p>
<h2 id="the-seam-bug-that-wouldn-t-die" tabindex="-1">The seam bug that wouldn't die <a class="header-anchor" href="#the-seam-bug-that-wouldn-t-die" aria-label="Permalink to &quot;The seam bug that wouldn't die&quot;"></a></h2>
<p>The standard fix for cross-chunk normal computation is border overlap: each chunk stores one extra ring of data from its neighbors, so the normal calculation at the boundary can sample both sides. I implemented that. Copied neighbor edge data into an expanded buffer. The seams still cracked.</p>
<p>I dug into the math. At the boundary between chunk A (cx=-1) and chunk B (cx=0), both chunks need to compute the same normal at the shared vertex. Chunk A's shader sampled <code>mix(own_col31, own_col32, 0.85)</code>. Chunk B sampled <code>mix(neighbor_edge, own_col0, 0.85)</code>. Those are different bilinear interpolation paths through different data. Even with correct border data, the two chunks compute different normals for the same point.</p>
<p>This was the moment I realized the border copy wasn't the real bug. The real bug was the data model.</p>
<p>Every spike from 1 to 24 used a procedural math function called <code>height_at()</code>. Feed it world coordinates, get a height. Clean, global, stateless. The brush couldn't modify a math function, so I'd added a <code>displacement</code> buffer on top. The terrain was now <code>height_at(x,z) + displacement[i]</code>. The GPU shader embedded 30 lines of noise functions for the base terrain plus bilinear interpolation code for the displacement overlay. The flatten brush had to subtract <code>height_at()</code> to figure out what displacement value would produce the target height. Two systems, layered on top of each other, computing different things with different sampling strategies.</p>
<p>None of that is how a real game works. In production, authored terrain is sampled data stored in buffers. The procedural function was a convenient stand-in from the early spikes. It had served its purpose. Now it was actively causing bugs.</p>
<p>I killed it.</p>
<p>Each chunk now owns a <code>heightmap</code> Float32Array with actual height values. At creation, procedural noise fills it. After that, the noise function is never called again. The brush modifies the stored heights directly. The GPU shader reads from one buffer using one function: <code>hm_at(i,j)</code>. Normals use grid-aligned central differences on the same data. No bilinear interpolation ambiguity. No two-system mismatch. The shader went from 90 lines to 40.</p>
<p>The seams fixed themselves. Both chunks at a shared edge now read the same discrete height values from their respective buffers (with correct neighbor interior points in the border overlap). Same data in, same normals out.</p>
<p>This wasn't a brush lesson. It was a data architecture lesson that the brush exposed.</p>
<h2 id="the-exploding-mesh" tabindex="-1">The exploding mesh <a class="header-anchor" href="#the-exploding-mesh" aria-label="Permalink to &quot;The exploding mesh&quot;"></a></h2>
<p>Spike 26 was the volumetric counterpart. Modify a 64-cubed SDF volume with a brush, then re-mesh with marching cubes. Same question as Spike 25, but in 3D.</p>
<p>The first time I ran it, the mesh exploded. Long spikes shooting in every direction, like a sea urchin having a bad day.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/26-sdf-brush/" title="Spike 26 SDF Brush" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/26-sdf-brush/" target="_blank">Open Spike 26 in a new tab ↗</a> · <a href="/blog/spike-source?spike=26-sdf-brush">View source</a></p>
<p>The MC case table I'd generated had 3840 entries instead of 4096. The full table is <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="26.441ex" height="1.756ex" role="img" focusable="false" viewBox="0 -694 11687 776" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(1500,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(1750,0)"><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(444,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(944,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1338,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(1782,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4148.2,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(5148.4,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(6148.4,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(6398.4,0)"><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(394,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(672,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(1172,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(1561,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(8631.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(9687,0)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="39" d="M352 287Q304 211 232 211Q154 211 104 270T44 396Q42 412 42 436V444Q42 537 111 606Q171 666 243 666Q245 666 249 666T257 665H261Q273 665 286 663T323 651T370 619T413 560Q456 472 456 334Q456 194 396 97Q361 41 312 10T208 -22Q147 -22 108 7T68 93T121 149Q143 149 158 135T173 96Q173 78 164 65T148 49T135 44L131 43Q131 41 138 37T164 27T206 22H212Q272 22 313 86Q352 142 352 280V287ZM244 248Q292 248 321 297T351 430Q351 508 343 542Q341 552 337 562T323 588T293 615T246 625Q208 625 181 598Q160 576 154 546T147 441Q147 358 152 329T172 282Q197 248 244 248Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1500,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>256</mn><mtext> </mtext><mtext>cases</mtext><mo>×</mo><mn>16</mn><mtext> </mtext><mtext>slots</mtext><mo>=</mo><mn>4096</mn></math></mjx-assistive-mml></mjx-container>, but I had <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="15.964ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 7056 759" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1722.2,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2722.4,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4000.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(5056,0)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" style="stroke-width: 3;"/><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1500,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>256</mn><mo>×</mo><mn>15</mn><mo>=</mo><mn>3840</mn></math></mjx-assistive-mml></mjx-container>, sixteen missing rows starting at case 112. Every lookup after that index was shifted, so the case number no longer matched the triangulation data. When marching cubes reads the wrong entry, it creates edges where both endpoints are on the same side of the surface. On a real crossing edge the interpolation parameter</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -1.909ex;" xmlns="http://www.w3.org/2000/svg" width="11.698ex" height="4.757ex" role="img" focusable="false" viewBox="0 -1259 5170.4 2102.8" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(638.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(1694.6,0)"><g data-mml-node="mrow" transform="translate(877.9,676)"><g data-mml-node="mo"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(778,0)"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(518,-150) scale(0.707)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mrow" transform="translate(220,-686)"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(518,-150) scale(0.707)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1093.6,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2093.8,0)"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(518,-150) scale(0.707)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g></g><rect width="3235.9" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>t</mi><mo>=</mo><mfrac><mrow><mo>−</mo><msub><mi>v</mi><mi>a</mi></msub></mrow><mrow><msub><mi>v</mi><mi>b</mi></msub><mo>−</mo><msub><mi>v</mi><mi>a</mi></msub></mrow></mfrac></math></mjx-assistive-mml></mjx-container><p>lands in <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="4.526ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 2000.7 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(278,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(778,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1222.7,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1722.7,0)"><path data-c="5D" d="M22 710V750H159V-250H22V-210H119V710H22Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">[</mo><mn>0</mn><mo>,</mo><mn>1</mn><mo stretchy="false">]</mo></math></mjx-assistive-mml></mjx-container> because <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.355ex;" xmlns="http://www.w3.org/2000/svg" width="2.131ex" height="1.358ex" role="img" focusable="false" viewBox="0 -443 942.1 600.1" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(518,-150) scale(0.707)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>v</mi><mi>a</mi></msub></math></mjx-assistive-mml></mjx-container> and <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.357ex;" xmlns="http://www.w3.org/2000/svg" width="1.971ex" height="1.359ex" role="img" focusable="false" viewBox="0 -443 871.3 600.8" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(518,-150) scale(0.707)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>v</mi><mi>b</mi></msub></math></mjx-assistive-mml></mjx-container> have opposite signs. On a fake edge they have the same sign, so <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.357ex;" xmlns="http://www.w3.org/2000/svg" width="6.868ex" height="1.676ex" role="img" focusable="false" viewBox="0 -583 3035.9 740.8" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(518,-150) scale(0.707)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1093.6,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2093.8,0)"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(518,-150) scale(0.707)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>v</mi><mi>b</mi></msub><mo>−</mo><msub><mi>v</mi><mi>a</mi></msub></math></mjx-assistive-mml></mjx-container> is near zero or flips sign and <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="0.817ex" height="1.441ex" role="img" focusable="false" viewBox="0 -626 361 637" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>t</mi></math></mjx-assistive-mml></mjx-container> shoots outside <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="4.526ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 2000.7 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(278,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(778,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1222.7,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1722.7,0)"><path data-c="5D" d="M22 710V750H159V-250H22V-210H119V710H22Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">[</mo><mn>0</mn><mo>,</mo><mn>1</mn><mo stretchy="false">]</mo></math></mjx-assistive-mml></mjx-container>, placing the vertex far outside the volume. Multiply that by a few hundred wrong cells and you get a hedgehog.</p>
<p>The fix was stupid simple: copy the proven table from Spike 12 byte-for-byte. Lesson learned. Never regenerate a lookup table when a proven copy exists.</p>
<p>The second bug was more subtle. The smooth brush was supposed to soften terrain features. Instead it created sharp creases. The problem: I was pulling every SDF value toward zero (the isosurface). That sounds like it should smooth things, but it collapses the distance field. Voxels above and below the surface both rush toward zero, flattening everything in the brush radius. At the boundary, smoothed voxels meet unsmoothed ones with a hard step. The &quot;smooth&quot; brush was a crease generator.</p>
<p>The fix was proper Laplacian smoothing. Instead of pulling each value toward zero, pull it toward the average of its 6 direct neighbors:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -3.222ex;" xmlns="http://www.w3.org/2000/svg" width="30.99ex" height="7.575ex" role="img" focusable="false" viewBox="0 -1924.1 13697.6 3348.2" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D719" d="M409 688Q413 694 421 694H429H442Q448 688 448 686Q448 679 418 563Q411 535 404 504T392 458L388 442Q388 441 397 441T429 435T477 418Q521 397 550 357T579 260T548 151T471 65T374 11T279 -10H275L251 -105Q245 -128 238 -160Q230 -192 227 -198T215 -205H209Q189 -205 189 -198Q189 -193 211 -103L234 -11Q234 -10 226 -10Q221 -10 206 -8T161 6T107 36T62 89T43 171Q43 231 76 284T157 370T254 422T342 441Q347 441 348 445L378 567Q409 686 409 688ZM122 150Q122 116 134 91T167 53T203 35T237 27H244L337 404Q333 404 326 403T297 395T255 379T211 350T170 304Q152 276 137 237Q122 191 122 150ZM500 282Q500 320 484 347T444 385T405 400T381 404H378L332 217L284 29Q284 27 285 27Q293 27 317 33T357 47Q400 66 431 100T475 170T494 234T500 282Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(629,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1200.7,0)"><path data-c="2190" d="M944 261T944 250T929 230H165Q167 228 182 216T211 189T244 152T277 96T303 25Q308 7 308 0Q308 -11 288 -11Q281 -11 278 -11T272 -7T267 2T263 21Q245 94 195 151T73 236Q58 242 55 247Q55 254 59 257T73 264Q121 283 158 314T215 375T247 434T264 480L267 497Q269 503 270 505T275 509T288 511Q308 511 308 500Q308 493 303 475Q293 438 278 406T246 352T215 315T185 287T165 270H929Q944 261 944 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2478.5,0)"><g data-mml-node="mi"><path data-c="1D719" d="M409 688Q413 694 421 694H429H442Q448 688 448 686Q448 679 418 563Q411 535 404 504T392 458L388 442Q388 441 397 441T429 435T477 418Q521 397 550 357T579 260T548 151T471 65T374 11T279 -10H275L251 -105Q245 -128 238 -160Q230 -192 227 -198T215 -205H209Q189 -205 189 -198Q189 -193 211 -103L234 -11Q234 -10 226 -10Q221 -10 206 -8T161 6T107 36T62 89T43 171Q43 231 76 284T157 370T254 422T342 441Q347 441 348 445L378 567Q409 686 409 688ZM122 150Q122 116 134 91T167 53T203 35T237 27H244L337 404Q333 404 326 403T297 395T255 379T211 350T170 304Q152 276 137 237Q122 191 122 150ZM500 282Q500 320 484 347T444 385T405 400T381 404H378L332 217L284 29Q284 27 285 27Q293 27 317 33T357 47Q400 66 431 100T475 170T494 234T500 282Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(629,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3623.7,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4623.9,0)"><path data-c="1D706" d="M166 673Q166 685 183 694H202Q292 691 316 644Q322 629 373 486T474 207T524 67Q531 47 537 34T546 15T551 6T555 2T556 -2T550 -11H482Q457 3 450 18T399 152L354 277L340 262Q327 246 293 207T236 141Q211 112 174 69Q123 9 111 -1T83 -12Q47 -12 47 20Q47 37 61 52T199 187Q229 216 266 252T321 306L338 322Q338 323 288 462T234 612Q214 657 183 657Q166 657 166 673Z" style="stroke-width: 3;"/></g><g data-mml-node="mrow" transform="translate(5373.6,0)"><g data-mml-node="mo"><path data-c="239B" d="M837 1154Q843 1148 843 1145Q843 1141 818 1106T753 1002T667 841T574 604T494 299Q417 -84 417 -609Q417 -641 416 -647T411 -654Q409 -655 366 -655Q299 -655 297 -654Q292 -652 292 -643T291 -583Q293 -400 304 -242T347 110T432 470T574 813T785 1136Q787 1139 790 1142T794 1147T796 1150T799 1152T802 1153T807 1154T813 1154H819H837Z" transform="translate(0,770.1)" style="stroke-width: 3;"/><path data-c="239D" d="M843 -635Q843 -638 837 -644H820Q801 -644 800 -643Q792 -635 785 -626Q684 -503 605 -363T473 -75T385 216T330 518T302 809T291 1093Q291 1144 291 1153T296 1164Q298 1165 366 1165Q409 1165 411 1164Q415 1163 416 1157T417 1119Q417 529 517 109T833 -617Q843 -631 843 -635Z" transform="translate(0,-780.1)" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(875,0)"><g data-mml-node="mn" transform="translate(220,676)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(220,-686)"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" style="stroke-width: 3;"/></g><rect width="700" height="60" x="120" y="220"/></g><g data-mml-node="munder" transform="translate(1981.7,0)"><g data-mml-node="mo" transform="translate(370.5,0)"><path data-c="2211" d="M60 948Q63 950 665 950H1267L1325 815Q1384 677 1388 669H1348L1341 683Q1320 724 1285 761Q1235 809 1174 838T1033 881T882 898T699 902H574H543H251L259 891Q722 258 724 252Q725 250 724 246Q721 243 460 -56L196 -356Q196 -357 407 -357Q459 -357 548 -357T676 -358Q812 -358 896 -353T1063 -332T1204 -283T1307 -196Q1328 -170 1348 -124H1388Q1388 -125 1381 -145T1356 -210T1325 -294L1267 -449L666 -450Q64 -450 61 -448Q55 -446 55 -439Q55 -437 57 -433L590 177Q590 178 557 222T452 366T322 544L56 909L55 924Q55 945 60 948Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(0,-1147.3) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D457" d="M297 596Q297 627 318 644T361 661Q378 661 389 651T403 623Q403 595 384 576T340 557Q322 557 310 567T297 596ZM288 376Q288 405 262 405Q240 405 220 393T185 362T161 325T144 293L137 279Q135 278 121 278H107Q101 284 101 286T105 299Q126 348 164 391T252 441Q253 441 260 441T272 442Q296 441 316 432Q341 418 354 401T367 348V332L318 133Q267 -67 264 -75Q246 -125 194 -164T75 -204Q25 -204 7 -183T-12 -137Q-12 -110 7 -91T53 -71Q70 -71 82 -81T95 -112Q95 -148 63 -167Q69 -168 77 -168Q111 -168 139 -140T182 -74L193 -32Q204 11 219 72T251 197T278 308T289 365Q289 372 288 376Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(412,0)"><path data-c="2208" d="M84 250Q84 372 166 450T360 539Q361 539 377 539T419 540T469 540H568Q583 532 583 520Q583 511 570 501L466 500Q355 499 329 494Q280 482 242 458T183 409T147 354T129 306T124 272V270H568Q583 262 583 250T568 230H124V228Q124 207 134 177T167 112T231 48T328 7Q355 1 466 0H570Q583 -10 583 -20Q583 -32 568 -40H471Q464 -40 446 -40T417 -41Q262 -41 172 45Q84 127 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1079,0)"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1967,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2356,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2701,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="msub" transform="translate(4333.3,0)"><g data-mml-node="mi"><path data-c="1D719" d="M409 688Q413 694 421 694H429H442Q448 688 448 686Q448 679 418 563Q411 535 404 504T392 458L388 442Q388 441 397 441T429 435T477 418Q521 397 550 357T579 260T548 151T471 65T374 11T279 -10H275L251 -105Q245 -128 238 -160Q230 -192 227 -198T215 -205H209Q189 -205 189 -198Q189 -193 211 -103L234 -11Q234 -10 226 -10Q221 -10 206 -8T161 6T107 36T62 89T43 171Q43 231 76 284T157 370T254 422T342 441Q347 441 348 445L378 567Q409 686 409 688ZM122 150Q122 116 134 91T167 53T203 35T237 27H244L337 404Q333 404 326 403T297 395T255 379T211 350T170 304Q152 276 137 237Q122 191 122 150ZM500 282Q500 320 484 347T444 385T405 400T381 404H378L332 217L284 29Q284 27 285 27Q293 27 317 33T357 47Q400 66 431 100T475 170T494 234T500 282Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(629,-150) scale(0.707)"><path data-c="1D457" d="M297 596Q297 627 318 644T361 661Q378 661 389 651T403 623Q403 595 384 576T340 557Q322 557 310 567T297 596ZM288 376Q288 405 262 405Q240 405 220 393T185 362T161 325T144 293L137 279Q135 278 121 278H107Q101 284 101 286T105 299Q126 348 164 391T252 441Q253 441 260 441T272 442Q296 441 316 432Q341 418 354 401T367 348V332L318 133Q267 -67 264 -75Q246 -125 194 -164T75 -204Q25 -204 7 -183T-12 -137Q-12 -110 7 -91T53 -71Q70 -71 82 -81T95 -112Q95 -148 63 -167Q69 -168 77 -168Q111 -168 139 -140T182 -74L193 -32Q204 11 219 72T251 197T278 308T289 365Q289 372 288 376Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(5525.8,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(6526.1,0)"><g data-mml-node="mi"><path data-c="1D719" d="M409 688Q413 694 421 694H429H442Q448 688 448 686Q448 679 418 563Q411 535 404 504T392 458L388 442Q388 441 397 441T429 435T477 418Q521 397 550 357T579 260T548 151T471 65T374 11T279 -10H275L251 -105Q245 -128 238 -160Q230 -192 227 -198T215 -205H209Q189 -205 189 -198Q189 -193 211 -103L234 -11Q234 -10 226 -10Q221 -10 206 -8T161 6T107 36T62 89T43 171Q43 231 76 284T157 370T254 422T342 441Q347 441 348 445L378 567Q409 686 409 688ZM122 150Q122 116 134 91T167 53T203 35T237 27H244L337 404Q333 404 326 403T297 395T255 379T211 350T170 304Q152 276 137 237Q122 191 122 150ZM500 282Q500 320 484 347T444 385T405 400T381 404H378L332 217L284 29Q284 27 285 27Q293 27 317 33T357 47Q400 66 431 100T475 170T494 234T500 282Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(629,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(7449,0)"><path data-c="239E" d="M31 1143Q31 1154 49 1154H59Q72 1154 75 1152T89 1136Q190 1013 269 873T401 585T489 294T544 -8T572 -299T583 -583Q583 -634 583 -643T577 -654Q575 -655 508 -655Q465 -655 463 -654Q459 -653 458 -647T457 -609Q457 -58 371 340T100 1037Q87 1059 61 1098T31 1143Z" transform="translate(0,770.1)" style="stroke-width: 3;"/><path data-c="23A0" d="M56 -644H50Q31 -644 31 -635Q31 -632 37 -622Q69 -579 100 -527Q286 -228 371 170T457 1119Q457 1161 462 1164Q464 1165 520 1165Q575 1165 577 1164Q582 1162 582 1153T583 1093Q581 910 570 752T527 400T442 40T300 -303T89 -626Q78 -640 75 -642T61 -644H56Z" transform="translate(0,-780.1)" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><msub><mi>ϕ</mi><mi>i</mi></msub><mo stretchy="false">←</mo><msub><mi>ϕ</mi><mi>i</mi></msub><mo>+</mo><mi>λ</mi><mrow data-mjx-texclass="INNER"><mo data-mjx-texclass="OPEN">(</mo><mfrac><mn>1</mn><mn>6</mn></mfrac><munder><mo data-mjx-texclass="OP">∑</mo><mrow data-mjx-texclass="ORD"><mi>j</mi><mo>∈</mo><mi>N</mi><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></munder><msub><mi>ϕ</mi><mi>j</mi></msub><mo>−</mo><msub><mi>ϕ</mi><mi>i</mi></msub><mo data-mjx-texclass="CLOSE">)</mo></mrow></math></mjx-assistive-mml></mjx-container><p>The term in parentheses is a discrete Laplacian, and <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="8.862ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 3917.2 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D706" d="M166 673Q166 685 183 694H202Q292 691 316 644Q322 629 373 486T474 207T524 67Q531 47 537 34T546 15T551 6T555 2T556 -2T550 -11H482Q457 3 450 18T399 152L354 277L340 262Q327 246 293 207T236 141Q211 112 174 69Q123 9 111 -1T83 -12Q47 -12 47 20Q47 37 61 52T199 187Q229 216 266 252T321 306L338 322Q338 323 288 462T234 612Q214 657 183 657Q166 657 166 673Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(860.8,0)"><path data-c="2208" d="M84 250Q84 372 166 450T360 539Q361 539 377 539T419 540T469 540H568Q583 532 583 520Q583 511 570 501L466 500Q355 499 329 494Q280 482 242 458T183 409T147 354T129 306T124 272V270H568Q583 262 583 250T568 230H124V228Q124 207 134 177T167 112T231 48T328 7Q355 1 466 0H570Q583 -10 583 -20Q583 -32 568 -40H471Q464 -40 446 -40T417 -41Q262 -41 172 45Q84 127 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1805.6,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2194.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2694.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3139.2,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3639.2,0)"><path data-c="5D" d="M22 710V750H159V-250H22V-210H119V710H22Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>λ</mi><mo>∈</mo><mo stretchy="false">(</mo><mn>0</mn><mo>,</mo><mn>1</mn><mo stretchy="false">]</mo></math></mjx-assistive-mml></mjx-container> is the smoothing strength. This averages nearby geometry, softening the surface shape while preserving the distance field gradient instead of collapsing it.</p>
<h2 id="everything-at-once" tabindex="-1">Everything at once <a class="header-anchor" href="#everything-at-once" aria-label="Permalink to &quot;Everything at once&quot;"></a></h2>
<p>Spike 27 was the integration gate. Take Spike 24's full pipeline (heightmap patches, MC chunks, Transvoxel seams, geomorph LOD) and combine it with the sampled data model from Spike 25 and both brush types.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/27-hm-mc-brush/" title="Spike 27 HM + MC + Brush Integration" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/27-hm-mc-brush/" target="_blank">Open Spike 27 in a new tab ↗</a> · <a href="/blog/spike-source?spike=27-hm-mc-brush">View source</a></p>
<p>The first thing I did was rip <code>height_at()</code> out of every shader. All three compute shaders (SDF fill, heightmap patch, Transvoxel seam) now bind the same 129x129 heightmap GPU buffer and use the same <code>hm_sample()</code> bilinear interpolation function through a shared WGSL preamble. One data source, multiple consumers. The procedural noise functions that had lived in every shader since Spike 1 were gone.</p>
<p>Then the interesting problems started.</p>
<p>When an SDF brush locks a chunk into MC mode, the Transvoxel seam between that chunk and its heightmap neighbor needs to sample from the SDF volume, not the heightmap. I extended the seam shader with additional storage buffer bindings and per-chunk MC flags. Four boundary combinations to handle: HM-HM, HM-MC, MC-HM, MC-MC.</p>
<p>LOD was another puzzle. In earlier spikes, switching an MC chunk to a lower LOD meant re-filling the SDF at a coarser resolution. I replaced that with stride-based sampling: SDF data stays at full resolution (65 grid points). The MC shader computes a stride from the ratio of grid size to cell count. At LOD0, stride is 1. At LOD1, stride is 2, sampling every other voxel. Chunks can change LOD freely without touching their SDF data.</p>
<p>The most satisfying fix was dynamic vertical chunk spawning. Sculpt upward past the top of a chunk, and a new MC-only chunk appears above it with its SDF initialized from the boundary face of the chunk below. Sculpt downward, same thing. The world grows to fit the edits.</p>
<p>The last gotcha was the heightmap brush silently doing nothing to MC-locked chunks. The HM brush modifies <code>heightmapCPU</code> and reuploads it. MC chunks don't read from the heightmap anymore because their SDF was filled from it and then diverged. I added <code>syncHeightmapToSdf()</code>: after the heightmap changes, re-derive the SDF columns for any MC chunks in the brush radius and upload the new values. Both brush types now work on both chunk types.</p>
<h2 id="what-we-actually-learned" tabindex="-1">What we actually learned <a class="header-anchor" href="#what-we-actually-learned" aria-label="Permalink to &quot;What we actually learned&quot;"></a></h2>
<p>The brush spikes were supposed to answer a performance question: can sculpting run within frame budget? It can. That was the easy part.</p>
<p>The hard part was discovering that 24 spikes of using <code>height_at()</code> as the terrain truth had created an invisible dependency that broke the moment we tried to edit anything. The procedural function was clean and global and stateless, right up until it wasn't the terrain anymore.</p>
<p>Rules we wrote down and won't forget:</p>
<ol>
<li>Terrain height comes from sampled data. Chunks own their buffers.</li>
<li>Procedural generation fills initial data. It is not the runtime truth.</li>
<li>The brush modifies chunk data directly. No displacement overlays.</li>
<li>Normals come from the same data via grid-aligned central differences.</li>
<li>Border overlap (1 cell from neighbor interior) handles cross-chunk normals.</li>
<li>Never regenerate a lookup table when a proven copy exists.</li>
</ol>
<p>In <a href="/blog/2026-04-13-open-world-browser-part-14-world-comes-alive.html">part 14</a> we stop sculpting debug geometry and start making it look and feel like an actual place.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Sampled heightmap architecture.</strong> Terrain stored as owned data per chunk rather than evaluated from a procedural function at runtime. Each chunk holds a <code>Float32Array</code> of actual height values. Procedural noise fills initial data at creation time, then the function is never called again. This eliminates the dual-system mismatch between math-based terrain and edit overlays, simplifies brush operations (edit stored values directly), and makes the GPU shader trivially simple (read from buffer, compute normals via grid-aligned central differences). For open world with streaming, per-chunk ownership with 1-cell border overlap from neighbors is the standard approach. See our <a href="/guides/landscape-generation-browser.html#terrain-data-representation">landscape generation guide</a>.</p>
<p><strong>SDF brush operations.</strong> Modifying a signed distance field to sculpt terrain. Add (inflate) uses smooth-step falloff around a sphere. Subtract (carve) uses the same shape negated. Smooth uses Laplacian averaging: read the 6 direct neighbors, compute their mean, pull toward the mean. The naive approach of pulling toward zero collapses the distance field and creates sharp edges. Laplacian smoothing preserves the field gradient while softening features. See <a href="/guides/landscape-generation-browser.html#signed-distance-fields-sdfs">SDF terrain representation</a>.</p>
<p><strong>Transvoxel with mixed data sources.</strong> Transition cells at the boundary between an MC chunk and a heightmap chunk need to sample different data on each side. The seam shader carries per-chunk flags and buffer bindings to handle all four combinations (HM-HM, HM-MC, MC-HM, MC-MC). When one side is MC-locked, the shader trilinearly interpolates the SDF buffer instead of sampling the heightmap.</p>
<p><strong>Stride-based LOD for marching cubes.</strong> SDF data stored at full resolution regardless of the chunk's current LOD level. The MC shader computes a sampling stride from the ratio of SDF grid points to MC cells. At full resolution the stride is 1, at half resolution the stride is 2. This decouples SDF data from LOD changes, so chunks can transition LOD freely without rebuilding the SDF.</p>
<hr>
<p>Part 13 of 14.
Previous: <a href="/blog/2026-02-25-open-world-browser-part-12-lessons.html">Part 12 - Rings, sky fog, and what we would do again</a>
Next: <a href="/blog/2026-04-13-open-world-browser-part-14-world-comes-alive.html">Part 14 - The world comes alive</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 14: The world comes alive]]></title>
            <link>https://app.cinevva.com/blog/2026-04-13-open-world-browser-part-14-world-comes-alive</link>
            <guid>https://app.cinevva.com/blog/2026-04-13-open-world-browser-part-14-world-comes-alive</guid>
            <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Materials that react to your sculpting, grass that grows on cave ceilings, and a glider that makes you forget you're in a browser tab.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-14-the-world-comes-alive" tabindex="-1">Building an open world in the browser, part 14: The world comes alive <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-14-the-world-comes-alive" aria-label="Permalink to &quot;Building an open world in the browser, part 14: The world comes alive&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>After <a href="/blog/2026-04-13-open-world-browser-part-13-terrain-sculpting.html">part 13</a> we had sculpting. Raise terrain, carve caves, smooth cliffs, all in real time, seams intact. But the world looked like a tech demo. Debug-colored flat shading, wireframe overlays, grey geometry with LOD colors to tell chunks apart. You could edit it. You couldn't feel it.</p>
<p>Three spikes changed that. Not the architecture. The exact same pipeline, buffers, and seam stitching from Part 13. We just added the layers that make terrain feel like a place: surfaces that respond to the shape, life growing on those surfaces, and a body that walks on them.</p>
<p>The difference between &quot;technically working&quot; and &quot;I want to stay here&quot; turned out to be surprisingly small.</p>
<h2 id="sculpt-a-cliff-and-watch-it-turn-to-rock" tabindex="-1">Sculpt a cliff and watch it turn to rock <a class="header-anchor" href="#sculpt-a-cliff-and-watch-it-turn-to-rock" aria-label="Permalink to &quot;Sculpt a cliff and watch it turn to rock&quot;"></a></h2>
<p>Spike 28 asked a narrow question: can a 4-layer material with triplanar mapping run on compute-generated terrain without killing frame budget? The answer was yes, but the interesting part was what happened next.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/28-multi-material/" title="Spike 28 Multi-Material Texturing" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/28-multi-material/" target="_blank">Open Spike 28 in a new tab ↗</a> · <a href="/blog/spike-source?spike=28-multi-material">View source</a></p>
<p>Four procedural textures (grass, rock, sand, snow) generated from FBM noise at startup. No external files, no asset pipeline, just math and a <code>DataTexture</code>. The material weights come from the surface itself: slope and altitude. Flat areas below the treeline get grass. Steep faces get rock. Low ground gets sand. High peaks get snow. Each raw weight comes from a <code>smoothstep</code> on slope and altitude, then they're normalized per-fragment so <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.663ex;" xmlns="http://www.w3.org/2000/svg" width="10.014ex" height="2.36ex" role="img" focusable="false" viewBox="0 -750 4426.1 1043.1" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="munder"><g data-mml-node="mo"><path data-c="2211" d="M61 748Q64 750 489 750H913L954 640Q965 609 976 579T993 533T999 516H979L959 517Q936 579 886 621T777 682Q724 700 655 705T436 710H319Q183 710 183 709Q186 706 348 484T511 259Q517 250 513 244L490 216Q466 188 420 134T330 27L149 -187Q149 -188 362 -188Q388 -188 436 -188T506 -189Q679 -189 778 -162T936 -43Q946 -27 959 6H999L913 -249L489 -250Q65 -250 62 -248Q56 -246 56 -239Q56 -234 118 -161Q186 -81 245 -11L428 206Q428 207 242 462L57 717L56 728Q56 744 61 748Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1089,-285.4) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="msub" transform="translate(1549.6,0)"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(749,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2870.3,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3926.1,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><munder><mo data-mjx-texclass="OP">∑</mo><mi>i</mi></munder><msub><mi>w</mi><mi>i</mi></msub><mo>=</mo><mn>1</mn></math></mjx-assistive-mml></mjx-container> and the final color is just the weighted blend <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.663ex;" xmlns="http://www.w3.org/2000/svg" width="7.963ex" height="2.36ex" role="img" focusable="false" viewBox="0 -750 3519.5 1043.1" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="munder"><g data-mml-node="mo"><path data-c="2211" d="M61 748Q64 750 489 750H913L954 640Q965 609 976 579T993 533T999 516H979L959 517Q936 579 886 621T777 682Q724 700 655 705T436 710H319Q183 710 183 709Q186 706 348 484T511 259Q517 250 513 244L490 216Q466 188 420 134T330 27L149 -187Q149 -188 362 -188Q388 -188 436 -188T506 -189Q679 -189 778 -162T936 -43Q946 -27 959 6H999L913 -249L489 -250Q65 -250 62 -248Q56 -246 56 -239Q56 -234 118 -161Q186 -81 245 -11L428 206Q428 207 242 462L57 717L56 728Q56 744 61 748Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1089,-285.4) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="msub" transform="translate(1549.6,0)"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(749,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mstyle" transform="translate(2592.6,0)"><g data-mml-node="mspace"/></g><g data-mml-node="msub" transform="translate(2759.6,0)"><g data-mml-node="mi"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(466,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><munder><mo data-mjx-texclass="OP">∑</mo><mi>i</mi></munder><msub><mi>w</mi><mi>i</mi></msub><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><msub><mi>c</mi><mi>i</mi></msub></math></mjx-assistive-mml></mjx-container>. Normalizing is what keeps the surface from going dark or blown-out where biomes overlap. Triplanar mapping handles UV projection for MC chunks where triangles have no meaningful UV coordinates.</p>
<p>The moment that sold it: raise the terrain with the brush to create a steep cliff, and rock texture appears on the new face in the same frame. Flatten it back down, and grass reclaims the surface. The material doesn't know about the brush. It just reads world position and surface normal, the same data the geometry was built from. The sculpting-to-visual feedback loop is instant and unscripted.</p>
<p>Spike 8 tested terrain material cost a month and a half ago with a static mesh on WebGL. Spike 28 proves it works on dynamic compute-generated terrain with the full sculpting pipeline underneath. We'd budgeted for this, but seeing it hold at 60fps with everything running was still a relief.</p>
<h2 id="_80-000-grass-clumps-and-a-cave-ceiling" tabindex="-1">80,000 grass clumps and a cave ceiling <a class="header-anchor" href="#_80-000-grass-clumps-and-a-cave-ceiling" aria-label="Permalink to &quot;80,000 grass clumps and a cave ceiling&quot;"></a></h2>
<p>Spike 29 is where the gamedev instincts took over.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/29-vegetation/" title="Spike 29 Vegetation / Shader Grass" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/29-vegetation/" target="_blank">Open Spike 29 in a new tab ↗</a> · <a href="/blog/spike-source?spike=29-vegetation">View source</a></p>
<p>We wanted Breath of the Wild grass. Not the poly count, the feel. Grass that covers the right places, moves with the wind, and makes you want to run through it.</p>
<p>Each grass clump is three intersecting quads at 60-degree angles with four vertical segments for bending. It's a cross shape that looks volumetric from any angle without billboard tricks. One <code>InstancedMesh</code> per chunk, 80,000 clumps across the world, roughly 2.4 million grass vertices total.</p>
<p>Placement is the part that matters. The CPU walks a jittered grid across each chunk and evaluates the same slope/altitude logic the GPU material shader uses. Where the material system says &quot;grass,&quot; grass grows. Where rock or sand dominates, density drops to zero. The falloff is smooth because the underlying <code>smoothstep</code> weights produce continuous gradients at biome edges. You don't notice a boundary because there isn't one.</p>
<p>Then we did something I wasn't sure would work. Grass on SDF surfaces. The scatter function walks each column of the SDF volume and finds zero-crossings between adjacent voxels. Where the SDF crosses from negative to positive, there's a surface. The surface normal is just the normalized gradient of the field, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -1.238ex;" xmlns="http://www.w3.org/2000/svg" width="9.256ex" height="3.445ex" role="img" focusable="false" viewBox="0 -975.7 4091.1 1522.8" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(300,16) translate(-250 0)"><path data-c="5E" d="M112 560L249 694L257 686Q387 562 387 560L361 531Q359 532 303 581L250 627L195 580Q182 569 169 557T148 538L140 532Q138 530 125 546L112 560Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(877.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(1933.6,0)"><g data-mml-node="mrow" transform="translate(573.6,485) scale(0.707)"><g data-mml-node="mi"><path data-c="2207" d="M46 676Q46 679 51 683H781Q786 679 786 676Q786 674 617 326T444 -26Q439 -33 416 -33T388 -26Q385 -22 216 326T46 676ZM697 596Q697 597 445 597T193 596Q195 591 319 336T445 80L697 596Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(833,0)"><path data-c="1D719" d="M409 688Q413 694 421 694H429H442Q448 688 448 686Q448 679 418 563Q411 535 404 504T392 458L388 442Q388 441 397 441T429 435T477 418Q521 397 550 357T579 260T548 151T471 65T374 11T279 -10H275L251 -105Q245 -128 238 -160Q230 -192 227 -198T215 -205H209Q189 -205 189 -198Q189 -193 211 -103L234 -11Q234 -10 226 -10Q221 -10 206 -8T161 6T107 36T62 89T43 171Q43 231 76 284T157 370T254 422T342 441Q347 441 348 445L378 567Q409 686 409 688ZM122 150Q122 116 134 91T167 53T203 35T237 27H244L337 404Q333 404 326 403T297 395T255 379T211 350T170 304Q152 276 137 237Q122 191 122 150ZM500 282Q500 320 484 347T444 385T405 400T381 404H378L332 217L284 29Q284 27 285 27Q293 27 317 33T357 47Q400 66 431 100T475 170T494 234T500 282Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mrow" transform="translate(220,-370.3) scale(0.707)"><g data-mml-node="mo"><path data-c="2016" d="M133 736Q138 750 153 750Q164 750 170 739Q172 735 172 250T170 -239Q164 -250 152 -250Q144 -250 138 -244L137 -243Q133 -241 133 -179T132 250Q132 731 133 736ZM329 739Q334 750 346 750Q353 750 361 744L362 743Q366 741 366 679T367 250T367 -178T362 -243L361 -244Q355 -250 347 -250Q335 -250 329 -239Q327 -235 327 250T329 739Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(500,0)"><path data-c="2207" d="M46 676Q46 679 51 683H781Q786 679 786 676Q786 674 617 326T444 -26Q439 -33 416 -33T388 -26Q385 -22 216 326T46 676ZM697 596Q697 597 445 597T193 596Q195 591 319 336T445 80L697 596Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1333,0)"><path data-c="1D719" d="M409 688Q413 694 421 694H429H442Q448 688 448 686Q448 679 418 563Q411 535 404 504T392 458L388 442Q388 441 397 441T429 435T477 418Q521 397 550 357T579 260T548 151T471 65T374 11T279 -10H275L251 -105Q245 -128 238 -160Q230 -192 227 -198T215 -205H209Q189 -205 189 -198Q189 -193 211 -103L234 -11Q234 -10 226 -10Q221 -10 206 -8T161 6T107 36T62 89T43 171Q43 231 76 284T157 370T254 422T342 441Q347 441 348 445L378 567Q409 686 409 688ZM122 150Q122 116 134 91T167 53T203 35T237 27H244L337 404Q333 404 326 403T297 395T255 379T211 350T170 304Q152 276 137 237Q122 191 122 150ZM500 282Q500 320 484 347T444 385T405 400T381 404H378L332 217L284 29Q284 27 285 27Q293 27 317 33T357 47Q400 66 431 100T475 170T494 234T500 282Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1929,0)"><path data-c="2016" d="M133 736Q138 750 153 750Q164 750 170 739Q172 735 172 250T170 -239Q164 -250 152 -250Q144 -250 138 -244L137 -243Q133 -241 133 -179T132 250Q132 731 133 736ZM329 739Q334 750 346 750Q353 750 361 744L362 743Q366 741 366 679T367 250T367 -178T362 -243L361 -244Q355 -250 347 -250Q335 -250 329 -239Q327 -235 327 250T329 739Z" style="stroke-width: 3;"/></g></g><rect width="1917.6" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow data-mjx-texclass="ORD"><mover><mi>n</mi><mo stretchy="false">^</mo></mover></mrow><mo>=</mo><mfrac><mrow><mi mathvariant="normal">∇</mi><mi>ϕ</mi></mrow><mrow><mo data-mjx-texclass="OPEN" fence="false" stretchy="false">‖</mo><mi mathvariant="normal">∇</mi><mi>ϕ</mi><mo data-mjx-texclass="CLOSE" fence="false" stretchy="false">‖</mo></mrow></mfrac></math></mjx-assistive-mml></mjx-container>, computed from voxel differences, and the slope is <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="11.911ex" height="2.398ex" role="img" focusable="false" viewBox="0 -810 5264.4 1060" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" transform="translate(892,0)" style="stroke-width: 3;"/><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" transform="translate(1336,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(1780,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(2280,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2674,0)"><path data-c="2061" d="" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2674,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(3063,0)"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(300,16) translate(-250 0)"><path data-c="5E" d="M112 560L249 694L257 686Q387 562 387 560L361 531Q359 532 303 581L250 627L195 580Q182 569 169 557T148 538L140 532Q138 530 125 546L112 560Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(3885.2,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(4385.4,0)"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(300.6,16) translate(-250 0)"><path data-c="5E" d="M112 560L249 694L257 686Q387 562 387 560L361 531Q359 532 303 581L250 627L195 580Q182 569 169 557T148 538L140 532Q138 530 125 546L112 560Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(4875.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>arccos</mi><mo data-mjx-texclass="NONE">⁡</mo><mo stretchy="false">(</mo><mrow data-mjx-texclass="ORD"><mover><mi>n</mi><mo stretchy="false">^</mo></mover></mrow><mo>⋅</mo><mrow data-mjx-texclass="ORD"><mover><mi>y</mi><mo stretchy="false">^</mo></mover></mrow><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>, the angle from vertical. If that's gentle enough, place grass.</p>
<p>Carve a cave with the SDF brush in Spike 27. Come back in Spike 29 and there's grass growing on top of the cave ceiling. The scatter code doesn't know what a cave is. It just sees a surface with the right slope at the right altitude. That's the kind of emergent behavior that makes open-world systems satisfying to build.</p>
<p>Wind is a sine wave in the TSL vertex shader, modulated by the blade's V coordinate so tips sway and roots stay planted. Toggle V to cycle between gentle, strong, and off. The wind runs on all 80K clumps simultaneously with zero CPU cost because it's entirely in the vertex shader.</p>
<p>Spike 7 tested 50K grass instances and we were nervous about hitting the limit. Spike 29 runs 80K on top of compute terrain, seam stitching, multi-material texturing, and brushes. Batching into fewer <code>InstancedMesh</code> draws still matters more than reducing per-blade vertex count. The lesson from Spike 7 held up.</p>
<p>One thing we punted: grass doesn't update when you sculpt under it. The instance matrices are set at scatter time. Sculpt a hill into a valley and the grass floats in midair until you toggle G to re-scatter. Good enough for a spike. Production will need dirty-chunk re-scatter.</p>
<h2 id="the-first-footstep" tabindex="-1">The first footstep <a class="header-anchor" href="#the-first-footstep" aria-label="Permalink to &quot;The first footstep&quot;"></a></h2>
<p>Spike 30 is the one I keep going back to.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/30-physics/" title="Spike 30 Terrain Physics" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/30-physics/" target="_blank">Open Spike 30 in a new tab ↗</a> · <a href="/blog/spike-source?spike=30-physics">View source</a></p>
<p>No physics library. Custom capsule at 120Hz fixed timestep. The production codebase uses Rapier in a web worker (Spike 2 proved the latency is fine), but this spike needed to prove the collision queries themselves. Can a character walk across heightmap terrain, step onto sculpted SDF terrain, and not fall through?</p>
<p>The core is a function called <code>terrainQuery(x, y, z)</code>. It checks whether the position falls inside an MC-locked chunk. If so, it trilinearly interpolates the CPU SDF mirror and returns the gradient as a surface normal. Otherwise, heightmap lookup with central-difference normal. The character doesn't know which terrain system it's standing on. It just asks for the ground and gets an answer.</p>
<p>SDF collision was the part I expected to be hard. Seven probe points around the capsule (bottom, center, top, four cardinal offsets). A probe at position <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.373ex" height="1.005ex" role="img" focusable="false" viewBox="0 -444 607 444" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D431" d="M227 0Q212 3 121 3Q40 3 28 0H21V62H117L245 213L109 382H26V444H34Q49 441 143 441Q247 441 265 444H274V382H246L281 339Q315 297 316 297Q320 297 354 341L389 382H352V444H360Q375 441 466 441Q547 441 559 444H566V382H471L355 246L504 63L545 62H586V0H578Q563 3 469 3Q365 3 347 0H338V62H366Q366 63 326 112T285 163L198 63L217 62H235V0H227Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">x</mi></mrow></math></mjx-assistive-mml></mjx-container> is penetrating whenever <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="8.519ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 3765.6 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D719" d="M409 688Q413 694 421 694H429H442Q448 688 448 686Q448 679 418 563Q411 535 404 504T392 458L388 442Q388 441 397 441T429 435T477 418Q521 397 550 357T579 260T548 151T471 65T374 11T279 -10H275L251 -105Q245 -128 238 -160Q230 -192 227 -198T215 -205H209Q189 -205 189 -198Q189 -193 211 -103L234 -11Q234 -10 226 -10Q221 -10 206 -8T161 6T107 36T62 89T43 171Q43 231 76 284T157 370T254 422T342 441Q347 441 348 445L378 567Q409 686 409 688ZM122 150Q122 116 134 91T167 53T203 35T237 27H244L337 404Q333 404 326 403T297 395T255 379T211 350T170 304Q152 276 137 237Q122 191 122 150ZM500 282Q500 320 484 347T444 385T405 400T381 404H378L332 217L284 29Q284 27 285 27Q293 27 317 33T357 47Q400 66 431 100T475 170T494 234T500 282Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(596,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(985,0)"><g data-mml-node="mi"><path data-c="1D431" d="M227 0Q212 3 121 3Q40 3 28 0H21V62H117L245 213L109 382H26V444H34Q49 441 143 441Q247 441 265 444H274V382H246L281 339Q315 297 316 297Q320 297 354 341L389 382H352V444H360Q375 441 466 441Q547 441 559 444H566V382H471L355 246L504 63L545 62H586V0H578Q563 3 469 3Q365 3 347 0H338V62H366Q366 63 326 112T285 163L198 63L217 62H235V0H227Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1592,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2258.8,0)"><path data-c="3C" d="M694 -11T694 -19T688 -33T678 -40Q671 -40 524 29T234 166L90 235Q83 240 83 250Q83 261 91 266Q664 540 678 540Q681 540 687 534T694 519T687 505Q686 504 417 376L151 250L417 124Q686 -4 687 -5Q694 -11 694 -19Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3314.6,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>ϕ</mi><mo stretchy="false">(</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">x</mi></mrow><mo stretchy="false">)</mo><mo>&lt;</mo><mi>r</mi></math></mjx-assistive-mml></mjx-container>, with penetration depth <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="12.423ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 5491 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(780.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1836.6,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2509.8,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3510,0)"><path data-c="1D719" d="M409 688Q413 694 421 694H429H442Q448 688 448 686Q448 679 418 563Q411 535 404 504T392 458L388 442Q388 441 397 441T429 435T477 418Q521 397 550 357T579 260T548 151T471 65T374 11T279 -10H275L251 -105Q245 -128 238 -160Q230 -192 227 -198T215 -205H209Q189 -205 189 -198Q189 -193 211 -103L234 -11Q234 -10 226 -10Q221 -10 206 -8T161 6T107 36T62 89T43 171Q43 231 76 284T157 370T254 422T342 441Q347 441 348 445L378 567Q409 686 409 688ZM122 150Q122 116 134 91T167 53T203 35T237 27H244L337 404Q333 404 326 403T297 395T255 379T211 350T170 304Q152 276 137 237Q122 191 122 150ZM500 282Q500 320 484 347T444 385T405 400T381 404H378L332 217L284 29Q284 27 285 27Q293 27 317 33T357 47Q400 66 431 100T475 170T494 234T500 282Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4106,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(4495,0)"><g data-mml-node="mi"><path data-c="1D431" d="M227 0Q212 3 121 3Q40 3 28 0H21V62H117L245 213L109 382H26V444H34Q49 441 143 441Q247 441 265 444H274V382H246L281 339Q315 297 316 297Q320 297 354 341L389 382H352V444H360Q375 441 466 441Q547 441 559 444H566V382H471L355 246L504 63L545 62H586V0H578Q563 3 469 3Q365 3 347 0H338V62H366Q366 63 326 112T285 163L198 63L217 62H235V0H227Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(5102,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>p</mi><mo>=</mo><mi>r</mi><mo>−</mo><mi>ϕ</mi><mo stretchy="false">(</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">x</mi></mrow><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>, and the push-out direction is the field gradient <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -1.238ex;" xmlns="http://www.w3.org/2000/svg" width="9.256ex" height="3.445ex" role="img" focusable="false" viewBox="0 -975.7 4091.1 1522.8" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(300,16) translate(-250 0)"><path data-c="5E" d="M112 560L249 694L257 686Q387 562 387 560L361 531Q359 532 303 581L250 627L195 580Q182 569 169 557T148 538L140 532Q138 530 125 546L112 560Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(877.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(1933.6,0)"><g data-mml-node="mrow" transform="translate(573.6,485) scale(0.707)"><g data-mml-node="mi"><path data-c="2207" d="M46 676Q46 679 51 683H781Q786 679 786 676Q786 674 617 326T444 -26Q439 -33 416 -33T388 -26Q385 -22 216 326T46 676ZM697 596Q697 597 445 597T193 596Q195 591 319 336T445 80L697 596Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(833,0)"><path data-c="1D719" d="M409 688Q413 694 421 694H429H442Q448 688 448 686Q448 679 418 563Q411 535 404 504T392 458L388 442Q388 441 397 441T429 435T477 418Q521 397 550 357T579 260T548 151T471 65T374 11T279 -10H275L251 -105Q245 -128 238 -160Q230 -192 227 -198T215 -205H209Q189 -205 189 -198Q189 -193 211 -103L234 -11Q234 -10 226 -10Q221 -10 206 -8T161 6T107 36T62 89T43 171Q43 231 76 284T157 370T254 422T342 441Q347 441 348 445L378 567Q409 686 409 688ZM122 150Q122 116 134 91T167 53T203 35T237 27H244L337 404Q333 404 326 403T297 395T255 379T211 350T170 304Q152 276 137 237Q122 191 122 150ZM500 282Q500 320 484 347T444 385T405 400T381 404H378L332 217L284 29Q284 27 285 27Q293 27 317 33T357 47Q400 66 431 100T475 170T494 234T500 282Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mrow" transform="translate(220,-370.3) scale(0.707)"><g data-mml-node="mo"><path data-c="2016" d="M133 736Q138 750 153 750Q164 750 170 739Q172 735 172 250T170 -239Q164 -250 152 -250Q144 -250 138 -244L137 -243Q133 -241 133 -179T132 250Q132 731 133 736ZM329 739Q334 750 346 750Q353 750 361 744L362 743Q366 741 366 679T367 250T367 -178T362 -243L361 -244Q355 -250 347 -250Q335 -250 329 -239Q327 -235 327 250T329 739Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(500,0)"><path data-c="2207" d="M46 676Q46 679 51 683H781Q786 679 786 676Q786 674 617 326T444 -26Q439 -33 416 -33T388 -26Q385 -22 216 326T46 676ZM697 596Q697 597 445 597T193 596Q195 591 319 336T445 80L697 596Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1333,0)"><path data-c="1D719" d="M409 688Q413 694 421 694H429H442Q448 688 448 686Q448 679 418 563Q411 535 404 504T392 458L388 442Q388 441 397 441T429 435T477 418Q521 397 550 357T579 260T548 151T471 65T374 11T279 -10H275L251 -105Q245 -128 238 -160Q230 -192 227 -198T215 -205H209Q189 -205 189 -198Q189 -193 211 -103L234 -11Q234 -10 226 -10Q221 -10 206 -8T161 6T107 36T62 89T43 171Q43 231 76 284T157 370T254 422T342 441Q347 441 348 445L378 567Q409 686 409 688ZM122 150Q122 116 134 91T167 53T203 35T237 27H244L337 404Q333 404 326 403T297 395T255 379T211 350T170 304Q152 276 137 237Q122 191 122 150ZM500 282Q500 320 484 347T444 385T405 400T381 404H378L332 217L284 29Q284 27 285 27Q293 27 317 33T357 47Q400 66 431 100T475 170T494 234T500 282Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1929,0)"><path data-c="2016" d="M133 736Q138 750 153 750Q164 750 170 739Q172 735 172 250T170 -239Q164 -250 152 -250Q144 -250 138 -244L137 -243Q133 -241 133 -179T132 250Q132 731 133 736ZM329 739Q334 750 346 750Q353 750 361 744L362 743Q366 741 366 679T367 250T367 -178T362 -243L361 -244Q355 -250 347 -250Q335 -250 329 -239Q327 -235 327 250T329 739Z" style="stroke-width: 3;"/></g></g><rect width="1917.6" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow data-mjx-texclass="ORD"><mover><mi>n</mi><mo stretchy="false">^</mo></mover></mrow><mo>=</mo><mfrac><mrow><mi mathvariant="normal">∇</mi><mi>ϕ</mi></mrow><mrow><mo data-mjx-texclass="OPEN" fence="false" stretchy="false">‖</mo><mi mathvariant="normal">∇</mi><mi>ϕ</mi><mo data-mjx-texclass="CLOSE" fence="false" stretchy="false">‖</mo></mrow></mfrac></math></mjx-assistive-mml></mjx-container>. To stop the capsule re-entering the surface, velocity is projected to cancel its inward component:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="17.018ex" height="2.398ex" role="img" focusable="false" viewBox="0 -810 7521.9 1060" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D42F" d="M401 444Q413 441 495 441Q568 441 574 444H580V382H510L409 156Q348 18 339 6Q331 -4 320 -4Q318 -4 313 -4T303 -3H288Q273 -3 264 12T221 102Q206 135 197 156L96 382H26V444H34Q49 441 145 441Q252 441 270 444H279V382H231L284 264Q335 149 338 149Q338 150 389 264T442 381Q442 382 418 382H394V444H401Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(640,413) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1162.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(2218,0)"><g data-mml-node="mi"><path data-c="1D42F" d="M401 444Q413 441 495 441Q568 441 574 444H580V382H510L409 156Q348 18 339 6Q331 -4 320 -4Q318 -4 313 -4T303 -3H288Q273 -3 264 12T221 102Q206 135 197 156L96 382H26V444H34Q49 441 145 441Q252 441 270 444H279V382H231L284 264Q335 149 338 149Q338 150 389 264T442 381Q442 382 418 382H394V444H401Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3047.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4047.5,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(4436.5,0)"><g data-mml-node="mi"><path data-c="1D42F" d="M401 444Q413 441 495 441Q568 441 574 444H580V382H510L409 156Q348 18 339 6Q331 -4 320 -4Q318 -4 313 -4T303 -3H288Q273 -3 264 12T221 102Q206 135 197 156L96 382H26V444H34Q49 441 145 441Q252 441 270 444H279V382H231L284 264Q335 149 338 149Q338 150 389 264T442 381Q442 382 418 382H394V444H401Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(5265.7,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(5765.9,0)"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(300,16) translate(-250 0)"><path data-c="5E" d="M112 560L249 694L257 686Q387 562 387 560L361 531Q359 532 303 581L250 627L195 580Q182 569 169 557T148 538L140 532Q138 530 125 546L112 560Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(6365.9,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(6754.9,0)"><g data-mml-node="mspace"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(6921.9,0)"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(300,16) translate(-250 0)"><path data-c="5E" d="M112 560L249 694L257 686Q387 562 387 560L361 531Q359 532 303 581L250 627L195 580Q182 569 169 557T148 538L140 532Q138 530 125 546L112 560Z" style="stroke-width: 3;"/></g></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><msup><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">v</mi></mrow><mo data-mjx-alternate="1">′</mo></msup><mo>=</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">v</mi></mrow><mo>−</mo><mo stretchy="false">(</mo><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">v</mi></mrow><mo>⋅</mo><mrow data-mjx-texclass="ORD"><mover><mi>n</mi><mo stretchy="false">^</mo></mover></mrow><mo stretchy="false">)</mo><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mrow data-mjx-texclass="ORD"><mover><mi>n</mi><mo stretchy="false">^</mo></mover></mrow></math></mjx-assistive-mml></mjx-container><p>which leaves only the component sliding along the surface. It's not a physics engine. It's geometry queries and simple response. But it handles caves, overhangs, and sculpted tunnels with zero special-case code per shape. You walk into a cave you carved ten seconds ago and the capsule follows the ceiling contour exactly.</p>
<p>The movement model started practical and got fun. Walk, run, sprint, jump, slope sliding above 45 degrees. Then I added a BotW paraglider and the spike turned into something I didn't want to close.</p>
<p>Press Space while airborne. Gravity drops from -30 to -4. Fall speed caps at -3. A delta-wing mesh unfolds from the capsule with scale interpolation. Bank left and the wing tilts. The capsule's facing auto-aligns toward the velocity vector so you're always looking where you're going. Let go of Space to drop. Hit the ground and you're running again.</p>
<p>The camera pulls back into third-person (toggle P). It follows behind the player with yaw smoothing. A ray-march from player to camera tests terrain at 20 steps. Fly into a cave and the camera arm shortens smoothly instead of clipping through rock. Emerge on the other side and it extends back out.</p>
<p>Here's the moment that made the spike worth it: sculpt a tall cliff with the heightmap brush. Switch to the third-person camera. Run to the edge. Jump. Deploy the glider. Bank left over the terrain you just sculpted, with the grass you grew in Spike 29 waving below, the rock texture from Spike 28 on the cliff face, Transvoxel seams holding at every chunk boundary. Land on the other side. Everything working together in one browser tab.</p>
<h2 id="sculpting-under-your-own-feet" tabindex="-1">Sculpting under your own feet <a class="header-anchor" href="#sculpting-under-your-own-feet" aria-label="Permalink to &quot;Sculpting under your own feet&quot;"></a></h2>
<p>One thing I wasn't sure about: what happens when you sculpt the terrain the character is standing on? The heightmap changes propagate through <code>terrainQuery()</code> instantly because it reads the CPU buffer directly. SDF changes propagate through the CPU SDF mirror. The capsule resolves penetration on the next physics tick, which at 120Hz is within <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.816ex;" xmlns="http://www.w3.org/2000/svg" width="9.304ex" height="2.773ex" role="img" focusable="false" viewBox="0 -864.9 4112.2 1225.5" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mfrac"><g data-mml-node="mn" transform="translate(573.6,394) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(220,-345) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g><rect width="1260.7" height="60" x="120" y="220"/></g><g data-mml-node="mo" transform="translate(1778.4,0)"><path data-c="2248" d="M55 319Q55 360 72 393T114 444T163 472T205 482Q207 482 213 482T223 483Q262 483 296 468T393 413L443 381Q502 346 553 346Q609 346 649 375T694 454Q694 465 698 474T708 483Q722 483 722 452Q722 386 675 338T555 289Q514 289 468 310T388 357T308 404T224 426Q164 426 125 393T83 318Q81 289 69 289Q55 289 55 319ZM55 85Q55 126 72 159T114 210T163 238T205 248Q207 248 213 248T223 249Q262 249 296 234T393 179L443 147Q502 112 553 112Q609 112 649 141T694 220Q694 249 708 249T722 217Q722 153 675 104T555 55Q514 55 468 76T388 123T308 170T224 192Q164 192 125 159T83 84Q80 55 69 55Q55 55 55 85Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2834.2,0)"><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z" style="stroke-width: 3;"/><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(778,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mfrac><mn>1</mn><mn>120</mn></mfrac><mo>≈</mo><mn>8.3</mn></math></mjx-assistive-mml></mjx-container> ms.</p>
<p>It just works. Raise the ground under the player and they rise with it. Carve the ground away and they fall. No special handling. The physics runs fast enough that single-frame terrain changes never produce large penetrations. This was a happy accident of choosing 120Hz for the timestep. We picked it for smooth movement, and it made terrain editing safe for free.</p>
<h2 id="_30-spikes-later" tabindex="-1">30 spikes later <a class="header-anchor" href="#_30-spikes-later" aria-label="Permalink to &quot;30 spikes later&quot;"></a></h2>
<p>We started this series with a flat heightmap and 500 cubes. Now we have sculpted terrain with volumetric caves, Transvoxel seam stitching, multi-material texturing that responds to the surface shape, 80,000 grass clumps swaying in the wind, and a character that walks, sprints, jumps, and glides across all of it.</p>
<p>None of this is in production yet. The <code>world/client/</code> codebase still runs WebGL with simple heightmap chunks. Everything from these 30 spikes lives in standalone HTML pages. The integration work is next: migrating to <code>WebGPURenderer</code>, wiring the hybrid HM/MC policy into the chunk manager, connecting the brush and material systems to multiplayer.</p>
<p>But the rendering system isn't the risk anymore. The open questions are about data flow: edit persistence, network sync of brush strokes, collaborative sculpting. The stuff that happens between players, not between triangles.</p>
<p>If you've followed this series from Part 1, thanks for sticking with the messy parts. If you just found this, go back to <a href="/blog/2026-02-25-open-world-browser-part-01-risk-first.html">Part 1</a>. The wrong turns are where the lessons live.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>TSL (Three Shading Language).</strong> Three.js's node-based shader system for the WebGPU renderer. Materials are composed from nodes (<code>positionWorld</code>, <code>normalWorld</code>, <code>smoothstep</code>, <code>triplanarTexture</code>) using function composition in JavaScript. The shader graph compiles to WGSL at runtime. TSL replaces raw GLSL <code>ShaderMaterial</code> for WebGPU targets and provides interop between standard Three.js material features (lights, shadows, fog) and custom per-fragment logic.</p>
<p><strong>Triplanar mapping.</strong> A texture projection technique that samples a texture three times (XY, XZ, YZ planes) and blends based on surface normal direction. This eliminates UV stretching on arbitrary mesh geometry, which is critical for marching cubes output where triangles have no meaningful UV coordinates. TSL provides <code>triplanarTexture()</code> as a built-in node.</p>
<p><strong>Instanced vegetation with cross-quad blades.</strong> Each grass clump is 3 intersecting quads at 60-degree angles, creating a volumetric appearance from any view direction. 4 vertical segments per quad allow smooth bending for wind animation. The entire field is rendered as a single <code>InstancedMesh</code> per chunk. Capacity is over-allocated by 25% so new instances can fill reserved slots when terrain is edited without reallocating the GPU buffer. See our <a href="/guides/landscape-generation-browser.html#vegetation-and-foliage">landscape generation guide on vegetation</a>.</p>
<p><strong>Capsule vs SDF collision.</strong> Character collision against volumetric terrain without a physics engine. The capsule is probed at multiple points against the SDF. Where the field value is less than the capsule radius, the gradient provides the outward normal and the difference gives the penetration depth. This handles caves, overhangs, and tunnels with no shape-specific code. See <a href="/guides/landscape-generation-browser.html#physics-on-sdf-terrain">SDF terrain collisions</a>.</p>
<p><strong>Fixed-timestep character controller.</strong> Physics steps at 120Hz regardless of frame rate, accumulating real time and consuming it in fixed-size steps. A max substep count prevents spiral-of-death on slow frames. Ground snapping keeps the capsule attached during slope traversal. The fixed timestep ensures deterministic behavior for future multiplayer replay.</p>
<hr>
<p>Part 14 of 29.
Previous: <a href="/blog/2026-04-13-open-world-browser-part-13-terrain-sculpting.html">Part 13 - Terrain sculpting and the death of the math function</a>
Next: <a href="/blog/2026-04-13-open-world-browser-part-15-multiplayer-and-water.html">Part 15 - Replace the baseline, then sync it</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 15: Replace the baseline, then sync it]]></title>
            <link>https://app.cinevva.com/blog/2026-04-13-open-world-browser-part-15-multiplayer-and-water</link>
            <guid>https://app.cinevva.com/blog/2026-04-13-open-world-browser-part-15-multiplayer-and-water</guid>
            <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spikes 31-32 made the choice to replace the legacy world with the best spike, then proved brush-param replay multiplayer and added an ocean with swimming.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-15-replace-the-baseline-then-sync-it" tabindex="-1">Building an open world in the browser, part 15: Replace the baseline, then sync it <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-15-replace-the-baseline-then-sync-it" aria-label="Permalink to &quot;Building an open world in the browser, part 15: Replace the baseline, then sync it&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>The first fourteen parts covered spikes 1 through 30. That run ended with a terrain system that could be sculpted in real time, plus a character who could walk, glide, and fall on it. This part picks the series back up at spike 31, and the first thing we had to decide wasn't technical. It was what to do with all that spike code.</p>
<h2 id="the-replace-don-t-backport-decision" tabindex="-1">The &quot;replace, don't backport&quot; decision <a class="header-anchor" href="#the-replace-don-t-backport-decision" aria-label="Permalink to &quot;The &quot;replace, don't backport&quot; decision&quot;"></a></h2>
<p>We had 30 standalone HTML files, each proving one isolated concept, and zero integration. The production <code>world/client/</code> was still the old stack: WebGL, a simple heightmap, a 75-line character controller, a MessagePack protocol with nine message types. No editing, no WebGPU, no materials, no vegetation.</p>
<p>The obvious plan was to backport spike results into that production codebase one at a time. We threw it out. Spike 30 already had better terrain, physics, materials, vegetation, and camera than <code>world/client/</code> ever did. Backporting into the old WebGL code would mean fighting it the whole way. So we made the call to replace the world implementation with the most successful spike and build forward. Spike 30 became the new baseline, and <code>world/client/</code> became dead code.</p>
<p>That reframed the remaining work. To get from &quot;great single-player tech demo&quot; to &quot;product&quot; we needed multiplayer, persistence, infinite-world streaming, and object placement. Multiplayer terrain sync went first, because it's the one that forces architecture. The question it answers is simple to ask and expensive to get wrong: when Player A sculpts, what actually goes over the wire?</p>
<h2 id="brush-param-replay-not-pixel-sync" tabindex="-1">Brush-param replay, not pixel sync <a class="header-anchor" href="#brush-param-replay-not-pixel-sync" aria-label="Permalink to &quot;Brush-param replay, not pixel sync&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/31-multiplayer-sync/" title="Spike 31 Multiplayer Terrain Sync" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/31-multiplayer-sync/" target="_blank">Open Spike 31 in a new tab ↗</a> · <a href="/blog/spike-source?spike=31-multiplayer-sync">View source</a></p>
<p>Before writing a line of network code, we traced exactly what a brush stroke does. The heightmap brush walks a radius around the cursor in a CPU <code>Float32Array</code>, applies a smoothstep falloff, and either adds, subtracts, smooths, or flattens. The SDF brush does the same thing in 3D over a sphere of voxels. Both paths are pure CPU array math. No GPU compute in the loop, no randomness, no nondeterministic floating point. Same input array plus same params equals same output, on every machine.</p>
<p>That's the whole trick. We don't send the edited terrain. We send the brush parameters, 56 bytes per stroke tick, and every client replays the same deterministic function. The sync protocol is four message types: peer discovery, the brush message <code>{op, wx, wy, wz, radius, strength, flattenTarget}</code>, and a player-position message at 20Hz.</p>
<p>For the spike we skipped the server entirely and used <code>BroadcastChannel</code>, the browser API for same-origin cross-tab messaging. Open two tabs, they talk, zero infrastructure. That isolates the sync question from latency, auth, and Durable Object wiring. If param replay converges across tabs, it'll converge across a WebSocket too.</p>
<p>The one place replay can diverge is order-dependent operations. Raise and lower are commutative, so <code>val + strength * falloff</code> lands the same regardless of who applied first. Smooth and flatten read neighbor values, so two clients smoothing the exact same spot in the same instant can drift by fractions of a millimeter per tick. In practice that never fires, and the production fix is already obvious: route edits through the DO, let it assign a monotonic sequence number, apply optimistically on the client, and correct order if the authoritative sequence disagrees. Classic optimistic concurrency, and the DO is a natural serialization point anyway.</p>
<h2 id="the-peer-capsule-that-kept-vanishing" tabindex="-1">The peer capsule that kept vanishing <a class="header-anchor" href="#the-peer-capsule-that-kept-vanishing" aria-label="Permalink to &quot;The peer capsule that kept vanishing&quot;"></a></h2>
<p>Edits synced on the first try. The remote player's capsule did not. It flickered in and out of existence on the other tab, and it took three separate bugs to make it stay solid.</p>
<p>The capsule spawned at the world origin, which is buried under the terrain, because the <code>join</code> message arrives before any position data. Fix: start it hidden and reveal it on the first position update. The position broadcast lived inside the render loop, and Chrome throttles <code>requestAnimationFrame</code> on unfocused tabs, so the other tab's staleness check would reap the peer and the next message would recreate it. Fix: move the broadcast to a <code>setInterval</code>, which isn't throttled for visible tabs. And the staleness timeout was a too-aggressive 5 seconds, tripped by any GC pause. Fix: raise it to 30 seconds and rely on the clean <code>leave</code> message for normal closes.</p>
<h2 id="persistence-and-late-join-same-format" tabindex="-1">Persistence and late-join, same format <a class="header-anchor" href="#persistence-and-late-join-same-format" aria-label="Permalink to &quot;Persistence and late-join, same format&quot;"></a></h2>
<p>We folded persistence into the same spike instead of spinning up a new one, because the serialization format is identical whether the destination is IndexedDB or another tab. A snapshot is the full heightmap (a 129×129 <code>Float32Array</code>, about 66 KB) plus only the edited SDF chunks (each <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="3.25ex" height="2.003ex" role="img" focusable="false" viewBox="0 -863.3 1436.6 885.3" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" style="stroke-width: 3;"/><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1033,393.1) scale(0.707)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mn>65</mn><mn>3</mn></msup></math></mjx-assistive-mml></mjx-container>, about 1.1 MB) plus the list of chunk IDs locked into marching-cubes mode. A debounced save writes to IndexedDB two seconds after the last edit. On load, procedural terrain generates synchronously and then the saved state overwrites it before the first meaningful frame. Late-join reuses the exact same bytes: when a new tab joins, an existing tab with edits serializes its state and sends it targeted at the newcomer, and the mountain you sculpted five minutes ago appears on their screen.</p>
<p>The first persistence test surfaced a nice ordering bug. Grass is scattered synchronously at init using the procedural heights, but the IndexedDB restore is async and overwrites the heightmap afterward, leaving every blade floating or sunk. The fix is a <code>refreshAllGrass()</code> pass that resamples the height under each instance and hides any blade now on a bad slope or altitude. The same function serves both load and late-join.</p>
<h2 id="the-slope-saga" tabindex="-1">The slope saga <a class="header-anchor" href="#the-slope-saga" aria-label="Permalink to &quot;The slope saga&quot;"></a></h2>
<p>Sculpted terrain is rougher than the smooth procedural baseline, and it exposed three physics bugs the old terrain never could. Walking straight uphill made the capsule slide sideways. The cause was a velocity projection meant to keep movement tangent to the ground, but written with only the horizontal components of the normal. On a diagonal slope with normal <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="15.967ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 7057.3 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(389,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1167,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(778,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2445,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2889.7,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="39" d="M352 287Q304 211 232 211Q154 211 104 270T44 396Q42 412 42 436V444Q42 537 111 606Q171 666 243 666Q245 666 249 666T257 665H261Q273 665 286 663T323 651T370 619T413 560Q456 472 456 334Q456 194 396 97Q361 41 312 10T208 -22Q147 -22 108 7T68 93T121 149Q143 149 158 135T173 96Q173 78 164 65T148 49T135 44L131 43Q131 41 138 37T164 27T206 22H212Q272 22 313 86Q352 142 352 280V287ZM244 248Q292 248 321 297T351 430Q351 508 343 542Q341 552 337 562T323 588T293 615T246 625Q208 625 181 598Q160 576 154 546T147 441Q147 358 152 329T172 282Q197 248 244 248Z" transform="translate(778,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4167.7,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4612.3,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(5390.3,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(778,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6668.3,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mo>−</mo><mn>0.3</mn><mo>,</mo><mn>0.9</mn><mo>,</mo><mo>−</mo><mn>0.3</mn><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>, walking north injected sideways velocity out of nowhere. The ground snap already keeps the player on the surface, so we just deleted the projection.</p>
<p>The drift persisted from a second source. The SDF collision probes push the body out along the gradient by the penetration depth. On any slope the gradient has horizontal components, so a 0.1 m penetration on a 15° slope pushes about 0.026 m sideways per step, and at 120Hz that's roughly 3 m/s of invisible drift. Fix: split the response by slope. On walkable ground (<mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.874ex;" xmlns="http://www.w3.org/2000/svg" width="5.379ex" height="2.444ex" role="img" focusable="false" viewBox="0 -694 2377.5 1080.3" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mtext"><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(892,0)" style="stroke-width: 3;"/><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" transform="translate(1392,0)" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1981,-241.4) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mtext>grad</mtext><mi>y</mi></msub></math></mjx-assistive-mml></mjx-container> above the walk threshold) the push is vertical only. On walls and cliffs the full 3D push stays, because that's exactly where you want to be deflected. We also raised the walkable limit from <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="6.798ex" height="1.667ex" role="img" focusable="false" viewBox="0 -715 3004.7 737" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(444,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(944,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1338,0)"><path data-c="2061" d="" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1504.7,0)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" style="stroke-width: 3;"/><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(2504.7,0)"><g data-mml-node="mo"><path data-c="B0" d="M147 628Q147 669 179 692T244 715Q298 715 325 689T352 629Q352 592 323 567T249 542Q202 542 175 567T147 628ZM313 628Q313 660 300 669T259 678H253Q248 678 242 678T234 679Q217 679 207 674T192 659T188 644T187 629Q187 600 198 590Q210 579 250 579H265Q279 579 288 581T305 595T313 628Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>cos</mi><mo data-mjx-texclass="NONE">⁡</mo><mn>45</mn><mrow data-mjx-texclass="ORD"><mo data-mjx-pseudoscript="true">°</mo></mrow></math></mjx-assistive-mml></mjx-container> to <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="6.798ex" height="1.667ex" role="img" focusable="false" viewBox="0 -715 3004.7 737" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(444,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(944,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1338,0)"><path data-c="2061" d="" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1504.7,0)"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(2504.7,0)"><g data-mml-node="mo"><path data-c="B0" d="M147 628Q147 669 179 692T244 715Q298 715 325 689T352 629Q352 592 323 567T249 542Q202 542 175 567T147 628ZM313 628Q313 660 300 669T259 678H253Q248 678 242 678T234 679Q217 679 207 674T192 659T188 644T187 629Q187 600 198 590Q210 579 250 579H265Q279 579 288 581T305 595T313 628Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>cos</mi><mo data-mjx-texclass="NONE">⁡</mo><mn>60</mn><mrow data-mjx-texclass="ORD"><mo data-mjx-pseudoscript="true">°</mo></mrow></math></mjx-assistive-mml></mjx-container> so brush-made hills feel climbable the way they do in Breath of the Wild.</p>
<p>The third bug froze the capsule at chunk boundaries, because collision probes sampled a single chunk's SDF and got the &quot;deep in air&quot; sentinel when a probe crossed into the neighbor. The fix was <code>sdfSampleWorld(wx, wy, wz)</code> and <code>sdfGradientWorld(...)</code>, which find the right chunk for any world position and fall back to a heightmap distance estimate where no SDF exists. The SDF-to-heightmap collision transition is continuous now.</p>
<h2 id="water-completes-the-world" tabindex="-1">Water completes the world <a class="header-anchor" href="#water-completes-the-world" aria-label="Permalink to &quot;Water completes the world&quot;"></a></h2>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/32-water-swimming/" title="Spike 32 Water and Swimming" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/32-water-swimming/" target="_blank">Open Spike 32 in a new tab ↗</a> · <a href="/blog/spike-source?spike=32-water-swimming">View source</a></p>
<p>Every spike up to here was &quot;land above water.&quot; Spike 32 added an ocean, and with it a new movement verb. We set the water level at 22 in a terrain that ranges roughly 8 to 58, which floods the low valleys, leaves beaches at the shoreline, and keeps plenty of dry land for play.</p>
<p>The surface is a <code>MeshStandardNodeMaterial</code> built in TSL, the same node approach as the terrain. Three overlapping sine waves at different frequencies displace the vertices, and the surface normal comes from the analytic cosine derivatives of those waves rather than from mesh normals. Color blends from turquoise shallows to dark teal depths using a depth estimate <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="24.614ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 10879.3 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(389,0)"><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(278,0)" style="stroke-width: 3;"/><path data-c="76" d="M338 431Q344 429 422 429Q479 429 503 431H508V385H497Q439 381 423 345Q421 341 356 172T288 -2Q283 -11 263 -11Q244 -11 239 -2Q99 359 98 364Q93 378 82 381T43 385H19V431H25L33 430Q41 430 53 430T79 430T104 429T122 428Q217 428 232 431H240V385H226Q187 384 184 370Q184 366 235 234L286 102L377 341V349Q377 363 367 372T349 383T335 385H331V431H338Z" transform="translate(722,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1250,0)" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(1694,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2583.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3583.4,0)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4295.7,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(5295.9,0)"><path data-c="77" d="M90 368Q84 378 76 380T40 385H18V431H24L43 430Q62 430 84 429T116 428Q206 428 221 431H229V385H215Q177 383 177 368Q177 367 221 239L265 113L339 328L333 345Q323 374 316 379Q308 384 278 385H258V431H264Q270 428 348 428Q439 428 454 431H461V385H452Q404 385 404 369Q404 366 418 324T449 234T481 143L496 100L537 219Q579 341 579 347Q579 363 564 373T530 385H522V431H529Q541 428 624 428Q692 428 698 431H703V385H697Q696 385 691 385T682 384Q635 377 619 334L559 161Q546 124 528 71Q508 12 503 1T487 -11H479Q460 -11 456 -4Q455 -3 407 133L361 267Q359 263 266 -4Q261 -11 243 -11H238Q225 -11 220 -3L90 368Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(722,0)" style="stroke-width: 3;"/><path data-c="76" d="M338 431Q344 429 422 429Q479 429 503 431H508V385H497Q439 381 423 345Q421 341 356 172T288 -2Q283 -11 263 -11Q244 -11 239 -2Q99 359 98 364Q93 378 82 381T43 385H19V431H25L33 430Q41 430 53 430T79 430T104 429T122 428Q217 428 232 431H240V385H226Q187 384 184 370Q184 366 235 234L286 102L377 341V349Q377 363 367 372T349 383T335 385H331V431H338Z" transform="translate(1222,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1750,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(7489.9,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(8101.1,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(9101.3,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" transform="translate(778,0)" style="stroke-width: 3;"/><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(1278,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mtext>level</mtext><mo>−</mo><mi>y</mi><mo>+</mo><mtext>wave</mtext><mo stretchy="false">)</mo><mo>×</mo><mn>0.12</mn></math></mjx-assistive-mml></mjx-container> clamped to <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="4.526ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 2000.7 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(278,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(778,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1222.7,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1722.7,0)"><path data-c="5D" d="M22 710V750H159V-250H22V-210H119V710H22Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">[</mo><mn>0</mn><mo>,</mo><mn>1</mn><mo stretchy="false">]</mo></math></mjx-assistive-mml></mjx-container>, foam rises where that estimate approaches zero at the shoreline, and alpha follows depth so shallow water reads as transparent and deep water as nearly opaque.</p>
<p>Swimming is a buoyancy spring. The player enters swim mode when the feet drop below the water level and the body center is within a capsule half-height of the surface. A spring pulls the body toward a target just under the surface, and with a buoyancy constant of 12 against a water damping of 4 the player bobs stably with their head out, no oscillation. Swim speed is slower than walking with floaty acceleration and drag, jumping near the surface launches you out at 60% of normal jump velocity, and entry caps downward velocity at -5 m/s so you don't plunge. Terrain collision still runs underwater, so you can walk the lake bed where it rises above the swim target. The swimming flag rides along in the position broadcast so peers see you swim, and an HTML gradient overlay tints the view when the camera dips below the surface.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Deterministic brush-param replay.</strong> Instead of streaming edited terrain, each client sends only the brush parameters and replays the same CPU function. This works because both the heightmap and SDF brushes are pure <code>Float32Array</code> math with no randomness or GPU nondeterminism, so identical inputs produce bit-identical outputs everywhere. Payload is 56 bytes per stroke tick. Commutative operations (raise, lower) converge regardless of order, while neighbor-reading operations (smooth, flatten) need a serialization point to guarantee convergence, which the production Durable Object provides via monotonic sequence numbers.</p>
<p><strong>BroadcastChannel as a WebSocket stand-in.</strong> A browser API for same-origin cross-tab messaging with zero server. Used here to test the sync protocol in isolation from network latency and authentication. The serialization format (raw <code>Float32Array</code> heightmap plus edited SDF chunks plus MC-locked chunk IDs) is the same bytes used for IndexedDB persistence and late-join state transfer, so one format covers three jobs.</p>
<p><strong>Slope-split SDF collision response.</strong> When a capsule probe penetrates volumetric terrain, the naive fix pushes the body out along the SDF gradient by the penetration depth. On slopes that gradient has horizontal components, injecting lateral drift. Splitting the response so walkable surfaces (<mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.874ex;" xmlns="http://www.w3.org/2000/svg" width="5.379ex" height="2.444ex" role="img" focusable="false" viewBox="0 -694 2377.5 1080.3" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mtext"><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(892,0)" style="stroke-width: 3;"/><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" transform="translate(1392,0)" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1981,-241.4) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mtext>grad</mtext><mi>y</mi></msub></math></mjx-assistive-mml></mjx-container> above threshold) get a vertical-only push, while steep surfaces keep the full 3D push plus velocity projection, removes the drift without losing wall collision. See <a href="/guides/landscape-generation-browser.html#physics-on-sdf-terrain">SDF terrain collisions</a>.</p>
<p><strong>TSL water with analytic wave normals.</strong> The ocean is a node material whose vertices are displaced by three summed sine waves. Rather than recomputing mesh normals after displacement, the surface normal is derived analytically from the cosine derivatives of the wave functions, which is cheaper and avoids the artifacts of finite-difference normals on a coarse grid. Depth-driven color, shoreline foam, and depth-driven transparency all key off a single depth estimate.</p>
<p><strong>Buoyancy-spring swimming.</strong> Swim physics models the body as a damped spring pulled toward a target just below the surface. With buoyancy constant 12 and damping 4, the player settles at the surface without oscillating. Distinct movement constants (slower speed, floaty acceleration, heavy drag) give swimming a different feel from walking, and the existing capsule-vs-terrain collision keeps working underwater.</p>
<hr>
<p>Part 15 of 29.
Previous: <a href="/blog/2026-04-13-open-world-browser-part-14-world-comes-alive.html">Part 14 - The world comes alive</a>
Next: <a href="/blog/2026-04-28-open-world-browser-part-16-structure-and-authoring.html">Part 16 - Structure for a world that keeps growing</a>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Cubcoats × Cinevva: Game Concepts One-Pager]]></title>
            <link>https://app.cinevva.com/blog/2026-03-19-cubcoats-game-concepts-one-pager</link>
            <guid>https://app.cinevva.com/blog/2026-03-19-cubcoats-game-concepts-one-pager</guid>
            <pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Three educational game concepts (Golden Tree, Kali's Word Island, Island Stories) based on the sketch-to-video-to-3D pipeline and Kali the Kitty. For licensing or partner pitch.]]></description>
            <content:encoded><![CDATA[<h1 id="cubcoats-×-cinevva-game-concepts-one-pager" tabindex="-1">Cubcoats × Cinevva: Game Concepts One-Pager <a class="header-anchor" href="#cubcoats-×-cinevva-game-concepts-one-pager" aria-label="Permalink to &quot;Cubcoats × Cinevva: Game Concepts One-Pager&quot;"></a></h1>
<p><em>Based on the sketch-to-video-to-3D pipeline and Kali the Kitty (Cubcoats). For licensing, blog, or partner pitch.</em></p>
<p><strong>To share as PDF:</strong> Open this page, then use your browser's <strong>File → Print → Save as PDF</strong>.</p>
<hr>
<h2 id="golden-tree-—-full-one-pager" tabindex="-1">Golden Tree — Full one-pager <a class="header-anchor" href="#golden-tree-—-full-one-pager" aria-label="Permalink to &quot;Golden Tree — Full one-pager&quot;"></a></h2>
<p><strong>Tagline:</strong> <em>A world that grows with you. Visit, water, watch. No levels. No pressure.</em></p>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Ages</strong></td>
<td>3–13 (broad)</td>
</tr>
<tr>
<td><strong>Format</strong></td>
<td>Episodic shared 3D world + weekly short &quot;moments&quot;</td>
</tr>
<tr>
<td><strong>Platform</strong></td>
<td>Web (and mobile later); low friction, no install required</td>
</tr>
<tr>
<td><strong>Viral hook</strong></td>
<td>&quot;Send your friend a seed.&quot; Shared growth, shareable calm clips.</td>
</tr>
</tbody>
</table>
<h3 id="what-it-is" tabindex="-1">What it is <a class="header-anchor" href="#what-it-is" aria-label="Permalink to &quot;What it is&quot;"></a></h3>
<p>A calm, collectible world that evolves in <strong>real-world time</strong>. One shared &quot;season&quot; (e.g. Spring Island): a single 3D island kids can walk around (like the playable Kali demo). There are no traditional levels or scores. Progression is <strong>your tree got a new branch</strong>, <strong>you've seen 12 moments</strong>, <strong>someone watered your seed</strong>.</p>
<p>Each visit a child can: <strong>water one plant</strong>, <strong>leave one wish</strong> (text or emoji), <strong>find one hidden Cubcoats character</strong>, or <strong>watch one 30-second &quot;moment&quot;</strong> (e.g. Kali under the golden tree). New moments and small story beats drop <strong>weekly</strong> (episodic). Optional ambient language: &quot;Today Kali said: [one line in EN and ZH].&quot;</p>
<p>Learning and growth are <strong>subliminal</strong>: themes (kindness, patience, trying again) live in the writing and art, not in lessons. The experience feels like a toy or a shared place, not school.</p>
<h3 id="core-loop" tabindex="-1">Core loop <a class="header-anchor" href="#core-loop" aria-label="Permalink to &quot;Core loop&quot;"></a></h3>
<ol>
<li><strong>Visit</strong> the island (browser or app).</li>
<li><strong>Do one gentle action</strong> per visit: water, wish, find a character, or watch a moment.</li>
<li><strong>See the world change</strong> over days/weeks (new flowers, new character poses, new short clips).</li>
<li><strong>Share</strong> a seed to a friend or &quot;visit a friend's island&quot; so growth feels social.</li>
</ol>
<h3 id="how-it-uses-the-article-s-pipeline-and-characters" tabindex="-1">How it uses the article's pipeline and characters <a class="header-anchor" href="#how-it-uses-the-article-s-pipeline-and-characters" aria-label="Permalink to &quot;How it uses the article's pipeline and characters&quot;"></a></h3>
<ul>
<li><strong>3D playable world:</strong> Same pipeline as the Kali demo: T-pose → 3D Model Generator → rigging → Cinevva Engine. The island and characters are explorable, third-person, low stakes.</li>
<li><strong>Golden tree:</strong> Directly from the article (Kali's &quot;fear to hope&quot; beat and the magical golden tree). The tree is the emotional and visual anchor of the season.</li>
<li><strong>Short &quot;moments&quot;:</strong> Produced like the article's vertical short: start frame + end frame + motion prompt → Video Generator (Kling 3.0 Pro), 9:16, ~15 s. Each weekly drop is one small story beat.</li>
<li><strong>Cubcoats IP:</strong> Kali and the other seven characters, the island world, and the warm, rounded art style. Fits a licensing-first relaunch (2026) with a digital, episodic product.</li>
</ul>
<h3 id="why-it-can-go-viral" tabindex="-1">Why it can go viral <a class="header-anchor" href="#why-it-can-go-viral" aria-label="Permalink to &quot;Why it can go viral&quot;"></a></h3>
<ul>
<li><strong>Low friction:</strong> One link, no heavy onboarding. &quot;Send your friend a seed&quot; is a simple share.</li>
<li><strong>Calm, beautiful clips:</strong> The short moments are standalone shareable (social, &quot;watch this 15 seconds&quot;). Meme-friendly in a positive way.</li>
<li><strong>Episodic:</strong> &quot;What's new this week?&quot; drives return visits and conversation.</li>
<li><strong>Subliminal:</strong> Parents see &quot;calm, creative, no ads, no stress&quot;; kids get routine, patience, and gentle surprise without being taught explicitly.</li>
</ul>
<h3 id="possible-cinevva-blog-angle" tabindex="-1">Possible Cinevva blog angle <a class="header-anchor" href="#possible-cinevva-blog-angle" aria-label="Permalink to &quot;Possible Cinevva blog angle&quot;"></a></h3>
<p><em>&quot;We took Kali from sketch to video to a character you can walk around. Golden Tree is the next step: a world that grows in real time, where kids visit, water, and watch short moments—no levels, no pressure. Same pipeline. New format.&quot;</em></p>
<hr>
<h2 id="kali-s-word-island-—-at-a-glance" tabindex="-1">Kali's Word Island — At a glance <a class="header-anchor" href="#kali-s-word-island-—-at-a-glance" aria-label="Permalink to &quot;Kali's Word Island — At a glance&quot;"></a></h2>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Tagline</strong></td>
<td>Help Kali and friends grow the island. Learn English and Chinese along the way.</td>
</tr>
<tr>
<td><strong>Ages</strong></td>
<td>5–11</td>
</tr>
<tr>
<td><strong>Focus</strong></td>
<td>English ↔ Chinese for kids; strong gamification and progression.</td>
</tr>
</tbody>
</table>
<p><strong>Core idea:</strong> The Cubcoats island is split into character zones (Kali's forest, another's beach). Kids help characters by doing short tasks that require understanding and producing EN/ZH. Progression = &quot;Seeds → Saplings → Trees → Golden Tree&quot; (unlock next zone when the zone's tree is full). Daily &quot;lantern&quot; run, phrase cards as collectibles, character &quot;mood&quot; bars, optional parent dashboard. Short story moments (start/end frame style) unlock when a zone is complete. <strong>Pipeline:</strong> Same 3D + video pipeline for characters and emotional beats.</p>
<hr>
<h2 id="cubcoats-island-stories-—-at-a-glance" tabindex="-1">Cubcoats: Island Stories — At a glance <a class="header-anchor" href="#cubcoats-island-stories-—-at-a-glance" aria-label="Permalink to &quot;Cubcoats: Island Stories — At a glance&quot;"></a></h2>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Tagline</strong></td>
<td>Make your own Cubcoats episode. Remix the story. Share your version.</td>
</tr>
<tr>
<td><strong>Ages</strong></td>
<td>3–13 (bands: 3–6, 7–10, 11–13)</td>
</tr>
<tr>
<td><strong>Focus</strong></td>
<td>Story creation and remix; education embedded but not the main pitch.</td>
</tr>
</tbody>
</table>
<p><strong>Core idea:</strong> Episodic narratives where kids choose branches (what does Kali say? where does she go?). The main activity is <strong>remixing</strong>: pick character, place, 2–3 beats → output a short &quot;episode&quot; (script + simple visuals, or later AI-generated short). Learning is subliminal (vocabulary and patterns in dialogue and choices). Social: share your episode, play someone else's version, rate &quot;kind / funny / surprising.&quot; <strong>Pipeline:</strong> Same character look + two-frame video pipeline for user-generated &quot;moments&quot;; 3D for character/world consistency.</p>
<hr>
<p><em>Cinevva tools referenced: Image Generator, Video Generator (Kling 3.0 Pro), 3D Model Generator, auto-rigging, Cinevva Engine. Reflects the pipeline described in &quot;From sketch to video and playable 3D&quot; (2026-03-04).</em></p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What it actually takes to build an open world in a browser]]></title>
            <link>https://app.cinevva.com/blog/2026-03-14-open-world-browser-medium-article</link>
            <guid>https://app.cinevva.com/blog/2026-03-14-open-world-browser-medium-article</guid>
            <pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A journalistic walkthrough of Cinevva's 12-part engineering series: 24 spike experiments, from ugly terrain baselines to GPU marching cubes, streaming LOD, and Transvoxel seam debugging.]]></description>
            <content:encoded><![CDATA[<h1 id="what-it-actually-takes-to-build-an-open-world-in-a-browser" tabindex="-1">What it actually takes to build an open world in a browser <a class="header-anchor" href="#what-it-actually-takes-to-build-an-open-world-in-a-browser" aria-label="Permalink to &quot;What it actually takes to build an open world in a browser&quot;"></a></h1>
<p><em>By <a href="/about.html">Mariana Muntean</a>, CEO of Cinevva</em></p>
<img src="/img/blog/open-world-browser-120fps.png" alt="The open world running in a browser at 120 FPS — terrain, trees, physics, and a player capsule, all rendered in a single tab" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>The team at Cinevva just published one of the most transparent engineering journals in recent game-dev memory: a <a href="/blog/2026-02-25-open-world-browser-series-guide.html">12-part series</a> documenting our attempt to build a multiplayer open world that runs entirely in the browser. No download. No app store. Just a URL.</p>
<p>The project spanned 24 technical experiments we call &quot;spikes&quot; -- short, focused prototypes designed to answer one risky question each. Every spike shipped with live source code you can open and run in your browser right now. The series was written by <a href="/about.html">Oleg Sidorkin</a>, CTO and co-founder of Cinevva, and it reads less like marketing and more like a field journal from the frontlines of what browsers can actually do in 2026.</p>
<p>What makes the series worth reading -- even if you never plan to build terrain systems -- is the method underneath. It's a case study in how to de-risk an ambitious project before you've committed to anything expensive.</p>
<h2 id="starting-with-the-hardest-question-first" tabindex="-1">Starting with the hardest question first <a class="header-anchor" href="#starting-with-the-hardest-question-first" aria-label="Permalink to &quot;Starting with the hardest question first&quot;"></a></h2>
<p>Most open-world projects die in a predictable sequence. First you get a beautiful concept. Then a pretty scene. Then you discover that your frame budget was already spent before gameplay existed.</p>
<p>Our team inverted the order. The <a href="/blog/2026-02-25-open-world-browser-part-01-risk-first.html">first spike</a> was deliberately ugly: a 512-meter terrain mesh, 500 instanced objects, procedural height noise, a water plane, and fog. No shadows, no beauty pass. The only question was whether a browser could hold a stable frame rate while the camera moved through it.</p>
<p>It could. And that &quot;yes&quot; established something Oleg calls a &quot;baseline contract&quot; -- a measured reference cost for a minimal scene that every subsequent feature had to justify itself against. If a new effect looked great but blew the frame budget, it didn't ship. Not yet, anyway.</p>
<p>That kind of discipline sounds obvious. In practice, it's rare in fast-moving prototype environments where everyone is excited about the next visual win.</p>
<h2 id="the-physics-gamble" tabindex="-1">The physics gamble <a class="header-anchor" href="#the-physics-gamble" aria-label="Permalink to &quot;The physics gamble&quot;"></a></h2>
<p>The <a href="/blog/2026-02-25-open-world-browser-part-02-worker-physics.html">second experiment</a> tackled an architectural debate that divides browser game developers: should physics run on the main thread, where it's simpler, or in a Web Worker, where it can't block rendering?</p>
<p>Worker-based physics is cleaner on paper. In practice, the fear is latency. Every input event has to cross a message boundary twice: once to reach the worker, once to bring back the result. If that round trip is too slow, pressing a key and seeing your character move will feel sluggish.</p>
<p>The team integrated the Rapier physics engine (compiled from Rust to WebAssembly) in a dedicated worker, wired up the message pipeline, and measured. The overhead was negligible. Controls still felt immediate. But we were careful to note that we'd validated one specific scenario, not a universal rule. When GPU pressure and streaming complexity changed later, assumptions would need rechecking.</p>
<h2 id="the-boring-spikes-that-saved-the-project" tabindex="-1">The boring spikes that saved the project <a class="header-anchor" href="#the-boring-spikes-that-saved-the-project" aria-label="Permalink to &quot;The boring spikes that saved the project&quot;"></a></h2>
<p><a href="/blog/2026-02-25-open-world-browser-part-03-the-unflashy-spikes.html">Part three</a> of the series has no screenshots. It covers three experiments that looked unglamorous but carried product-level consequences.</p>
<p>The first tested whether Cloudflare Durable Objects could handle real-time position broadcasts at game-like tick rates -- the multiplayer backbone. If this had failed, the entire network architecture would have needed early sharding rather than single-island ownership.</p>
<p>The second validated a mobile quality profile: not a desktop preset renamed, but an explicit low-cost rendering path from the same terrain baseline. The question was whether the world could remain readable and responsive under mobile GPU constraints without rewriting the renderer.</p>
<p>The third evaluated whether AI-generated behavior scripts for creator workflows would be reliable enough for production use.</p>
<p>None of these produced demo reels. All three set hard boundaries that shaped every architectural decision afterward. Oleg writes that these &quot;unflashy spikes changed architecture faster than visual spikes did.&quot;</p>
<h2 id="streaming-where-pretty-projects-fall-apart" tabindex="-1">Streaming: where pretty projects fall apart <a class="header-anchor" href="#streaming-where-pretty-projects-fall-apart" aria-label="Permalink to &quot;Streaming: where pretty projects fall apart&quot;"></a></h2>
<img src="/img/blog/open-world-chunk-streaming.png" alt="Spike 6: Chunk streaming in action — each colored area is a terrain chunk that loads and unloads dynamically as the camera moves" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>You can hide a lot in a still frame. You can't hide a 40-millisecond stutter when crossing a chunk boundary while running.</p>
<p>The team tested <a href="/blog/2026-02-25-open-world-browser-part-04-streaming-before-fancy.html">streaming before building advanced terrain</a>, deliberately separating concerns. Spike 6 validated neighborhood chunk loading with simple content. Only after that clean signal did Spike 11 introduce compressed heightmap streaming with progressive refinement -- loading terrain at 17-sample resolution first, then 33, then the full 65-sample grid.</p>
<p>The sequencing mattered more than we expected. Had we started directly with compressed height chunks, every hitch would have been ambiguous. Was it a decode issue, a texture upload stall, or a geometry update problem? Testing simple streaming first eliminated one entire category of uncertainty.</p>
<p>A practical lesson emerged: measure upload stalls directly, not through average FPS. Averages hide frame spikes, and frame spikes are what players actually feel.</p>
<h2 id="the-visual-budget-wars" tabindex="-1">The visual budget wars <a class="header-anchor" href="#the-visual-budget-wars" aria-label="Permalink to &quot;The visual budget wars&quot;"></a></h2>
<p>Three separate experiments attacked rendering costs in isolation rather than bundling them. <a href="/blog/2026-02-25-open-world-browser-part-05-budgeting-the-pretty.html">Vegetation</a> density and wind animation. Multi-layer terrain materials with triplanar mapping for cliff faces. Cascaded shadow maps under realistic terrain load.</p>
<p>The vegetation spike revealed that batching instances into fewer meshes mattered more than reducing per-blade polygon count. The material spike found that triplanar projection on vertical surfaces was worth the GPU cost, but adding a fifth texture splat layer wasn't. The shadow spike determined that three cascades at 1024 resolution delivered acceptable contact shadows without exceeding 2 milliseconds of GPU time.</p>
<p>The team adopted a blunt rule: a feature moves forward only if it can explain its cost with measured frame-time data. That constraint, set early, made later architectural decisions around volumetric terrain and clipmaps significantly cleaner.</p>
<h2 id="the-pivot-that-changed-the-project-s-trajectory" tabindex="-1">The pivot that changed the project's trajectory <a class="header-anchor" href="#the-pivot-that-changed-the-project-s-trajectory" aria-label="Permalink to &quot;The pivot that changed the project's trajectory&quot;"></a></h2>
<p>Before Spike 10, our mental model was &quot;bigger world means more geometry.&quot; After <a href="/blog/2026-02-25-open-world-browser-part-06-clipmaps.html">Spike 10</a>, it became &quot;constant geometry budget, camera-centered ring updates.&quot;</p>
<p>Geometry clipmaps -- concentric rings of terrain centered on the camera, each ring progressively coarser -- meant the triangle count stayed roughly constant regardless of draw distance. The practical trick was geomorphing at ring boundaries: smoothly blending vertex heights in the shader so that the shift between resolution levels is invisible in motion.</p>
<p>A subtle lesson came from testing methodology. Clipmaps look fine in screenshots. They reveal their artifacts only under sustained camera movement through ring boundaries. The team spent time running constant-speed traversals and watching for temporal noise. &quot;Screenshots lied,&quot; Oleg writes. &quot;Motion told the truth.&quot;</p>
<h2 id="going-underground" tabindex="-1">Going underground <a class="header-anchor" href="#going-underground" aria-label="Permalink to &quot;Going underground&quot;"></a></h2>
<p>Heightmaps can't represent caves. They store one elevation value per point on a grid. The moment you need tunnels, overhangs, or carved rock faces, you need volumetric terrain.</p>
<p><a href="/blog/2026-02-25-open-world-browser-part-07-marching-cubes.html">Spike 12</a> implemented marching cubes on the GPU using WebGPU compute shaders, extracting triangle meshes from a 3D signed distance field. Four 64-cubed chunks ran simultaneously with per-frame mesh updates from animated SDF edits. The compute shader handled everything -- evaluating the field, classifying cells, emitting vertices -- without any CPU readback.</p>
<p>The challenge wasn't making it work. It was making it work alongside everything else. Integration with Three.js's scene graph, buffer lifecycle management (WebGPU buffers can't be resized), fence handling to avoid destroying GPU resources still in flight -- the series devotes <a href="/blog/2026-02-25-open-world-browser-part-08-webgpu-integration.html">two</a> full parts to what we call &quot;incremental hardening,&quot; the unglamorous process of adding one capability at a time and verifying the previous layer still functions after each addition.</p>
<h2 id="the-seam-nightmare" tabindex="-1">The seam nightmare <a class="header-anchor" href="#the-seam-nightmare" aria-label="Permalink to &quot;The seam nightmare&quot;"></a></h2>
<p>The most technically harrowing section of the series spans <a href="/blog/2026-02-25-open-world-browser-part-09-transvoxel-first-cut.html">Parts 9 through 11</a>, covering what happens when terrain chunks at different resolutions meet.</p>
<p>When a high-detail chunk sits next to a low-detail chunk, their independently generated meshes don't align at the boundary. The result is visible cracks, flickering edges, and T-junctions where light bleeds through. The Transvoxel algorithm solves this with special transition cells that bridge resolution differences -- but implementing it correctly across all chunk configurations, with consistent winding order, proper buffer management, and accurate draw ranges, consumed six separate experiments.</p>
<p>The team's most memorable debugging story: two days chasing a seam artifact we blamed on transition logic. The actual culprit was stale data. The GPU compute shader wrote N vertices into a buffer, but the draw call was still configured to render N+M vertices from the previous frame. Those extra vertices contained garbage that produced flickering razor-thin triangles. One line fix: clip the draw range to the atomic counter's active vertex count.</p>
<p>&quot;Rendering bugs often masquerade as meshing bugs,&quot; Oleg observes. &quot;The geometry was correct the whole time.&quot;</p>
<h2 id="from-chaos-to-governance" tabindex="-1">From chaos to governance <a class="header-anchor" href="#from-chaos-to-governance" aria-label="Permalink to &quot;From chaos to governance&quot;"></a></h2>
<p>After the seam battle, the team replaced ad-hoc chunk behavior with an <a href="/blog/2026-02-25-open-world-browser-part-11-policy-modes.html">explicit policy system</a>. A central function now decided each chunk's LOD level, rendering mode (heightmap or marching cubes), and which faces needed transition cells. Distance rings determined the base LOD. An adjacency constraint ensured no two neighboring chunks differed by more than one resolution level. An edit bitmap kept volumetric chunks in marching-cubes mode regardless of distance if they contained creator modifications.</p>
<p>Color-coded debug overlays -- green for heightmap chunks, blue for marching cubes, orange for transition faces -- turned &quot;I saw a bug somewhere near that ridge&quot; into &quot;the bug appears at position (142, 12, -67) facing northwest.&quot;</p>
<p>&quot;Policy didn't reduce complexity,&quot; Oleg writes. &quot;It organized complexity.&quot;</p>
<h2 id="what-it-adds-up-to" tabindex="-1">What it adds up to <a class="header-anchor" href="#what-it-adds-up-to" aria-label="Permalink to &quot;What it adds up to&quot;"></a></h2>
<p>The final spike combined clipmap rings, per-fragment sky fog (sampling the actual skybox color in the direction of each terrain fragment), and Three.js module wiring into a unified demonstration. The result is a terrain system that layers near-field volumetric editing, mid-field heightmap chunks, and far-field clipmap rings under a policy layer that governs mode, LOD, and transitions.</p>
<p>The series closes with <a href="/blog/2026-02-25-open-world-browser-part-12-lessons.html">lessons</a> Oleg says he'd repeat on any future project:</p>
<ul>
<li><strong>Start with risk spikes before feature work.</strong> Kill the &quot;can we even do this?&quot; questions before investing in content pipelines.</li>
<li><strong>Freeze known-good baselines before integration jumps.</strong> The day spent establishing a clean checkpoint saves multiple days bisecting regressions later.</li>
<li><strong>Force policy and observability before optimization marathons.</strong> Named conditions with trigger rules beat mystery bugs every time.</li>
<li><strong>Test under motion, not screenshots.</strong> Pops, flicker, and streaming hitches all hide in still frames.</li>
<li><strong>Measure per-feature frame time, not average FPS.</strong> Averages hide the spikes that users actually feel.</li>
<li><strong>Publish the messy parts.</strong> The wrong turns, the ghost hunts, the two days blaming the wrong system. Those are the parts people can actually learn from.</li>
</ul>
<h2 id="why-this-matters-beyond-cinevva" tabindex="-1">Why this matters beyond Cinevva <a class="header-anchor" href="#why-this-matters-beyond-cinevva" aria-label="Permalink to &quot;Why this matters beyond Cinevva&quot;"></a></h2>
<p>The series is significant for three reasons that extend past one company's terrain pipeline.</p>
<p>First, it demonstrates that WebGPU compute shaders, WebAssembly physics, and edge-deployed Durable Objects have crossed a threshold. A multiplayer open world with volumetric terrain, real-time editing, and streaming LOD is architecturally viable in a browser tab in 2026. That was not true two years ago.</p>
<p>Second, the spike methodology -- small, focused experiments that each answer one risky question with live, measurable results -- offers a template for any team attempting something that might not work. The discipline of measuring before committing, of establishing baselines before integrating, of naming edge cases before optimizing, applies far beyond terrain systems.</p>
<p>Third, the radical transparency is the point. Publishing source code for all 24 experiments, including the dead ends and the two-day debugging detours, makes this more than a technical blog. It's a public engineering notebook that treats the reader as a colleague rather than a customer.</p>
<p>The full series is available in our <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>, with every spike running live in the browser.</p>
<hr>
<p><em>This article was originally published on <a href="https://vio-202020.medium.com/de-risking-an-ambitious-project-before-committing-to-anything-big-like-an-online-open-world-c94a7c05ee06" target="_blank" rel="noreferrer">Medium</a>.</em></p>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/blog/open-world-browser-120fps.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[From sketch to video and playable 3D: character, story frames, and rig]]></title>
            <link>https://app.cinevva.com/blog/2026-03-04-sketch-to-animated-3d-character</link>
            <guid>https://app.cinevva.com/blog/2026-03-04-sketch-to-animated-3d-character</guid>
            <pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How we used Cinevva's Image Generator, Video Generator, and 3D Model Generator to turn a rough character sketch into an AI-generated vertical short and a rigged 3D model you can walk around in a browser.]]></description>
            <content:encoded><![CDATA[<h1 id="from-sketch-to-video-and-playable-3d" tabindex="-1">From sketch to video and playable 3D <a class="header-anchor" href="#from-sketch-to-video-and-playable-3d" aria-label="Permalink to &quot;From sketch to video and playable 3D&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO of Cinevva</em></p>
<p>Taking a character from a flat sketch to a playable 3D game used to require a concept artist, a 3D modeler, a rigger, an animator, and someone to wire it all together in an engine. We wanted to see how much of that pipeline <a href="/tools/">Cinevva's creative tools</a> could handle on their own. Here's what we found.</p>
<p>The character is <strong>Kali the Kitty</strong> from <a href="https://thecubcoats.com" target="_blank" rel="noreferrer">Cubcoats</a>, a children's brand that sold over a million stuffed-animal hoodies through Nordstrom, Amazon, and Disney Store. Cubcoats has eight original characters with defined personalities, a fictional island world, and 14 patents. They're relaunching in 2026 as a licensing-first platform, and we wanted to show what that IP looks like when it moves beyond physical products: a vertical video short and a rigged 3D character you can walk around in a browser. Everything below is real output from that exploration.</p>
<p>Drag the 3D viewers to orbit. Scroll the film strip to see how the vertical short moves from first frame to last.</p>
<h2 id="from-flat-book-art-to-a-character-you-can-spin-around" tabindex="-1">From flat book art to a character you can spin around <a class="header-anchor" href="#from-flat-book-art-to-a-character-you-can-spin-around" aria-label="Permalink to &quot;From flat book art to a character you can spin around&quot;"></a></h2>
<p>Cubcoats already had beautiful 2D illustrations. Mimi Chao's art direction gave every character a warm, rounded, hand-drawn look that works perfectly on hoodies and in storybooks. But flat art doesn't feed a 3D pipeline. We needed a high-fidelity character that could hold up as a reference for video generation, mesh creation, and rigging, all from the same face.</p>
<p>We started by feeding the original book illustrations into our <a href="/tools/flux.html">Image Generator</a> and asking it to produce a 3D rendered version of the same character.</p>
<figure class="pipeline-media pipeline-media--single">
<img src="/blog/sketch-to-3d-pipeline/kali-flat-book-art.png" alt="Original flat Cubcoats illustrations of Kali the Kitty" loading="lazy">
<figcaption><strong>The starting point.</strong> Kali's original 2D book illustrations. Warm, expressive, completely flat.</figcaption>
</figure>
<p>The proportions held, the personality came through, and the soft 3D cartoon look gave every downstream tool something consistent to agree on. From there we iterated on the exact pose and framing until we had a locked hero reference.</p>
<div class="pipeline-media pipeline-media--two">
<figure>
<img src="/blog/sketch-to-3d-pipeline/kali-3d-chatgpt.png" alt="Locked 3D-style character reference for Kali" loading="lazy">
<figcaption><strong>Locked hero reference.</strong> Full-body 3D cartoon look used for video and as the visual anchor for the mesh.</figcaption>
</figure>
<figure>
<img src="/blog/sketch-to-3d-pipeline/kali-3d-tpose.png" alt="T-pose reference for 3D generation and rigging" loading="lazy">
<figcaption><strong>T-pose sheet.</strong> Same character, arms out. This fed both the [3D Model Generator](/tools/hunyuan3d) and the [auto-rigger](/tools/rigger).</figcaption>
</figure>
</div>
<p>The T-pose matters because rigging expects arms away from the torso. Skip it and the auto-rigger fuses the arms to the body and gives up. We generated the T-pose from the same locked character look so the silhouette and proportions stayed consistent across every step.</p>
<h2 id="write-the-story-before-you-generate-anything" tabindex="-1">Write the story before you generate anything <a class="header-anchor" href="#write-the-story-before-you-generate-anything" aria-label="Permalink to &quot;Write the story before you generate anything&quot;"></a></h2>
<p>Before touching video tools, we wrote a beat sheet in plain language. Kali's personality trait in the Cubcoats universe is &quot;Positive.&quot; She's the one who makes everyone feel included, stays upbeat when things get hard. So we built a ten-second arc around that:</p>
<blockquote>
<p>Kali walks into a dark misty forest at night holding a tiny glowing lantern. She gets scared, sits down alone, almost gives up. Then she discovers a magical golden tree lighting up behind her. Wonder, joy, golden particles raining down. Fear to hope in one breath.</p>
</blockquote>
<p>That's the whole plot. It doesn't need to be complex. It needs to fit one emotional turn into one continuous shot so the start frame and end frame can actually connect.</p>
<h2 id="two-frames-that-bookend-the-whole-thing" tabindex="-1">Two frames that bookend the whole thing <a class="header-anchor" href="#two-frames-that-bookend-the-whole-thing" aria-label="Permalink to &quot;Two frames that bookend the whole thing&quot;"></a></h2>
<p>We generated two key stills from that plot using our <a href="/tools/flux.html">Image Generator</a>. Portrait aspect to match the vertical short. The prompts describe the exact same character in two different moments.</p>
<p><strong>Start frame prompt:</strong></p>
<blockquote>
<p>3D animated Pixar-style render of Kali, a small pink kitty character with large round dark eyes, pink pointed ears, a light pink belly, and a cheerful round face. She is standing at the edge of a dark misty forest at night, holding a tiny glowing lantern in both paws. Her ears are slightly flattened and her expression is nervous but determined. Cinematic lighting with cool blue moonlight from above and warm orange glow from the lantern. Dense fog between dark tree trunks in the background. Camera angle: medium shot, slightly low looking up at her. No text, no watermark.</p>
</blockquote>
<p><strong>End frame prompt:</strong></p>
<blockquote>
<p>3D animated Pixar-style render of Kali, a small pink kitty character with large round dark eyes, pink pointed ears, and a light pink belly. She is standing in front of a massive magical tree covered in glowing golden flowers, arms outstretched wide, beaming with a huge joyful smile. Golden petals float in the air around her. The tree radiates warm golden light that illuminates the entire forest clearing. Starry night sky visible above. Camera angle: wide shot from slightly below, epic reveal composition. No text, no watermark.</p>
</blockquote>
<div class="pipeline-media pipeline-media--two">
<figure>
<img src="/blog/sketch-to-3d-pipeline/kali-start-frame.png" alt="Start frame for vertical AI video" loading="lazy">
<figcaption><strong>Start frame.</strong> Nervous Kali at the forest edge with her lantern.</figcaption>
</figure>
<figure>
<img src="/blog/sketch-to-3d-pipeline/kali-end-frame.png" alt="End frame for vertical AI video" loading="lazy">
<figcaption><strong>End frame.</strong> Joyful Kali in front of the golden tree.</figcaption>
</figure>
</div>
<p>Seedance interpolates between these two bookends. The character description stays identical across both prompts so the model knows it's the same person. Only the scene, emotion, and camera change.</p>
<h2 id="the-video-came-out-better-than-expected" tabindex="-1">The video came out better than expected <a class="header-anchor" href="#the-video-came-out-better-than-expected" aria-label="Permalink to &quot;The video came out better than expected&quot;"></a></h2>
<p>We loaded both frames into Cinevva's <a href="/tools/video.html">Video Generator</a>, picked Seedance 1.5 Pro, set 9:16 aspect and 15 seconds, typed the plot into the motion prompt, and hit Generate.</p>
<figure class="pipeline-media pipeline-media--single">
<img src="/blog/sketch-to-3d-pipeline/video-generator-setup.png" alt="Cinevva Video Generator with start and end frames loaded" loading="lazy">
<figcaption><strong>The actual setup.</strong> Start frame, end frame, motion prompt, 9:16 vertical, 15 seconds, audio on.</figcaption>
</figure>
<p>The clip interpolates between the two bookends. Native audio was generated in the same pass. Here are frames sampled along the timeline.</p>
<div class="pipeline-filmstrip" aria-label="Frames from generated vertical video">
<img src="/blog/sketch-to-3d-pipeline/seedance-frames/frame_01.jpg" alt="Video frame 1" loading="lazy">
<img src="/blog/sketch-to-3d-pipeline/seedance-frames/frame_02.jpg" alt="Video frame 2" loading="lazy">
<img src="/blog/sketch-to-3d-pipeline/seedance-frames/frame_03.jpg" alt="Video frame 3" loading="lazy">
<img src="/blog/sketch-to-3d-pipeline/seedance-frames/frame_04.jpg" alt="Video frame 4" loading="lazy">
<img src="/blog/sketch-to-3d-pipeline/seedance-frames/frame_05.jpg" alt="Video frame 5" loading="lazy">
<img src="/blog/sketch-to-3d-pipeline/seedance-frames/frame_06.jpg" alt="Video frame 6" loading="lazy">
<img src="/blog/sketch-to-3d-pipeline/seedance-frames/frame_07.jpg" alt="Video frame 7" loading="lazy">
<img src="/blog/sketch-to-3d-pipeline/seedance-frames/frame_08.jpg" alt="Video frame 8" loading="lazy">
<img src="/blog/sketch-to-3d-pipeline/seedance-frames/frame_09.jpg" alt="Video frame 9" loading="lazy">
</div>
<p class="pipeline-caption">Left to right: progression through the generated vertical short.</p>
<p>Start plus end frame was the right mode when we cared about hitting an exact closing pose. For longer emotional arcs, single reference plus prompt worked well with the <a href="/tools/video.html">Video Generator's</a> Kling 3.0 Pro storyboard mode. Different settings for different jobs.</p>
<h2 id="same-character-now-in-3d" tabindex="-1">Same character, now in 3D <a class="header-anchor" href="#same-character-now-in-3d" aria-label="Permalink to &quot;Same character, now in 3D&quot;"></a></h2>
<p>Here's where it gets interesting. The T-pose still from the same family as the video references went to our <a href="/tools/hunyuan3d.html">3D Model Generator</a> for image-to-3D generation. You're not matching the game mesh to the video pixel for pixel. You're matching player memory. The character in the game should feel like the same personality as in the reel. And because we locked one visual identity at the start, it did.</p>
<figure class="pipeline-media pipeline-media--single">
<img src="/blog/sketch-to-3d-pipeline/kali-preview.png" alt="Rendered preview of Hunyuan3D mesh" loading="lazy">
<figcaption>Preview render of the generated mesh. Textures and proportions before rigging.</figcaption>
</figure>
<p>The <a href="/tools/hunyuan3d.html">3D Model Generator</a> outputs a textured GLB. Not game-ready, but recognizably Kali. The silhouette matched. The materials were close. Good enough to move forward.</p>
<h2 id="drag-these-around" tabindex="-1">Drag these around <a class="header-anchor" href="#drag-these-around" aria-label="Permalink to &quot;Drag these around&quot;"></a></h2>
<p>Raw output from the <a href="/tools/hunyuan3d.html">3D Model Generator</a>. No rig, just a textured mesh. Next to it, the same character after auto-rigging with a baked walk animation.</p>
<ClientOnly>
  <SketchPipelineModels />
</ClientOnly>
<h2 id="rigging-and-animation" tabindex="-1">Rigging and animation <a class="header-anchor" href="#rigging-and-animation" aria-label="Permalink to &quot;Rigging and animation&quot;"></a></h2>
<p>The <a href="/tools/hunyuan3d.html">3D Model Generator</a> gives you a mesh. To make that mesh move, it needs a skeleton and animations. Our platform handles rigging automatically: upload the GLB, get back a rigged model with a full animation library. Walk cycles, idle breathing, jumps, everything a game character needs.</p>
<p>The default walk animation looked almost right, but not quite. Kali's arms sat too close to her body and her head tilted at a slightly wrong angle. Both made sense for a generic humanoid but felt off on a round cartoon kitty. So we added runtime bone corrections in the game code: a shoulder offset that pushes the arms outward and a head rotation fix that straightens the tilt. Small tweaks, big difference. Without them she looked stiff and robotic. With them she looked like Kali again.</p>
<h2 id="playing-the-character-in-a-browser" tabindex="-1">Playing the character in a browser <a class="header-anchor" href="#playing-the-character-in-a-browser" aria-label="Permalink to &quot;Playing the character in a browser&quot;"></a></h2>
<p>We dropped the rigged model into the <a href="/engine.html">Cinevva Engine</a>. Third-person camera, WASD movement, collectibles scattered around, orbit camera when idle. Getting the animation blending right took iteration: crossfading between idle and walk, normalizing height, fixing the forward axis, tweaking bone offsets. Third-person camera behind the character closes the loop from sketch to something you can walk around.</p>
<p>Try it yourself. WASD to move.</p>
<div class="pipeline-game-embed">
<iframe src="https://api.cinevva.com/games/game-mmbijtzp-d63d/" allow="fullscreen" loading="lazy"></iframe>
</div>
<h2 id="what-i-d-tell-someone-doing-this-tomorrow" tabindex="-1">What I'd tell someone doing this tomorrow <a class="header-anchor" href="#what-i-d-tell-someone-doing-this-tomorrow" aria-label="Permalink to &quot;What I'd tell someone doing this tomorrow&quot;"></a></h2>
<p>Lock one character look before you touch any generation tool. Every minute you spend getting that reference right saves an hour of chasing consistency later.</p>
<p>Write a tiny plot first. Then generate your start and end frames with the <a href="/tools/flux.html">Image Generator</a>. Keep reference images within API size limits. Use the T-pose from the same design system when you feed the <a href="/tools/hunyuan3d.html">3D Model Generator</a>.</p>
<p>Feed both frames to the <a href="/tools/video.html">Video Generator</a> and let it interpolate. For the game mesh, rig the model through the platform and tweak bone offsets at runtime if the default animations don't quite fit your character's proportions.</p>
<p>Two years ago, this pipeline didn't exist. You'd need a team and a budget. Now you need a sketch and Cinevva's <a href="/tools/">creative tools</a>. I think that's a meaningful change in who gets to bring a character to life.</p>
<hr>
<p><em>Cinevva tools used: <a href="/tools/flux.html">Image Generator</a>, <a href="/tools/video.html">Video Generator</a>, <a href="/tools/hunyuan3d.html">3D Model Generator</a>, auto-rigging, and <a href="/engine.html">Cinevva Engine</a>. Reflects early 2026.</em></p>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/blog/sketch-to-3d-pipeline/kali-start-frame.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 1: We started by trying to break it]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-01-risk-first</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-01-risk-first</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Before building caves or fancy shaders, we needed to answer one ugly question: can this thing even run in a browser?]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-1-we-started-by-trying-to-break-it" tabindex="-1">Building an open world in the browser, part 1: We started by trying to break it <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-1-we-started-by-trying-to-break-it" aria-label="Permalink to &quot;Building an open world in the browser, part 1: We started by trying to break it&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>We're building a multiplayer open world that runs entirely in the browser. No install, no app store, just a URL. The biggest risk up front was obvious: can a browser even render a persistent 3D world at playable frame rates while leaving headroom for gameplay, physics, and networking?</p>
<p>Most open world projects fail in a predictable order. First you get a nice concept. Then you get a pretty scene. Then you realize your frame budget is already gone before gameplay exists.</p>
<p>We wanted to answer the render budget question before investing in anything else. So Spike 1 skipped the pretty trailer and went straight to measuring.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/01-terrain/" title="Spike 1 Terrain and Instancing" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/01-terrain/" target="_blank">Open Spike 1 in a new tab ↗</a> · <a href="/blog/spike-source?spike=01-terrain">View source</a></p>
<p>The setup was simple on purpose. A 512 meter terrain mesh, procedural height from layered sine noise with island falloff, a water plane, atmospheric fog, and 500 instanced objects. We used plain WebGL with Three.js, ACES tone mapping, and no shadows.</p>
<p>We didn't care how it looked. We cared whether the scene stayed stable while moving the camera through it.</p>
<p>Two things came out of this spike that shaped the whole project.</p>
<p>First, we confirmed we had real headroom on desktop if we kept the first pass disciplined. That gave us confidence to attempt the harder terrain architecture later.</p>
<p>Second, we created a baseline contract. Every next spike had to explain its cost relative to this scene. If a new feature looked good but cost too much, it did not get promoted.</p>
<p>That baseline discipline became critical later when we hit seam artifacts, mixed LOD transitions, and compute driven meshing. Without a stable reference, every bug looks bigger than it is.</p>
<p>In part 2 we move from rendering to input feel. Worker physics sounds great in architecture docs. It only matters if the character still feels immediate when you press a key.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Heightmap terrain.</strong> A 2D grid where each cell stores a single elevation value. The GPU displaces a flat mesh in the vertex shader to create the terrain surface. Heightmaps are compact (a 65x65 chunk is ~8 KB at 16-bit), GPU-friendly, and fast to render. The limitation is that they can't represent caves, overhangs, or any surface that folds back over itself. For background on heightmap constraints and what comes after them, see our <a href="/guides/landscape-generation-browser.html#why-heightmaps-arent-enough">landscape generation guide</a>.</p>
<p><strong>Three.js.</strong> The rendering library we used throughout this project. Three.js abstracts WebGL 2 (and later WebGPU) into a scene graph with cameras, lights, materials, and geometry objects. It provides <code>InstancedMesh</code> for rendering many copies of the same geometry in a single draw call, frustum culling, PBR materials, and post-processing. See <a href="https://github.com/mrdoob/three.js" target="_blank" rel="noreferrer">Three.js on GitHub</a>. For how Three.js fits into a browser open world stack, see our <a href="/guides/browser-3d-open-world-tech.html#three-js">browser 3D tech guide</a>.</p>
<p><strong>InstancedMesh.</strong> A Three.js feature that renders N copies of the same geometry with a single draw call, each at a different position/rotation/scale. The per-instance transforms are stored in a matrix attribute buffer. This is how we rendered 500 objects in Spike 1 without 500 separate draw calls. For vegetation at scale, GPU-driven instanced culling takes this further. See our <a href="/guides/landscape-generation-browser.html#gpu-driven-vegetation-culling">landscape guide on GPU vegetation culling</a>.</p>
<p><strong>Frame budget.</strong> At 60 fps, each frame has <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.816ex;" xmlns="http://www.w3.org/2000/svg" width="13.598ex" height="2.773ex" role="img" focusable="false" viewBox="0 -864.9 6010.2 1225.5" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mfrac"><g data-mml-node="mrow" transform="translate(220,394) scale(0.707)"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(2000,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(2250,0)"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(833,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mn" transform="translate(1095.8,-345) scale(0.707)"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)" style="stroke-width: 3;"/></g><rect width="2658.6" height="60" x="120" y="220"/></g><g data-mml-node="mo" transform="translate(3176.4,0)"><path data-c="2248" d="M55 319Q55 360 72 393T114 444T163 472T205 482Q207 482 213 482T223 483Q262 483 296 468T393 413L443 381Q502 346 553 346Q609 346 649 375T694 454Q694 465 698 474T708 483Q722 483 722 452Q722 386 675 338T555 289Q514 289 468 310T388 357T308 404T224 426Q164 426 125 393T83 318Q81 289 69 289Q55 289 55 319ZM55 85Q55 126 72 159T114 210T163 238T205 248Q207 248 213 248T223 249Q262 249 296 234T393 179L443 147Q502 112 553 112Q609 112 649 141T694 220Q694 249 708 249T722 217Q722 153 675 104T555 55Q514 55 468 76T388 123T308 170T224 192Q164 192 125 159T83 84Q80 55 69 55Q55 55 55 85Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(4232.2,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="37" d="M55 458Q56 460 72 567L88 674Q88 676 108 676H128V672Q128 662 143 655T195 646T364 644H485V605L417 512Q408 500 387 472T360 435T339 403T319 367T305 330T292 284T284 230T278 162T275 80Q275 66 275 52T274 28V19Q270 2 255 -10T221 -22Q210 -22 200 -19T179 0T168 40Q168 198 265 368Q285 400 349 489L395 552H302Q128 552 119 546Q113 543 108 522T98 479L95 458V455H55V458Z" transform="translate(1278,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mfrac><mrow><mn>1000</mn><mtext> </mtext><mtext>ms</mtext></mrow><mn>60</mn></mfrac><mo>≈</mo><mn>16.7</mn></math></mjx-assistive-mml></mjx-container> ms for everything: JavaScript logic, physics, rendering, and compositing. A &quot;budget check&quot; spike measures how much of that a baseline scene consumes, so the headroom left for everything you add later is <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.357ex;" xmlns="http://www.w3.org/2000/svg" width="22.813ex" height="1.886ex" role="img" focusable="false" viewBox="0 -676 10083.4 833.8" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(394,-150) scale(0.707)"><path data-c="66" d="M273 0Q255 3 146 3Q43 3 34 0H26V46H42Q70 46 91 49Q99 52 103 60Q104 62 104 224V385H33V431H104V497L105 564L107 574Q126 639 171 668T266 704Q267 704 275 704T289 705Q330 702 351 679T372 627Q372 604 358 590T321 576T284 590T270 627Q270 647 288 667H284Q280 668 273 668Q245 668 223 647T189 592Q183 572 182 497V431H293V385H185V225Q185 63 186 61T189 57T194 54T199 51T206 49T213 48T222 47T231 47T241 46T251 46H282V0H273Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(306,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(750,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(1250,0)" style="stroke-width: 3;"/><path data-c="75" d="M383 58Q327 -10 256 -10H249Q124 -10 105 89Q104 96 103 226Q102 335 102 348T96 369Q86 385 36 385H25V408Q25 431 27 431L38 432Q48 433 67 434T105 436Q122 437 142 438T172 441T184 442H187V261Q188 77 190 64Q193 49 204 40Q224 26 264 26Q290 26 311 35T343 58T363 90T375 120T379 144Q379 145 379 161T380 201T380 248V315Q380 361 370 372T320 385H302V431Q304 431 378 436T457 442H464V264Q464 84 465 81Q468 61 479 55T524 46H542V0Q540 0 467 -5T390 -11H383V58Z" transform="translate(1639,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(2195,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(2587,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(3031,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3143.6,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(4199.4,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="37" d="M55 458Q56 460 72 567L88 674Q88 676 108 676H128V672Q128 662 143 655T195 646T364 644H485V605L417 512Q408 500 387 472T360 435T339 403T319 367T305 330T292 284T284 230T278 162T275 80Q275 66 275 52T274 28V19Q270 2 255 -10T221 -22Q210 -22 200 -19T179 0T168 40Q168 198 265 368Q285 400 349 489L395 552H302Q128 552 119 546Q113 543 108 522T98 479L95 458V455H55V458Z" transform="translate(1278,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6199.6,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(7199.8,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(394,-150) scale(0.707)"><path data-c="62" d="M307 -11Q234 -11 168 55L158 37Q156 34 153 28T147 17T143 10L138 1L118 0H98V298Q98 599 97 603Q94 622 83 628T38 637H20V660Q20 683 22 683L32 684Q42 685 61 686T98 688Q115 689 135 690T165 693T176 694H179V543Q179 391 180 391L183 394Q186 397 192 401T207 411T228 421T254 431T286 439T323 442Q401 442 461 379T522 216Q522 115 458 52T307 -11ZM182 98Q182 97 187 90T196 79T206 67T218 55T233 44T250 35T271 29T295 26Q330 26 363 46T412 113Q424 148 424 212Q424 287 412 323Q385 405 300 405Q270 405 239 390T188 347L182 339V98Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(1056,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1450,0)" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(1894,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(2172,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(2450,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(3006,0)" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>t</mi><mtext>features</mtext></msub><mo>=</mo><mn>16.7</mn><mo>−</mo><msub><mi>t</mi><mtext>baseline</mtext></msub></math></mjx-assistive-mml></mjx-container>. If the baseline already eats 12 ms, only ~4.7 ms remains for gameplay, physics, and networking combined. This approach comes from AAA open world development where <a href="/guides/browser-3d-open-world-tech.html#what-we-can-learn-from-skyrim-and-the-witcher">GTA V, Skyrim, and Elden Ring</a> all use aggressive LOD and streaming to stay within fixed frame budgets.</p>
<hr>
<p>Part 1 of 12.<br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-02-worker-physics.html">Part 2 - Worker physics and the input lag fear</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 2: Worker physics and the input lag fear]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-02-worker-physics</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-02-worker-physics</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We moved Rapier into a Web Worker and measured what everyone worries about first: does movement feel late?]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-2-worker-physics-and-the-input-lag-fear" tabindex="-1">Building an open world in the browser, part 2: Worker physics and the input lag fear <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-2-worker-physics-and-the-input-lag-fear" aria-label="Permalink to &quot;Building an open world in the browser, part 2: Worker physics and the input lag fear&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>If you build browser multiplayer long enough, you eventually get this argument.</p>
<p>&quot;Physics in a worker is clean architecture. Physics on the main thread feels safer.&quot;</p>
<p>Both can be true. What matters is control feel and latency under real input.</p>
<p>Spike 2 was built to answer that with measurements, not opinions.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/02-rapier-worker/" title="Spike 2 Rapier Worker" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/02-rapier-worker/" target="_blank">Open Spike 2 in a new tab ↗</a> · <a href="/blog/spike-source?spike=02-rapier-worker">View source</a></p>
<p>We reused the Spike 1 terrain and integrated Rapier in a dedicated module worker. Input state was sent to the worker every frame, simulation stepped there, and authoritative position came back to the renderer.</p>
<p>The key metrics were input to visible movement latency and physics step timing. We also watched for jitter under normal movement, sprint bursts, and jump cadence.</p>
<p>The result was better than expected. With our message shape and cadence, the worker boundary did not dominate latency. Controls still felt immediate, which was the only thing players would care about.</p>
<p>One challenge from this phase was interpretation risk. After a successful result, teams often overgeneralize and assume the architecture question is closed forever. It is not. We only validated one concrete scenario and hardware profile. Later spikes still had to recheck assumptions when GPU and streaming pressure changed.</p>
<p>This spike also gave us a process upgrade. We started exposing timing telemetry in HUD by default for interactive spikes. That changed team conversations from &quot;it feels off&quot; to &quot;this path added 1.2 ms.&quot;</p>
<p>In part 3 we cover the less glamorous experiments that prevented expensive surprises later. Broadcast load, mobile constraints, and behavior generation reliability.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Rapier.</strong> A physics engine written in Rust that compiles to WebAssembly for browser use. It handles rigid bodies, colliders, joints, character controllers, and raycasting at 2-3x native performance. For open worlds, Rapier provides player character controllers (walking on terrain, climbing steps, sliding on slopes), object collision, raycasting for interactions, and trigger volumes. See <a href="https://rapier.rs/" target="_blank" rel="noreferrer">Rapier documentation</a> and our <a href="/guides/browser-3d-open-world-tech.html#rapier-rust-wasm">browser 3D tech guide on physics</a>.</p>
<p><strong>Web Workers.</strong> Browser threads that run JavaScript (or Wasm) off the main thread. Physics simulation in a worker means a heavy <code>world.step()</code> call doesn't block rendering. The main thread sends input state to the worker each frame via <code>postMessage</code> and receives authoritative positions back. The latency penalty is the two message hops (~0.1-0.5 ms each on desktop). The benefit is that the render thread never stalls on collision detection. Transferable objects (<code>ArrayBuffer</code> transfer) eliminate copy overhead for large position arrays.</p>
<p><strong>WebAssembly (Wasm).</strong> A binary instruction format that runs at near-native speed in browsers. Rapier, Havok, and Recast all compile to Wasm. The physics step in Rapier-Wasm is typically 0.5-2 ms for a few hundred bodies, compared to 5-15 ms for equivalent JavaScript. Wasm modules load as <code>.wasm</code> files fetched alongside the JavaScript glue code. See <a href="https://webassembly.org/" target="_blank" rel="noreferrer">WebAssembly specification</a>.</p>
<p><strong>Input-to-visual latency.</strong> The time between a keypress and the resulting visual change on screen. For movement to feel &quot;immediate,&quot; this needs to stay under ~80 ms. In a worker physics setup the chain is additive:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.669ex;" xmlns="http://www.w3.org/2000/svg" width="39.876ex" height="2.214ex" role="img" focusable="false" viewBox="0 -683 17625.4 978.7" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(958.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2014.6,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(394,-150) scale(0.707)"><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(278,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(834,0)" style="stroke-width: 3;"/><path data-c="75" d="M383 58Q327 -10 256 -10H249Q124 -10 105 89Q104 96 103 226Q102 335 102 348T96 369Q86 385 36 385H25V408Q25 431 27 431L38 432Q48 433 67 434T105 436Q122 437 142 438T172 441T184 442H187V261Q188 77 190 64Q193 49 204 40Q224 26 264 26Q290 26 311 35T343 58T363 90T375 120T379 144Q379 145 379 161T380 201T380 248V315Q380 361 370 372T320 385H302V431Q304 431 378 436T457 442H464V264Q464 84 465 81Q468 61 479 55T524 46H542V0Q540 0 467 -5T390 -11H383V58Z" transform="translate(1390,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(1946,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(4331.9,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(5332.1,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(5832.1,0)"><g data-mml-node="mspace"/></g><g data-mml-node="msub" transform="translate(5999.1,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(394,-150) scale(0.707)"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" transform="translate(1227,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(7886.5,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(8886.7,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(394,-150) scale(0.707)"><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(394,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(783,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(1227,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(10813.7,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(11813.9,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(394,-150) scale(0.707)"><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(392,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(836,0)" style="stroke-width: 3;"/><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" transform="translate(1392,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1948,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(2392,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(14448.7,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(15449,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(394,-150) scale(0.707)"><path data-c="76" d="M338 431Q344 429 422 429Q479 429 503 431H508V385H497Q439 381 423 345Q421 341 356 172T288 -2Q283 -11 263 -11Q244 -11 239 -2Q99 359 98 364Q93 378 82 381T43 385H19V431H25L33 430Q41 430 53 430T79 430T104 429T122 428Q217 428 232 431H240V385H226Q187 384 184 370Q184 366 235 234L286 102L377 341V349Q377 363 367 372T349 383T335 385H331V431H338Z" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(528,0)" style="stroke-width: 3;"/><path data-c="79" d="M69 -66Q91 -66 104 -80T118 -116Q118 -134 109 -145T91 -160Q84 -163 97 -166Q104 -168 111 -168Q131 -168 148 -159T175 -138T197 -106T213 -75T225 -43L242 0L170 183Q150 233 125 297Q101 358 96 368T80 381Q79 382 78 382Q66 385 34 385H19V431H26L46 430Q65 430 88 429T122 428Q129 428 142 428T171 429T200 430T224 430L233 431H241V385H232Q183 385 185 366L286 112Q286 113 332 227L376 341V350Q376 365 366 373T348 383T334 385H331V431H337H344Q351 431 361 431T382 430T405 429T422 429Q477 429 503 431H508V385H497Q441 380 422 345Q420 343 378 235T289 9T227 -131Q180 -204 113 -204Q69 -204 44 -177T19 -116Q19 -89 35 -78T69 -66Z" transform="translate(922,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1450,0)" style="stroke-width: 3;"/><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" transform="translate(2006,0)" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>L</mi><mo>=</mo><msub><mi>t</mi><mtext>input</mtext></msub><mo>+</mo><mn>2</mn><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><msub><mi>t</mi><mtext>msg</mtext></msub><mo>+</mo><msub><mi>t</mi><mtext>step</mtext></msub><mo>+</mo><msub><mi>t</mi><mtext>render</mtext></msub><mo>+</mo><msub><mi>t</mi><mtext>vsync</mtext></msub></math></mjx-assistive-mml></mjx-container><p>keydown event (main thread), <code>postMessage</code> to the worker and back (<mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.669ex;" xmlns="http://www.w3.org/2000/svg" width="5.276ex" height="2.176ex" role="img" focusable="false" viewBox="0 -666 2332.2 961.7" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(500,0)"><g data-mml-node="mspace"/></g><g data-mml-node="msub" transform="translate(667,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(394,-150) scale(0.707)"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" transform="translate(1227,0)" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>2</mn><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><msub><mi>t</mi><mtext>msg</mtext></msub></math></mjx-assistive-mml></mjx-container>), the physics step, the renderer applying the new position, then the next vsync. Each term is small on its own (~0.1-0.5 ms per message hop on desktop), but they stack, which is why we measured <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.541ex" height="1.545ex" role="img" focusable="false" viewBox="0 -683 681 683" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>L</mi></math></mjx-assistive-mml></mjx-container> directly instead of trusting the architecture diagram.</p>
<hr>
<p>Part 2 of 12.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-01-risk-first.html">Part 1 - We started by trying to break it</a><br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-03-the-unflashy-spikes.html">Part 3 - The unflashy spikes that saved us</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 3: The unflashy spikes that saved us]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-03-the-unflashy-spikes</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-03-the-unflashy-spikes</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Durable Object broadcast load, mobile quality constraints, and behavior generation reliability were not glamorous, but they prevented expensive surprises.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-3-the-unflashy-spikes-that-saved-us" tabindex="-1">Building an open world in the browser, part 3: The unflashy spikes that saved us <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-3-the-unflashy-spikes-that-saved-us" aria-label="Permalink to &quot;Building an open world in the browser, part 3: The unflashy spikes that saved us&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>This part has fewer shiny screenshots and more architecture insurance.</p>
<p>After Spikes 1 and 2, we ran three risk checks that looked small but had product level impact.</p>
<p>First was broadcast fan out with Durable Objects. We tested multi client position distribution at game-like tick rates and tracked latency distribution, CPU per tick, and delivery integrity. If this had failed, we would have moved to early sharding instead of single island ownership.</p>
<p>Second was mobile constraint validation. Not a desktop preset renamed to mobile, but an explicit low-cost profile from the same baseline terrain.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/01-terrain/?quality=mobile" title="Mobile quality profile based on Spike 1" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/01-terrain/?quality=mobile" target="_blank">Open the mobile profile in a new tab ↗</a> · <a href="/blog/spike-source?spike=01-terrain">View source</a></p>
<p>We reduced segment density, object count, render resolution pressure, and fog range. The question was simple. Can this world remain readable and responsive on mobile class constraints without rewriting the entire renderer.</p>
<p>Third was behavior generation reliability for creator workflows. We evaluated valid JSON rate, semantic correctness against expected primitives, and response latency. If this had failed, we would have moved to strict form based behavior authoring.</p>
<p>The key insight from this chapter is that these unflashy spikes changed architecture faster than visual spikes did. They set hard boundaries on network topology, mobile promises, and tool UX.</p>
<p>In part 4 we return to visible terrain work and test movement-time streaming behavior, not just static loading screenshots.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Cloudflare Durable Objects.</strong> Edge-deployed stateful serverless instances with built-in persistence and WebSocket support. Each Durable Object holds authoritative state for a world shard (or chunk). Players connect via WebSocket and receive position broadcasts from other players in the same shard. When the player moves to an adjacent chunk, they connect to that chunk's Durable Object. Durable Objects auto-persist state to disk and scale to thousands of concurrent instances. See <a href="https://developers.cloudflare.com/durable-objects/" target="_blank" rel="noreferrer">Cloudflare Durable Objects docs</a> and our <a href="/guides/browser-3d-open-world-tech.html#multiplayer-networking">browser 3D tech guide on multiplayer networking</a>.</p>
<p><strong>Spatial sharding.</strong> Dividing the world across server instances by geographic region. Each shard owns a rectangular area of the world grid. As player density shifts, shards can split or merge. Players near a shard boundary see both shards' content through cross-shard visibility queries. This is how <a href="/guides/browser-3d-open-world-tech.html#server-architecture">EVE Online handles thousands of players</a> in one universe.</p>
<p><strong>WebSocket broadcast fan-out.</strong> Distributing real-time position updates from one server to many connected clients. Per-client downstream bandwidth scales linearly with the crowd you can see:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.666ex;" xmlns="http://www.w3.org/2000/svg" width="23.398ex" height="2.236ex" role="img" focusable="false" viewBox="0 -694 10341.8 988.2" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D435" d="M231 637Q204 637 199 638T194 649Q194 676 205 682Q206 683 335 683Q594 683 608 681Q671 671 713 636T756 544Q756 480 698 429T565 360L555 357Q619 348 660 311T702 219Q702 146 630 78T453 1Q446 0 242 0Q42 0 39 2Q35 5 35 10Q35 17 37 24Q42 43 47 45Q51 46 62 46H68Q95 46 128 49Q142 52 147 61Q150 65 219 339T288 628Q288 635 231 637ZM649 544Q649 574 634 600T585 634Q578 636 493 637Q473 637 451 637T416 636H403Q388 635 384 626Q382 622 352 506Q352 503 351 500L320 374H401Q482 374 494 376Q554 386 601 434T649 544ZM595 229Q595 273 572 302T512 336Q506 337 429 337Q311 337 310 336Q310 334 293 263T258 122L240 52Q240 48 252 48T333 46Q422 46 429 47Q491 54 543 105T595 229Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(792,-150) scale(0.707)"><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(444,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(722,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1444,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(2000,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2809.1,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3864.8,0)"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(836,-150) scale(0.707)"><path data-c="76" d="M338 431Q344 429 422 429Q479 429 503 431H508V385H497Q439 381 423 345Q421 341 356 172T288 -2Q283 -11 263 -11Q244 -11 239 -2Q99 359 98 364Q93 378 82 381T43 385H19V431H25L33 430Q41 430 53 430T79 430T104 429T122 428Q217 428 232 431H240V385H226Q187 384 184 370Q184 366 235 234L286 102L377 341V349Q377 363 367 372T349 383T335 385H331V431H338Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(528,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(806,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(1200,0)" style="stroke-width: 3;"/><path data-c="62" d="M307 -11Q234 -11 168 55L158 37Q156 34 153 28T147 17T143 10L138 1L118 0H98V298Q98 599 97 603Q94 622 83 628T38 637H20V660Q20 683 22 683L32 684Q42 685 61 686T98 688Q115 689 135 690T165 693T176 694H179V543Q179 391 180 391L183 394Q186 397 192 401T207 411T228 421T254 431T286 439T323 442Q401 442 461 379T522 216Q522 115 458 52T307 -11ZM182 98Q182 97 187 90T196 79T206 67T218 55T233 44T250 35T271 29T295 26Q330 26 363 46T412 113Q424 148 424 212Q424 287 412 323Q385 405 300 405Q270 405 239 390T188 347L182 339V98Z" transform="translate(1478,0)" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(2034,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(2312,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(6921.8,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(7922.1,0)"><g data-mml-node="mi"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(462,-150) scale(0.707)"><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(834,0)" style="stroke-width: 3;"/><path data-c="79" d="M69 -66Q91 -66 104 -80T118 -116Q118 -134 109 -145T91 -160Q84 -163 97 -166Q104 -168 111 -168Q131 -168 148 -159T175 -138T197 -106T213 -75T225 -43L242 0L170 183Q150 233 125 297Q101 358 96 368T80 381Q79 382 78 382Q66 385 34 385H19V431H26L46 430Q65 430 88 429T122 428Q129 428 142 428T171 429T200 430T224 430L233 431H241V385H232Q183 385 185 366L286 112Q286 113 332 227L376 341V350Q376 365 366 373T348 383T334 385H331V431H337H344Q351 431 361 431T382 430T405 429T422 429Q477 429 503 431H508V385H497Q441 380 422 345Q420 343 378 235T289 9T227 -131Q180 -204 113 -204Q69 -204 44 -177T19 -116Q19 -89 35 -78T69 -66Z" transform="translate(1334,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1862,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(2306,0)" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><msub><mi>B</mi><mtext>client</mtext></msub><mo>=</mo><msub><mi>N</mi><mtext>visible</mtext></msub><mo>×</mo><msub><mi>b</mi><mtext>player</mtext></msub></math></mjx-assistive-mml></mjx-container><p>At game-like tick rates (20-30 Hz) each player generates <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.666ex;" xmlns="http://www.w3.org/2000/svg" width="11.885ex" height="2.236ex" role="img" focusable="false" viewBox="0 -694 5253.3 988.2" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(462,-150) scale(0.707)"><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(834,0)" style="stroke-width: 3;"/><path data-c="79" d="M69 -66Q91 -66 104 -80T118 -116Q118 -134 109 -145T91 -160Q84 -163 97 -166Q104 -168 111 -168Q131 -168 148 -159T175 -138T197 -106T213 -75T225 -43L242 0L170 183Q150 233 125 297Q101 358 96 368T80 381Q79 382 78 382Q66 385 34 385H19V431H26L46 430Q65 430 88 429T122 428Q129 428 142 428T171 429T200 430T224 430L233 431H241V385H232Q183 385 185 366L286 112Q286 113 332 227L376 341V350Q376 365 366 373T348 383T334 385H331V431H337H344Q351 431 361 431T382 430T405 429T422 429Q477 429 503 431H508V385H497Q441 380 422 345Q420 343 378 235T289 9T227 -131Q180 -204 113 -204Q69 -204 44 -177T19 -116Q19 -89 35 -78T69 -66Z" transform="translate(1334,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1862,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(2306,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2697.6,0)"><path data-c="2248" d="M55 319Q55 360 72 393T114 444T163 472T205 482Q207 482 213 482T223 483Q262 483 296 468T393 413L443 381Q502 346 553 346Q609 346 649 375T694 454Q694 465 698 474T708 483Q722 483 722 452Q722 386 675 338T555 289Q514 289 468 310T388 357T308 404T224 426Q164 426 125 393T83 318Q81 289 69 289Q55 289 55 319ZM55 85Q55 126 72 159T114 210T163 238T205 248Q207 248 213 248T223 249Q262 249 296 234T393 179L443 147Q502 112 553 112Q609 112 649 141T694 220Q694 249 708 249T722 217Q722 153 675 104T555 55Q514 55 468 76T388 123T308 170T224 192Q164 192 125 159T83 84Q80 55 69 55Q55 55 55 85Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3753.3,0)"><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>b</mi><mtext>player</mtext></msub><mo>≈</mo><mn>800</mn></math></mjx-assistive-mml></mjx-container> bytes/second of position data, so 200 players in view costs <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="15.964ex" height="1.692ex" role="img" focusable="false" viewBox="0 -666 7056 748" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1722.2,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2722.4,0)"><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4500.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(5556,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>200</mn><mo>×</mo><mn>800</mn><mo>=</mo><mn>160</mn></math></mjx-assistive-mml></mjx-container> KB/s per client. The naive all-to-all server cost is worse, growing like <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="3.119ex" height="1.887ex" role="img" focusable="false" viewBox="0 -833.9 1378.8 833.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(975.3,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>N</mi><mn>2</mn></msup></math></mjx-assistive-mml></mjx-container>, which is exactly why relevance filtering (spatial interest management) caps <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.357ex;" xmlns="http://www.w3.org/2000/svg" width="6.414ex" height="1.902ex" role="img" focusable="false" viewBox="0 -683 2834.8 840.8" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(836,-150) scale(0.707)"><path data-c="76" d="M338 431Q344 429 422 429Q479 429 503 431H508V385H497Q439 381 423 345Q421 341 356 172T288 -2Q283 -11 263 -11Q244 -11 239 -2Q99 359 98 364Q93 378 82 381T43 385H19V431H25L33 430Q41 430 53 430T79 430T104 429T122 428Q217 428 232 431H240V385H226Q187 384 184 370Q184 366 235 234L286 102L377 341V349Q377 363 367 372T349 383T335 385H331V431H338Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(528,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(806,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(1200,0)" style="stroke-width: 3;"/><path data-c="62" d="M307 -11Q234 -11 168 55L158 37Q156 34 153 28T147 17T143 10L138 1L118 0H98V298Q98 599 97 603Q94 622 83 628T38 637H20V660Q20 683 22 683L32 684Q42 685 61 686T98 688Q115 689 135 690T165 693T176 694H179V543Q179 391 180 391L183 394Q186 397 192 401T207 411T228 421T254 431T286 439T323 442Q401 442 461 379T522 216Q522 115 458 52T307 -11ZM182 98Q182 97 187 90T196 79T206 67T218 55T233 44T250 35T271 29T295 26Q330 26 363 46T412 113Q424 148 424 212Q424 287 412 323Q385 405 300 405Q270 405 239 390T188 347L182 339V98Z" transform="translate(1478,0)" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(2034,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(2312,0)" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>N</mi><mtext>visible</mtext></msub></math></mjx-assistive-mml></mjx-container> before it pushes to each connection within the tick budget. See our <a href="/guides/browser-3d-open-world-tech.html#client-server-communication">networking guide</a> for delta compression and update frequency tradeoffs.</p>
<p><strong>Mobile rendering constraints.</strong> Mobile GPUs have 1/5 to 1/10 the throughput of desktop GPUs, ~1 GB memory limit (vs 2-4 GB on desktop), and thermal throttling under sustained load. A mobile quality profile reduces segment density, object count, render resolution, draw distance, and shadow quality. The goal isn't parity with desktop but maintaining readability and responsiveness. See <a href="/guides/browser-3d-open-world-tech.html#browser-3d-performance-real-numbers">browser 3D performance numbers</a> for real GPU benchmarks.</p>
<hr>
<p>Part 3 of 12.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-02-worker-physics.html">Part 2 - Worker physics and the input lag fear</a><br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-04-streaming-before-fancy.html">Part 4 - Streaming before fancy terrain</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 4: Streaming before fancy terrain]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-04-streaming-before-fancy</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-04-streaming-before-fancy</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We tested chunk load and swap first with simple content, then moved to progressive heightmap refinement. That order paid off.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-4-streaming-before-fancy-terrain" tabindex="-1">Building an open world in the browser, part 4: Streaming before fancy terrain <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-4-streaming-before-fancy-terrain" aria-label="Permalink to &quot;Building an open world in the browser, part 4: Streaming before fancy terrain&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>Streaming is where &quot;looks good&quot; projects usually fall apart.</p>
<p>You can hide a lot in a still frame. You cannot hide a 40 ms hitch while crossing a chunk boundary.</p>
<p>We intentionally tested streaming before we built advanced terrain representation. That gave us clean signal on load and unload behavior.</p>
<p>Spike 6 validated neighborhood churn with simple chunk content.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/06-chunk-streaming/" title="Spike 6 Chunk Load Swap" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/06-chunk-streaming/" target="_blank">Open Spike 6 in a new tab ↗</a> · <a href="/blog/spike-source?spike=06-chunk-streaming">View source</a></p>
<p>Then we moved to the real terrain path in Spike 11. Height chunk streaming with worker side decode and progressive refinement from 17 to 33 to 65 sample grids.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/11-chunk-streaming/" title="Spike 11 Heightmap Chunk Streaming" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/11-chunk-streaming/" target="_blank">Open Spike 11 in a new tab ↗</a> · <a href="/blog/spike-source?spike=11-chunk-streaming">View source</a></p>
<p>The sequencing mattered more than we expected. If we had started directly with compressed height chunks, every hitch would have been ambiguous. Decode issue, texture upload issue, or geometry update issue. Spike 6 removed one uncertainty layer before Spike 11 added complexity.</p>
<p>A practical lesson from this chapter carried into later spikes. Upload stalls must be measured directly, not inferred from average FPS. Average FPS hides frame spikes, and frame spikes are what users actually feel.</p>
<p>In part 5 we move into the visual cost chapter where vegetation, terrain shaders, and cascaded shadows compete for the same frame budget.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Chunk-based streaming.</strong> The world is divided into a grid of independent chunks (typically 64x64 meters). As the player moves, chunks on the trailing edge unload while chunks on the leading edge stream in. This is how <a href="/guides/browser-3d-open-world-tech.html#skyrim-s-cell-system">Skyrim's cell system</a> works: a 5x5 grid of cells loaded around the player, swapping as they move. The browser version adds network latency to the equation, making predictive pre-fetching based on player velocity critical. See our <a href="/guides/landscape-generation-browser.html#streaming-architecture-for-terrain">streaming architecture guide</a>.</p>
<p><strong>Progressive heightmap refinement.</strong> Send terrain at low resolution first, then refine. The grid sizes aren't arbitrary: each level is a <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="18.385ex" height="2.497ex" role="img" focusable="false" viewBox="0 -853.7 8126.1 1103.7" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(389,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(533,363) scale(0.707)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1562.6,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2562.8,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3062.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3674.1,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4674.3,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(5063.3,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(533,363) scale(0.707)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(6236.9,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(7237.1,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(7737.1,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><msup><mn>2</mn><mi>k</mi></msup><mo>+</mo><mn>1</mn><mo stretchy="false">)</mo><mo>×</mo><mo stretchy="false">(</mo><msup><mn>2</mn><mi>k</mi></msup><mo>+</mo><mn>1</mn><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container> grid, so <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="11.295ex" height="2.09ex" role="img" focusable="false" viewBox="0 -841.7 4992.6 923.7" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/><path data-c="37" d="M55 458Q56 460 72 567L88 674Q88 676 108 676H128V672Q128 662 143 655T195 646T364 644H485V605L417 512Q408 500 387 472T360 435T339 403T319 367T305 330T292 284T284 230T278 162T275 80Q275 66 275 52T274 28V19Q270 2 255 -10T221 -22Q210 -22 200 -19T179 0T168 40Q168 198 265 368Q285 400 349 489L395 552H302Q128 552 119 546Q113 543 108 522T98 479L95 458V455H55V458Z" transform="translate(500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1277.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(2333.6,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(533,363) scale(0.707)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3492.3,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(4492.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>17</mn><mo>=</mo><msup><mn>2</mn><mn>4</mn></msup><mo>+</mo><mn>1</mn></math></mjx-assistive-mml></mjx-container>, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="11.295ex" height="2.072ex" role="img" focusable="false" viewBox="0 -833.9 4992.6 915.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" style="stroke-width: 3;"/><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1277.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(2333.6,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(533,363) scale(0.707)"><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3492.3,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(4492.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>33</mn><mo>=</mo><msup><mn>2</mn><mn>5</mn></msup><mo>+</mo><mn>1</mn></math></mjx-assistive-mml></mjx-container>, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="11.295ex" height="2.072ex" role="img" focusable="false" viewBox="0 -833.9 4992.6 915.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" style="stroke-width: 3;"/><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1277.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(2333.6,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(533,363) scale(0.707)"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3492.3,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(4492.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>65</mn><mo>=</mo><msup><mn>2</mn><mn>6</mn></msup><mo>+</mo><mn>1</mn></math></mjx-assistive-mml></mjx-container>. The <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="2.891ex" height="1.692ex" role="img" focusable="false" viewBox="0 -666 1278 748" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(778,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo>+</mo><mn>1</mn></math></mjx-assistive-mml></mjx-container> keeps a shared sample on every boundary so neighbors line up, and each step roughly quadruples the sample count (<mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.345ex" height="1.912ex" role="img" focusable="false" viewBox="0 -833.9 1036.6 844.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(633,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>n</mi><mn>2</mn></msup></math></mjx-assistive-mml></mjx-container> grows as the side doubles). A 17x17 grid (the minimum for a 64m chunk at 4m spacing) is ~200 bytes compressed and renders a visible surface instantly. Then stream the 33x33 refinement, then the full 65x65 resolution. Each level adds samples without replacing previous data. This maps directly to geometry clipmap LOD rings where distant terrain uses low-resolution data and close-up terrain uses full resolution. See <a href="/guides/landscape-generation-browser.html#progressive-chunk-loading">progressive chunk loading</a>.</p>
<p><strong>Delta encoding and compression.</strong> Heightmap data compresses well because adjacent cells have similar values. Delta encoding stores the difference between each cell and its predicted value (average of neighbors), clustering values near zero. Combined with zlib or brotli, a 65x65 chunk drops from 8.4 KB raw to 1-2 KB compressed. At reduced precision for distant chunks (8-bit instead of 16-bit): 0.5-1 KB. See <a href="/guides/landscape-generation-browser.html#terrain-data-compression-for-streaming">terrain data compression</a>.</p>
<p><strong>Predictive pre-fetching.</strong> Loading chunks before the player arrives. The lead distance has to cover how far the player travels while a chunk loads, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.65ex;" xmlns="http://www.w3.org/2000/svg" width="16.701ex" height="2.22ex" role="img" focusable="false" viewBox="0 -694 7381.6 981.2" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(553,-150) scale(0.707)"><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(948,0)" style="stroke-width: 3;"/><path data-c="66" d="M273 0Q255 3 146 3Q43 3 34 0H26V46H42Q70 46 91 49Q99 52 103 60Q104 62 104 224V385H33V431H104V497L105 564L107 574Q126 639 171 668T266 704Q267 704 275 704T289 705Q330 702 351 679T372 627Q372 604 358 590T321 576T284 590T270 627Q270 647 288 667H284Q280 668 273 668Q245 668 223 647T189 592Q183 572 182 497V431H293V385H185V225Q185 63 186 61T189 57T194 54T199 51T206 49T213 48T222 47T231 47T241 46T251 46H282V0H273Z" transform="translate(1392,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1698,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(2142,0)" style="stroke-width: 3;"/><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" transform="translate(2531,0)" style="stroke-width: 3;"/><path data-c="68" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 124T102 167T103 217T103 272T103 329Q103 366 103 407T103 482T102 542T102 586T102 603Q99 622 88 628T43 637H25V660Q25 683 27 683L37 684Q47 685 66 686T103 688Q120 689 140 690T170 693T181 694H184V367Q244 442 328 442Q451 442 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(2975,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3377.6,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4433.3,0)"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(5140.6,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(5640.8,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(394,-150) scale(0.707)"><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(278,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(778,0)" style="stroke-width: 3;"/><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" transform="translate(1278,0)" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>d</mi><mtext>prefetch</mtext></msub><mo>=</mo><mi>v</mi><mo>⋅</mo><msub><mi>t</mi><mtext>load</mtext></msub></math></mjx-assistive-mml></mjx-container>, so it scales with speed: at walking speed (5 km/h) pre-fetch 2 chunks ahead (128m), at running speed (15 km/h) pre-fetch 4. The load ring shifts with velocity direction. A priority queue sorts pending requests by urgency and cancels requests for chunks the player has moved away from. See <a href="/guides/landscape-generation-browser.html#predictive-pre-fetching">predictive pre-fetching</a>.</p>
<hr>
<p>Part 4 of 12.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-03-the-unflashy-spikes.html">Part 3 - The unflashy spikes that saved us</a><br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-05-budgeting-the-pretty.html">Part 5 - Budgeting the pretty stuff</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 5: Budgeting the pretty stuff]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-05-budgeting-the-pretty</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-05-budgeting-the-pretty</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Vegetation, terrain materials, and cascaded shadows looked great. The real work was proving they could fit the frame budget.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-5-budgeting-the-pretty-stuff" tabindex="-1">Building an open world in the browser, part 5: Budgeting the pretty stuff <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-5-budgeting-the-pretty-stuff" aria-label="Permalink to &quot;Building an open world in the browser, part 5: Budgeting the pretty stuff&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>This was the chapter where visual ambition met arithmetic.</p>
<p>We split rendering cost into separate spikes because bundled results are hard to diagnose. If you turn everything on at once, you only learn that the frame is slow. You don't learn which feature ate the budget.</p>
<p>Spike 7 targeted vegetation density and animation cost. The approach was runtime scattering from 32x32 density maps per terrain chunk, feeding large <code>InstancedMesh</code> sets. Each grass blade and shrub cluster got vertex-shader wind driven by a scrolling noise texture. The key number we watched wasn't triangle count but draw call overhead and vertex throughput on mid-range GPUs. We found that batching instances into fewer meshes mattered more than reducing per-blade polygon count.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/07-gpu-vegetation/" title="Spike 7 GPU Vegetation" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/07-gpu-vegetation/" target="_blank">Open Spike 7 in a new tab ↗</a> · <a href="/blog/spike-source?spike=07-gpu-vegetation">View source</a></p>
<p>Spike 8 pushed terrain material complexity. Multi-layer blending weighted by slope angle and altitude, optional triplanar projection for cliff faces, and per-layer normal maps. The shader was doing slope-based splatting with four texture layers, each needing a diffuse and normal sample. That's <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="9.176ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 4056 759" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1722.4,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2500.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3556,0)"><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>4</mn><mo>×</mo><mn>2</mn><mo>=</mo><mn>8</mn></math></mjx-assistive-mml></mjx-container> texture fetches per fragment before you add any lighting, and the count grows linearly with layers, so a fifth layer means 10 fetches every pixel. We profiled on integrated Intel GPUs specifically to find the floor. The takeaway was that triplanar projection on vertical surfaces was worth the cost, but adding a fifth splat layer wasn't.</p>
<p><a href="/spikes/08-terrain-material/" target="_blank">Open Spike 8 in a new tab ↗</a> · <a href="/blog/spike-source?spike=08-terrain-material">View source</a></p>
<p>Spike 9 focused on cascaded shadow map cost under realistic terrain and object load. CSM with three cascades was baseline. We tested with low-angle sun positions specifically because that's where cascade pressure gets worst. The far cascade covers a huge frustum slice, and shadow detail is set by texel density, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.783ex;" xmlns="http://www.w3.org/2000/svg" width="7.095ex" height="3.009ex" role="img" focusable="false" viewBox="0 -983.7 3135.9 1330" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D70C" d="M58 -216Q25 -216 23 -186Q23 -176 73 26T127 234Q143 289 182 341Q252 427 341 441Q343 441 349 441T359 442Q432 442 471 394T510 276Q510 219 486 165T425 74T345 13T266 -10H255H248Q197 -10 165 35L160 41L133 -71Q108 -168 104 -181T92 -202Q76 -216 58 -216ZM424 322Q424 359 407 382T357 405Q322 405 287 376T231 300Q217 269 193 170L176 102Q193 26 260 26Q298 26 334 62Q367 92 389 158T418 266T424 322Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(794.8,0)"><path data-c="2248" d="M55 319Q55 360 72 393T114 444T163 472T205 482Q207 482 213 482T223 483Q262 483 296 468T393 413L443 381Q502 346 553 346Q609 346 649 375T694 454Q694 465 698 474T708 483Q722 483 722 452Q722 386 675 338T555 289Q514 289 468 310T388 357T308 404T224 426Q164 426 125 393T83 318Q81 289 69 289Q55 289 55 319ZM55 85Q55 126 72 159T114 210T163 238T205 248Q207 248 213 248T223 249Q262 249 296 234T393 179L443 147Q502 112 553 112Q609 112 649 141T694 220Q694 249 708 249T722 217Q722 153 675 104T555 55Q514 55 468 76T388 123T308 170T224 192Q164 192 125 159T83 84Q80 55 69 55Q55 55 55 85Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(1850.6,0)"><g data-mml-node="msup" transform="translate(220,394) scale(0.707)"><g data-mml-node="mi"><path data-c="1D445" d="M230 637Q203 637 198 638T193 649Q193 676 204 682Q206 683 378 683Q550 682 564 680Q620 672 658 652T712 606T733 563T739 529Q739 484 710 445T643 385T576 351T538 338L545 333Q612 295 612 223Q612 212 607 162T602 80V71Q602 53 603 43T614 25T640 16Q668 16 686 38T712 85Q717 99 720 102T735 105Q755 105 755 93Q755 75 731 36Q693 -21 641 -21H632Q571 -21 531 4T487 82Q487 109 502 166T517 239Q517 290 474 313Q459 320 449 321T378 323H309L277 193Q244 61 244 59Q244 55 245 54T252 50T269 48T302 46H333Q339 38 339 37T336 19Q332 6 326 0H311Q275 2 180 2Q146 2 117 2T71 2T50 1Q33 1 33 10Q33 12 36 24Q41 43 46 45Q50 46 61 46H67Q94 46 127 49Q141 52 146 61Q149 65 218 339T287 628Q287 635 230 637ZM630 554Q630 586 609 608T523 636Q521 636 500 636T462 637H440Q393 637 386 627Q385 624 352 494T319 361Q319 360 388 360Q466 361 492 367Q556 377 592 426Q608 449 619 486T630 554Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(792,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(377.5,-346.3) scale(0.707)"><path data-c="1D434" d="M208 74Q208 50 254 46Q272 46 272 35Q272 34 270 22Q267 8 264 4T251 0Q249 0 239 0T205 1T141 2Q70 2 50 0H42Q35 7 35 11Q37 38 48 46H62Q132 49 164 96Q170 102 345 401T523 704Q530 716 547 716H555H572Q578 707 578 706L606 383Q634 60 636 57Q641 46 701 46Q726 46 726 36Q726 34 723 22Q720 7 718 4T704 0Q701 0 690 0T651 1T578 2Q484 2 455 0H443Q437 6 437 9T439 27Q443 40 445 43L449 46H469Q523 49 533 63L521 213H283L249 155Q208 86 208 74ZM516 260Q516 271 504 416T490 562L463 519Q447 492 400 412L310 260L413 259Q516 259 516 260Z" style="stroke-width: 3;"/></g><rect width="1045.4" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>ρ</mi><mo>≈</mo><mfrac><msup><mi>R</mi><mn>2</mn></msup><mi>A</mi></mfrac></math></mjx-assistive-mml></mjx-container> (a shadow map of resolution <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.048ex;" xmlns="http://www.w3.org/2000/svg" width="1.717ex" height="1.593ex" role="img" focusable="false" viewBox="0 -683 759 704" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D445" d="M230 637Q203 637 198 638T193 649Q193 676 204 682Q206 683 378 683Q550 682 564 680Q620 672 658 652T712 606T733 563T739 529Q739 484 710 445T643 385T576 351T538 338L545 333Q612 295 612 223Q612 212 607 162T602 80V71Q602 53 603 43T614 25T640 16Q668 16 686 38T712 85Q717 99 720 102T735 105Q755 105 755 93Q755 75 731 36Q693 -21 641 -21H632Q571 -21 531 4T487 82Q487 109 502 166T517 239Q517 290 474 313Q459 320 449 321T378 323H309L277 193Q244 61 244 59Q244 55 245 54T252 50T269 48T302 46H333Q339 38 339 37T336 19Q332 6 326 0H311Q275 2 180 2Q146 2 117 2T71 2T50 1Q33 1 33 10Q33 12 36 24Q41 43 46 45Q50 46 61 46H67Q94 46 127 49Q141 52 146 61Q149 65 218 339T287 628Q287 635 230 637ZM630 554Q630 586 609 608T523 636Q521 636 500 636T462 637H440Q393 637 386 627Q385 624 352 494T319 361Q319 360 388 360Q466 361 492 367Q556 377 592 426Q608 449 619 486T630 554Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>R</mi></math></mjx-assistive-mml></mjx-container> stretched over ground area <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.697ex" height="1.62ex" role="img" focusable="false" viewBox="0 -716 750 716" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D434" d="M208 74Q208 50 254 46Q272 46 272 35Q272 34 270 22Q267 8 264 4T251 0Q249 0 239 0T205 1T141 2Q70 2 50 0H42Q35 7 35 11Q37 38 48 46H62Q132 49 164 96Q170 102 345 401T523 704Q530 716 547 716H555H572Q578 707 578 706L606 383Q634 60 636 57Q641 46 701 46Q726 46 726 36Q726 34 723 22Q720 7 718 4T704 0Q701 0 690 0T651 1T578 2Q484 2 455 0H443Q437 6 437 9T439 27Q443 40 445 43L449 46H469Q523 49 533 63L521 213H283L249 155Q208 86 208 74ZM516 260Q516 271 504 416T490 562L463 519Q447 492 400 412L310 260L413 259Q516 259 516 260Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>A</mi></math></mjx-assistive-mml></mjx-container>). One map over the whole view distance makes <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.697ex" height="1.62ex" role="img" focusable="false" viewBox="0 -716 750 716" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D434" d="M208 74Q208 50 254 46Q272 46 272 35Q272 34 270 22Q267 8 264 4T251 0Q249 0 239 0T205 1T141 2Q70 2 50 0H42Q35 7 35 11Q37 38 48 46H62Q132 49 164 96Q170 102 345 401T523 704Q530 716 547 716H555H572Q578 707 578 706L606 383Q634 60 636 57Q641 46 701 46Q726 46 726 36Q726 34 723 22Q720 7 718 4T704 0Q701 0 690 0T651 1T578 2Q484 2 455 0H443Q437 6 437 9T439 27Q443 40 445 43L449 46H469Q523 49 533 63L521 213H283L249 155Q208 86 208 74ZM516 260Q516 271 504 416T490 562L463 519Q447 492 400 412L310 260L413 259Q516 259 516 260Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>A</mi></math></mjx-assistive-mml></mjx-container> enormous and <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.489ex;" xmlns="http://www.w3.org/2000/svg" width="1.17ex" height="1.489ex" role="img" focusable="false" viewBox="0 -442 517 658" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D70C" d="M58 -216Q25 -216 23 -186Q23 -176 73 26T127 234Q143 289 182 341Q252 427 341 441Q343 441 349 441T359 442Q432 442 471 394T510 276Q510 219 486 165T425 74T345 13T266 -10H255H248Q197 -10 165 35L160 41L133 -71Q108 -168 104 -181T92 -202Q76 -216 58 -216ZM424 322Q424 359 407 382T357 405Q322 405 287 376T231 300Q217 269 193 170L176 102Q193 26 260 26Q298 26 334 62Q367 92 389 158T418 266T424 322Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>ρ</mi></math></mjx-assistive-mml></mjx-container> collapses. Cascades fix this by splitting the frustum so each slice gets its own <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.048ex;" xmlns="http://www.w3.org/2000/svg" width="6.2ex" height="1.593ex" role="img" focusable="false" viewBox="0 -683 2740.4 704" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D445" d="M230 637Q203 637 198 638T193 649Q193 676 204 682Q206 683 378 683Q550 682 564 680Q620 672 658 652T712 606T733 563T739 529Q739 484 710 445T643 385T576 351T538 338L545 333Q612 295 612 223Q612 212 607 162T602 80V71Q602 53 603 43T614 25T640 16Q668 16 686 38T712 85Q717 99 720 102T735 105Q755 105 755 93Q755 75 731 36Q693 -21 641 -21H632Q571 -21 531 4T487 82Q487 109 502 166T517 239Q517 290 474 313Q459 320 449 321T378 323H309L277 193Q244 61 244 59Q244 55 245 54T252 50T269 48T302 46H333Q339 38 339 37T336 19Q332 6 326 0H311Q275 2 180 2Q146 2 117 2T71 2T50 1Q33 1 33 10Q33 12 36 24Q41 43 46 45Q50 46 61 46H67Q94 46 127 49Q141 52 146 61Q149 65 218 339T287 628Q287 635 230 637ZM630 554Q630 586 609 608T523 636Q521 636 500 636T462 637H440Q393 637 386 627Q385 624 352 494T319 361Q319 360 388 360Q466 361 492 367Q556 377 592 426Q608 449 619 486T630 554Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(981.2,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1981.4,0)"><path data-c="1D445" d="M230 637Q203 637 198 638T193 649Q193 676 204 682Q206 683 378 683Q550 682 564 680Q620 672 658 652T712 606T733 563T739 529Q739 484 710 445T643 385T576 351T538 338L545 333Q612 295 612 223Q612 212 607 162T602 80V71Q602 53 603 43T614 25T640 16Q668 16 686 38T712 85Q717 99 720 102T735 105Q755 105 755 93Q755 75 731 36Q693 -21 641 -21H632Q571 -21 531 4T487 82Q487 109 502 166T517 239Q517 290 474 313Q459 320 449 321T378 323H309L277 193Q244 61 244 59Q244 55 245 54T252 50T269 48T302 46H333Q339 38 339 37T336 19Q332 6 326 0H311Q275 2 180 2Q146 2 117 2T71 2T50 1Q33 1 33 10Q33 12 36 24Q41 43 46 45Q50 46 61 46H67Q94 46 127 49Q141 52 146 61Q149 65 218 339T287 628Q287 635 230 637ZM630 554Q630 586 609 608T523 636Q521 636 500 636T462 637H440Q393 637 386 627Q385 624 352 494T319 361Q319 360 388 360Q466 361 492 367Q556 377 592 426Q608 449 619 486T630 554Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>R</mi><mo>×</mo><mi>R</mi></math></mjx-assistive-mml></mjx-container> map over a small <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.697ex" height="1.62ex" role="img" focusable="false" viewBox="0 -716 750 716" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D434" d="M208 74Q208 50 254 46Q272 46 272 35Q272 34 270 22Q267 8 264 4T251 0Q249 0 239 0T205 1T141 2Q70 2 50 0H42Q35 7 35 11Q37 38 48 46H62Q132 49 164 96Q170 102 345 401T523 704Q530 716 547 716H555H572Q578 707 578 706L606 383Q634 60 636 57Q641 46 701 46Q726 46 726 36Q726 34 723 22Q720 7 718 4T704 0Q701 0 690 0T651 1T578 2Q484 2 455 0H443Q437 6 437 9T439 27Q443 40 445 43L449 46H469Q523 49 533 63L521 213H283L249 155Q208 86 208 74ZM516 260Q516 271 504 416T490 562L463 519Q447 492 400 412L310 260L413 259Q516 259 516 260Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>A</mi></math></mjx-assistive-mml></mjx-container>, keeping <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.489ex;" xmlns="http://www.w3.org/2000/svg" width="1.17ex" height="1.489ex" role="img" focusable="false" viewBox="0 -442 517 658" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D70C" d="M58 -216Q25 -216 23 -186Q23 -176 73 26T127 234Q143 289 182 341Q252 427 341 441Q343 441 349 441T359 442Q432 442 471 394T510 276Q510 219 486 165T425 74T345 13T266 -10H255H248Q197 -10 165 35L160 41L133 -71Q108 -168 104 -181T92 -202Q76 -216 58 -216ZM424 322Q424 359 407 382T357 405Q322 405 287 376T231 300Q217 269 193 170L176 102Q193 26 260 26Q298 26 334 62Q367 92 389 158T418 266T424 322Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>ρ</mi></math></mjx-assistive-mml></mjx-container> roughly constant from near to far. We measured the GPU time difference between two and four cascades, then between 1024 and 2048 shadow map resolution. The result was that three cascades at 1024 gave us acceptable contact shadows near the camera without exceeding 2ms of GPU time on our target hardware.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/09-csm-shadows/" title="Spike 9 CSM Shadows" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/09-csm-shadows/" target="_blank">Open Spike 9 in a new tab ↗</a> · <a href="/blog/spike-source?spike=09-csm-shadows">View source</a></p>
<p>The hard part in this phase was product discipline. Some effects looked excellent and still had to be constrained because they consumed too much of the frame budget relative to their visual impact.</p>
<p>Our rule became simple. A feature moves forward only if it can explain its cost with measured frame-time data.</p>
<p>That sounds obvious. It's not common in fast prototype cycles where everyone is excited about the next visual win. Keeping this rule early made architecture decisions around clipmaps and volumetric zones much cleaner later, because we already knew the per-feature cost of everything competing for the same 16ms.</p>
<p>In part 6 we hit the first major terrain architecture pivot with geometry clipmaps.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>InstancedMesh and GPU vegetation.</strong> Three.js's <code>InstancedMesh</code> renders N copies of the same geometry with one draw call. For vegetation, a density map (32x32 per chunk) drives runtime scattering of grass blades and shrub clusters into instance buffers. Wind animation runs in the vertex shader using a scrolling noise texture. At scale, WebGPU's <code>ComputeInstanceCulling</code> eliminates off-screen and distant instances before rasterization, and <code>IndirectBatchedMesh</code> packs multiple vegetation types into a single buffer drawn with multi-draw indirect. See our <a href="/guides/landscape-generation-browser.html#gpu-driven-vegetation-culling">landscape guide on GPU vegetation culling</a>.</p>
<p><strong>Triplanar mapping.</strong> Standard UV-mapped textures stretch on steep slopes because UV coordinates compress. Triplanar mapping projects textures along all three axes (X, Y, Z) and blends based on the surface normal. Cliff faces get the X or Z projection (no stretching), flat ground gets the Y projection. The blending is smooth and automatic with no UV unwrapping required. For PBR terrain, the same blending weights apply to albedo, normal, roughness, and ambient occlusion channels. See <a href="/guides/landscape-generation-browser.html#triplanar-mapping">triplanar mapping details</a>.</p>
<p><strong>Slope and altitude-based material splatting.</strong> Instead of hand-painted splat maps, materials are assigned procedurally in the fragment shader based on terrain properties. Flat ground at low altitude gets grass, steep slopes get rock, high altitude gets snow (only on surfaces flat enough for accumulation), near sea level gets sand. The transitions use <code>smoothstep</code> for smooth blending. In our implementation, each terrain chunk evaluates four texture layers with diffuse and normal samples per layer, resulting in eight texture fetches per fragment before lighting. See <a href="/guides/landscape-generation-browser.html#slope-and-altitude-based-material-assignment">slope and altitude material assignment</a>.</p>
<p><strong>Cascaded Shadow Maps (CSM).</strong> CSM splits the camera's view frustum into 3-4 distance ranges (cascades). Each cascade renders a shadow map from the sun's perspective at a resolution matched to its distance. Close cascades get high-resolution shadows (detailed contact shadows under trees and buildings), far cascades get lower resolution (broad mountain shadows). The terrain shader samples all cascades and selects the appropriate one per fragment. Performance cost: 3-4 cascades at 1024x1024 add ~0.5-1 ms for shadow map rendering plus ~0.2-0.3 ms for sampling. See <a href="/guides/landscape-generation-browser.html#shadows-for-terrain">shadows for terrain</a>.</p>
<hr>
<p>Part 5 of 12.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-04-streaming-before-fancy.html">Part 4 - Streaming before fancy terrain</a><br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-06-clipmaps.html">Part 6 - Clipmaps changed the plot</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 6: Clipmaps changed the plot]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-06-clipmaps</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-06-clipmaps</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Geometry clipmaps gave us a way to keep terrain cost predictable while moving through a much larger world.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-6-clipmaps-changed-the-plot" tabindex="-1">Building an open world in the browser, part 6: Clipmaps changed the plot <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-6-clipmaps-changed-the-plot" aria-label="Permalink to &quot;Building an open world in the browser, part 6: Clipmaps changed the plot&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>Before Spike 10, our mental model was still &quot;bigger world means more geometry.&quot; After Spike 10, the model became &quot;constant geometry budget, camera-centered ring updates.&quot; That shift changed the project trajectory.</p>
<p>The idea behind geometry clipmaps is straightforward. You render terrain as a set of concentric rings centered on the camera. The innermost ring has the highest vertex density. Each ring <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.179ex" height="1.595ex" role="img" focusable="false" viewBox="0 -694 521 705" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>k</mi></math></mjx-assistive-mml></mjx-container> outward doubles the vertex spacing, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.375ex;" xmlns="http://www.w3.org/2000/svg" width="10.935ex" height="2.306ex" role="img" focusable="false" viewBox="0 -853.7 4833.4 1019.3" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(502,-150) scale(0.707)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1198.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2254,0)"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(502,-150) scale(0.707)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3381.7,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(3882,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(533,363) scale(0.707)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>s</mi><mi>k</mi></msub><mo>=</mo><msub><mi>s</mi><mn>0</mn></msub><mo>⋅</mo><msup><mn>2</mn><mi>k</mi></msup></math></mjx-assistive-mml></mjx-container>, so it covers <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="2.891ex" height="1.532ex" role="img" focusable="false" viewBox="0 -677 1278 677" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(500,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>4</mn><mo>×</mo></math></mjx-assistive-mml></mjx-container> the ground area of the ring inside it.</p>
<p>That doubling is the whole trick. View distance grows geometrically with the number of rings, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.375ex;" xmlns="http://www.w3.org/2000/svg" width="13.628ex" height="2.288ex" role="img" focusable="false" viewBox="0 -846 6023.5 1011.5" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(553,-150) scale(0.707)"><path data-c="76" d="M338 431Q344 429 422 429Q479 429 503 431H508V385H497Q439 381 423 345Q421 341 356 172T288 -2Q283 -11 263 -11Q244 -11 239 -2Q99 359 98 364Q93 378 82 381T43 385H19V431H25L33 430Q41 430 53 430T79 430T104 429T122 428Q217 428 232 431H240V385H226Q187 384 184 370Q184 366 235 234L286 102L377 341V349Q377 363 367 372T349 383T335 385H331V431H338Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(528,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(806,0)" style="stroke-width: 3;"/><path data-c="77" d="M90 368Q84 378 76 380T40 385H18V431H24L43 430Q62 430 84 429T116 428Q206 428 221 431H229V385H215Q177 383 177 368Q177 367 221 239L265 113L339 328L333 345Q323 374 316 379Q308 384 278 385H258V431H264Q270 428 348 428Q439 428 454 431H461V385H452Q404 385 404 369Q404 366 418 324T449 234T481 143L496 100L537 219Q579 341 579 347Q579 363 564 373T530 385H522V431H529Q541 428 624 428Q692 428 698 431H703V385H697Q696 385 691 385T682 384Q635 377 619 334L559 161Q546 124 528 71Q508 12 503 1T487 -11H479Q460 -11 456 -4Q455 -3 407 133L361 267Q359 263 266 -4Q261 -11 243 -11H238Q225 -11 220 -3L90 368Z" transform="translate(1250,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2275.2,0)"><path data-c="2248" d="M55 319Q55 360 72 393T114 444T163 472T205 482Q207 482 213 482T223 483Q262 483 296 468T393 413L443 381Q502 346 553 346Q609 346 649 375T694 454Q694 465 698 474T708 483Q722 483 722 452Q722 386 675 338T555 289Q514 289 468 310T388 357T308 404T224 426Q164 426 125 393T83 318Q81 289 69 289Q55 289 55 319ZM55 85Q55 126 72 159T114 210T163 238T205 248Q207 248 213 248T223 249Q262 249 296 234T393 179L443 147Q502 112 553 112Q609 112 649 141T694 220Q694 249 708 249T722 217Q722 153 675 104T555 55Q514 55 468 76T388 123T308 170T224 192Q164 192 125 159T83 84Q80 55 69 55Q55 55 55 85Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3331,0)"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(502,-150) scale(0.707)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(4458.7,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(4959,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>d</mi><mtext>view</mtext></msub><mo>≈</mo><msub><mi>s</mi><mn>0</mn></msub><mo>⋅</mo><msup><mn>2</mn><mrow data-mjx-texclass="ORD"><mi>L</mi></mrow></msup></math></mjx-assistive-mml></mjx-container>, while the vertex cost grows only linearly:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.357ex;" xmlns="http://www.w3.org/2000/svg" width="14.108ex" height="2.357ex" role="img" focusable="false" viewBox="0 -883.9 6235.6 1041.7" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D449" d="M52 648Q52 670 65 683H76Q118 680 181 680Q299 680 320 683H330Q336 677 336 674T334 656Q329 641 325 637H304Q282 635 274 635Q245 630 242 620Q242 618 271 369T301 118L374 235Q447 352 520 471T595 594Q599 601 599 609Q599 633 555 637Q537 637 537 648Q537 649 539 661Q542 675 545 679T558 683Q560 683 570 683T604 682T668 681Q737 681 755 683H762Q769 676 769 672Q769 655 760 640Q757 637 743 637Q730 636 719 635T698 630T682 623T670 615T660 608T652 599T645 592L452 282Q272 -9 266 -16Q263 -18 259 -21L241 -22H234Q216 -22 216 -15Q213 -9 177 305Q139 623 138 626Q133 637 76 637H59Q52 642 52 648Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(616,-150) scale(0.707)"><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(389,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(889,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(1278,0)" style="stroke-width: 3;"/><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" transform="translate(1778,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2397.6,0)"><path data-c="2248" d="M55 319Q55 360 72 393T114 444T163 472T205 482Q207 482 213 482T223 483Q262 483 296 468T393 413L443 381Q502 346 553 346Q609 346 649 375T694 454Q694 465 698 474T708 483Q722 483 722 452Q722 386 675 338T555 289Q514 289 468 310T388 357T308 404T224 426Q164 426 125 393T83 318Q81 289 69 289Q55 289 55 319ZM55 85Q55 126 72 159T114 210T163 238T205 248Q207 248 213 248T223 249Q262 249 296 234T393 179L443 147Q502 112 553 112Q609 112 649 141T694 220Q694 249 708 249T722 217Q722 153 675 104T555 55Q514 55 468 76T388 123T308 170T224 192Q164 192 125 159T83 84Q80 55 69 55Q55 55 55 85Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(3453.4,0)"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(975.3,413) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(5054.4,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5554.6,0)"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><msub><mi>V</mi><mtext>total</mtext></msub><mo>≈</mo><msup><mi>N</mi><mn>2</mn></msup><mo>⋅</mo><mi>L</mi></math></mjx-assistive-mml></mjx-container><p>for <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.541ex" height="1.545ex" role="img" focusable="false" viewBox="0 -683 681 683" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>L</mi></math></mjx-assistive-mml></mjx-container> rings of <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="6.784ex" height="1.545ex" role="img" focusable="false" viewBox="0 -683 2998.4 683" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1110.2,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2110.4,0)"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>N</mi><mo>×</mo><mi>N</mi></math></mjx-assistive-mml></mjx-container> vertices each. Doubling how far you can see costs one more ring, not four times the geometry. The triangle count stays roughly constant no matter the world size, because you always render the same <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.541ex" height="1.545ex" role="img" focusable="false" viewBox="0 -683 681 683" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>L</mi></math></mjx-assistive-mml></mjx-container> rings at the same resolution.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/10-clipmap-geomorph/" title="Spike 10 Geometry Clipmaps Geomorphing" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/10-clipmap-geomorph/" target="_blank">Open Spike 10 in a new tab ↗</a> · <a href="/blog/spike-source?spike=10-clipmap-geomorph">View source</a></p>
<p>The practical trick was geomorphing at ring boundaries. When a vertex transitions from one LOD ring to the next, its height has to blend smoothly between the high-res and low-res sample. Without that, you get visible pops every time the camera moves and rings shift. We handled this with a blend factor based on the vertex's distance to the ring edge, interpolating height in the vertex shader:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="58.451ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 25835.3 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(853.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1909.6,0)"><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(278,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(722,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(1114,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3579.6,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3968.6,0)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(609,-150) scale(0.707)"><path data-c="66" d="M273 0Q255 3 146 3Q43 3 34 0H26V46H42Q70 46 91 49Q99 52 103 60Q104 62 104 224V385H33V431H104V497L105 564L107 574Q126 639 171 668T266 704Q267 704 275 704T289 705Q330 702 351 679T372 627Q372 604 358 590T321 576T284 590T270 627Q270 647 288 667H284Q280 668 273 668Q245 668 223 647T189 592Q183 572 182 497V431H293V385H185V225Q185 63 186 61T189 57T194 54T199 51T206 49T213 48T222 47T231 47T241 46T251 46H282V0H273Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(306,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(584,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(1140,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(5747.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(6025.6,0)"><g data-mml-node="mspace"/></g><g data-mml-node="msub" transform="translate(6359.3,0)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(609,-150) scale(0.707)"><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(444,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(944,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(1444,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(1836,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(2230,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(8909.1,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(9187.1,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(9520.7,0)"><path data-c="1D6FC" d="M34 156Q34 270 120 356T309 442Q379 442 421 402T478 304Q484 275 485 237V208Q534 282 560 374Q564 388 566 390T582 393Q603 393 603 385Q603 376 594 346T558 261T497 161L486 147L487 123Q489 67 495 47T514 26Q528 28 540 37T557 60Q559 67 562 68T577 70Q597 70 597 62Q597 56 591 43Q579 19 556 5T512 -10H505Q438 -10 414 62L411 69L400 61Q390 53 370 41T325 18T267 -2T203 -11Q124 -11 79 39T34 156ZM208 26Q257 26 306 47T379 90L403 112Q401 255 396 290Q382 405 304 405Q235 405 183 332Q156 292 139 224T121 120Q121 71 146 49T208 26Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(10160.7,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(10549.7,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(10827.7,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(12994.4,0)"><path data-c="1D6FC" d="M34 156Q34 270 120 356T309 442Q379 442 421 402T478 304Q484 275 485 237V208Q534 282 560 374Q564 388 566 390T582 393Q603 393 603 385Q603 376 594 346T558 261T497 161L486 147L487 123Q489 67 495 47T514 26Q528 28 540 37T557 60Q559 67 562 68T577 70Q597 70 597 62Q597 56 591 43Q579 19 556 5T512 -10H505Q438 -10 414 62L411 69L400 61Q390 53 370 41T325 18T267 -2T203 -11Q124 -11 79 39T34 156ZM208 26Q257 26 306 47T379 90L403 112Q401 255 396 290Q382 405 304 405Q235 405 183 332Q156 292 139 224T121 120Q121 71 146 49T208 26Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(13912.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(14968,0)"><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" style="stroke-width: 3;"/><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(394,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(1227,0)" style="stroke-width: 3;"/><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(1727,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(2227,0)" style="stroke-width: 3;"/><path data-c="68" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 124T102 167T103 217T103 272T103 329Q103 366 103 407T103 482T102 542T102 586T102 603Q99 622 88 628T43 637H25V660Q25 683 27 683L37 684Q47 685 66 686T103 688Q120 689 140 690T170 693T181 694H184V367Q244 442 328 442Q451 442 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(2616,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(3172,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(3566,0)" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(3955,0)" style="stroke-width: 3;"/><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(4399,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(19923,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(20312,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(553,-150) scale(0.707)"><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(1500,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(22252.8,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(22530.8,0)"><g data-mml-node="mspace"/></g><g data-mml-node="msub" transform="translate(22864.5,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(553,-150) scale(0.707)"><path data-c="66" d="M273 0Q255 3 146 3Q43 3 34 0H26V46H42Q70 46 91 49Q99 52 103 60Q104 62 104 224V385H33V431H104V497L105 564L107 574Q126 639 171 668T266 704Q267 704 275 704T289 705Q330 702 351 679T372 627Q372 604 358 590T321 576T284 590T270 627Q270 647 288 667H284Q280 668 273 668Q245 668 223 647T189 592Q183 572 182 497V431H293V385H185V225Q185 63 186 61T189 57T194 54T199 51T206 49T213 48T222 47T231 47T241 46T251 46H282V0H273Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(306,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(806,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(24314.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(24592.6,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(24926.3,0)"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(25446.3,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>h</mi><mo>=</mo><mi>lerp</mi><mo stretchy="false">(</mo><msub><mi>h</mi><mtext>fine</mtext></msub><mo>,</mo><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><msub><mi>h</mi><mtext>coarse</mtext></msub><mo>,</mo><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mi>α</mi><mo stretchy="false">)</mo><mo>,</mo><mstyle scriptlevel="0"><mspace width="2em"/></mstyle><mi>α</mi><mo>=</mo><mi>smoothstep</mi><mo stretchy="false">(</mo><msub><mi>d</mi><mtext>near</mtext></msub><mo>,</mo><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><msub><mi>d</mi><mtext>far</mtext></msub><mo>,</mo><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mi>d</mi><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container><p>where <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.023ex;" xmlns="http://www.w3.org/2000/svg" width="1.176ex" height="1.593ex" role="img" focusable="false" viewBox="0 -694 520 704" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>d</mi></math></mjx-assistive-mml></mjx-container> is the vertex's distance from the camera. Inside the ring (<mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.357ex;" xmlns="http://www.w3.org/2000/svg" width="8.585ex" height="1.927ex" role="img" focusable="false" viewBox="0 -694 3794.4 851.8" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(797.8,0)"><path data-c="2264" d="M674 636Q682 636 688 630T694 615T687 601Q686 600 417 472L151 346L399 228Q687 92 691 87Q694 81 694 76Q694 58 676 56H670L382 192Q92 329 90 331Q83 336 83 348Q84 359 96 365Q104 369 382 500T665 634Q669 636 674 636ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(1853.6,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(553,-150) scale(0.707)"><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(556,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(1500,0)" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>d</mi><mo>≤</mo><msub><mi>d</mi><mtext>near</mtext></msub></math></mjx-assistive-mml></mjx-container>) the vertex uses its full-resolution height; by the time it reaches the next ring (<mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.357ex;" xmlns="http://www.w3.org/2000/svg" width="7.474ex" height="1.927ex" role="img" focusable="false" viewBox="0 -694 3303.7 851.8" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(797.8,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(1853.6,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(553,-150) scale(0.707)"><path data-c="66" d="M273 0Q255 3 146 3Q43 3 34 0H26V46H42Q70 46 91 49Q99 52 103 60Q104 62 104 224V385H33V431H104V497L105 564L107 574Q126 639 171 668T266 704Q267 704 275 704T289 705Q330 702 351 679T372 627Q372 604 358 590T321 576T284 590T270 627Q270 647 288 667H284Q280 668 273 668Q245 668 223 647T189 592Q183 572 182 497V431H293V385H185V225Q185 63 186 61T189 57T194 54T199 51T206 49T213 48T222 47T231 47T241 46T251 46H282V0H273Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(306,0)" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(806,0)" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>d</mi><mo>≥</mo><msub><mi>d</mi><mtext>far</mtext></msub></math></mjx-assistive-mml></mjx-container>) it has already migrated to the coarse height that ring will use, so there's nothing left to pop.</p>
<p>One subtle lesson came from camera movement testing. It's easy to judge clipmaps in static screenshots and miss transition artifacts. We spent time running constant-speed traversals through ring boundaries and watching for temporal noise. Screenshots lied. Motion told the truth.</p>
<p>This spike also gave us a clean architectural boundary. Near-field terrain could become dynamic and expensive over time, with volumetric editing, higher material complexity, and physics interaction. Far-field terrain could stay stable, predictable, and cheap. That separation became the backbone of every architectural decision from this point forward.</p>
<p>If you're evaluating clipmaps for your own project, test stress loops, not beauty shots. Long traversal paths, changing camera altitude, and repeated boundary crossings are what expose the real problems.</p>
<p>In part 7 we add volumetric meshing and move from &quot;terrain as a surface&quot; to &quot;terrain as editable volume.&quot; That was the point where this project stopped being a renderer and started becoming a world editor.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Geometry clipmaps.</strong> Introduced by Losasso and Hoppe at SIGGRAPH 2004 (<a href="https://hhoppe.com/geomclipmap.pdf" target="_blank" rel="noreferrer">paper</a>), geometry clipmaps render terrain as concentric square rings centered on the camera. Each ring is twice the area of the previous one at half the vertex resolution. The total vertex count is constant: roughly <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="6.295ex" height="1.887ex" role="img" focusable="false" viewBox="0 -833.9 2782.2 833.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(975.3,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1601,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2101.2,0)"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>N</mi><mn>2</mn></msup><mo>⋅</mo><mi>L</mi></math></mjx-assistive-mml></mjx-container>. With <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="8.42ex" height="1.731ex" role="img" focusable="false" viewBox="0 -683 3721.6 765" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1165.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2221.6,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>N</mi><mo>=</mo><mn>256</mn></math></mjx-assistive-mml></mjx-container> and <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.689ex" height="1.731ex" role="img" focusable="false" viewBox="0 -683 2514.6 765" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(958.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2014.6,0)"><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>L</mi><mo>=</mo><mn>8</mn></math></mjx-assistive-mml></mjx-container> levels, that's <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.439ex;" xmlns="http://www.w3.org/2000/svg" width="18.712ex" height="2.394ex" role="img" focusable="false" viewBox="0 -864 8270.6 1058" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1533,393.1) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2158.8,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3159,0)"><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3936.8,0)"><path data-c="2248" d="M55 319Q55 360 72 393T114 444T163 472T205 482Q207 482 213 482T223 483Q262 483 296 468T393 413L443 381Q502 346 553 346Q609 346 649 375T694 454Q694 465 698 474T708 483Q722 483 722 452Q722 386 675 338T555 289Q514 289 468 310T388 357T308 404T224 426Q164 426 125 393T83 318Q81 289 69 289Q55 289 55 319ZM55 85Q55 126 72 159T114 210T163 238T205 248Q207 248 213 248T223 249Q262 249 296 234T393 179L443 147Q502 112 553 112Q609 112 649 141T694 220Q694 249 708 249T722 217Q722 153 675 104T555 55Q514 55 468 76T388 123T308 170T224 192Q164 192 125 159T83 84Q80 55 69 55Q55 55 55 85Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(4992.6,0)"><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" style="stroke-width: 3;"/><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" transform="translate(1000,0)" style="stroke-width: 3;"/><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" transform="translate(1500,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(1778,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(2278,0)" style="stroke-width: 3;"/><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(2778,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mn>256</mn><mn>2</mn></msup><mo>×</mo><mn>8</mn><mo>≈</mo><mn>524,000</mn></math></mjx-assistive-mml></mjx-container> vertices regardless of world size. The CPU updates heightmap data for each ring as the camera moves. The vertex shader reads height from a texture and displaces the flat grid. See our <a href="/guides/landscape-generation-browser.html#geometry-clipmaps">landscape guide on geometry clipmaps</a> and <a href="https://developer.nvidia.com/gpugems/gpugems2/part-i-geometric-complexity/chapter-2-terrain-rendering-using-gpu-based-geometry" target="_blank" rel="noreferrer">GPU Gems 2, Chapter 2</a>.</p>
<p><strong>Geomorphing.</strong> The biggest visual artifact in terrain LOD is popping: vertices suddenly jump when a patch switches LOD level. Geomorphing eliminates this by blending vertex positions between LOD levels over a transition zone. Each vertex stores both its current-LOD height and its coarser-LOD height. A morph factor based on camera distance smoothly interpolates between them: <code>morphedHeight = mix(fineLodHeight, coarseLodHeight, smoothstep(lodNear, lodFar, distance))</code>. The transition zone is typically the outer 20% of each ring. At normal camera speeds, the transition is invisible. See <a href="/guides/landscape-generation-browser.html#geomorphing-pop-free-lod-transitions">geomorphing details</a>.</p>
<p><strong>CDLOD (Quadtree-Adaptive Clipmaps).</strong> An improvement on fixed-ring clipmaps by Strugar (2014, <a href="https://www.vertexasylum.com/CDLOD/cdlod_latest.pdf" target="_blank" rel="noreferrer">paper</a>). Instead of concentric rings with uniform resolution, CDLOD uses a quadtree that adapts to terrain complexity. Flat areas use coarse nodes, while areas with high detail (cliffs, ridges) get finer subdivision. This matters for creator worlds where different chunks have vastly different complexity. See <a href="/guides/landscape-generation-browser.html#cdlod-quadtree-adaptive-clipmaps">CDLOD in our landscape guide</a>.</p>
<hr>
<p>Part 6 of 12.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-05-budgeting-the-pretty.html">Part 5 - Budgeting the pretty stuff</a><br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-07-marching-cubes.html">Part 7 - Marching cubes and the first real caves</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 7: Marching cubes and the first real caves]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-07-marching-cubes</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-07-marching-cubes</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 12 brought real-time WebGPU marching cubes into the project and changed what terrain could be.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-7-marching-cubes-and-the-first-real-caves" tabindex="-1">Building an open world in the browser, part 7: Marching cubes and the first real caves <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-7-marching-cubes-and-the-first-real-caves" aria-label="Permalink to &quot;Building an open world in the browser, part 7: Marching cubes and the first real caves&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>Heightmaps are great until you need overhangs.</p>
<p>The moment you want carved tunnels, floating rock lips, or cave ceilings, a pure heightfield pipeline starts blocking you. A heightmap stores one Y value per XZ coordinate. It's physically incapable of representing any surface that folds back over itself. We needed a volumetric representation.</p>
<p>Spike 12 implemented marching cubes on the GPU using WebGPU compute shaders. The algorithm evaluates a signed distance field (SDF) on a 3D grid and extracts a triangle mesh at the zero-crossing surface. Each cell has 8 corners, each either inside or outside the surface, which gives <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="8.53ex" height="2.072ex" role="img" focusable="false" viewBox="0 -833.9 3770.1 915.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(533,363) scale(0.707)"><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1214.3,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2270.1,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mn>2</mn><mn>8</mn></msup><mo>=</mo><mn>256</mn></math></mjx-assistive-mml></mjx-container> possible sign patterns. A lookup table maps each pattern to a set of triangles. Vertices sit on the cell edges at the point where the field crosses zero. For an edge between corners <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.023ex;" xmlns="http://www.w3.org/2000/svg" width="1.197ex" height="1.02ex" role="img" focusable="false" viewBox="0 -441 529 451" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>a</mi></math></mjx-assistive-mml></mjx-container> and <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="0.971ex" height="1.595ex" role="img" focusable="false" viewBox="0 -694 429 705" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>b</mi></math></mjx-assistive-mml></mjx-container> with field values <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="2.143ex" height="2.059ex" role="img" focusable="false" viewBox="0 -705 947.1 910" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(523,-150) scale(0.707)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>f</mi><mi>a</mi></msub></math></mjx-assistive-mml></mjx-container> and <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="1.983ex" height="2.059ex" role="img" focusable="false" viewBox="0 -705 876.3 910" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(523,-150) scale(0.707)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>f</mi><mi>b</mi></msub></math></mjx-assistive-mml></mjx-container>, linear interpolation places the vertex at</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -2.016ex;" xmlns="http://www.w3.org/2000/svg" width="37.407ex" height="5.14ex" role="img" focusable="false" viewBox="0 -1381 16534 2272" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D42F" d="M401 444Q413 441 495 441Q568 441 574 444H580V382H510L409 156Q348 18 339 6Q331 -4 320 -4Q318 -4 313 -4T303 -3H288Q273 -3 264 12T221 102Q206 135 197 156L96 382H26V444H34Q49 441 145 441Q252 441 270 444H279V382H231L284 264Q335 149 338 149Q338 150 389 264T442 381Q442 382 418 382H394V444H401Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(884.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(1940.6,0)"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D429" d="M32 442L123 446Q214 450 215 450H221V409Q222 409 229 413T251 423T284 436T328 446T382 450Q480 450 540 388T600 223Q600 128 539 61T361 -6H354Q292 -6 236 28L227 34V-132H296V-194H287Q269 -191 163 -191Q56 -191 38 -194H29V-132H98V113V284Q98 330 97 348T93 370T83 376Q69 380 42 380H29V442H32ZM457 224Q457 303 427 349T350 395Q282 395 235 352L227 345V104L233 97Q274 45 337 45Q383 45 420 86T457 224Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(672,-150) scale(0.707)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3258.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4259.1,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(4620.1,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mo" transform="translate(4787.1,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(5176.1,0)"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D429" d="M32 442L123 446Q214 450 215 450H221V409Q222 409 229 413T251 423T284 436T328 446T382 450Q480 450 540 388T600 223Q600 128 539 61T361 -6H354Q292 -6 236 28L227 34V-132H296V-194H287Q269 -191 163 -191Q56 -191 38 -194H29V-132H98V113V284Q98 330 97 348T93 370T83 376Q69 380 42 380H29V442H32ZM457 224Q457 303 427 349T350 395Q282 395 235 352L227 345V104L233 97Q274 45 337 45Q383 45 420 86T457 224Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(672,-150) scale(0.707)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(6423.6,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(7423.9,0)"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D429" d="M32 442L123 446Q214 450 215 450H221V409Q222 409 229 413T251 423T284 436T328 446T382 450Q480 450 540 388T600 223Q600 128 539 61T361 -6H354Q292 -6 236 28L227 34V-132H296V-194H287Q269 -191 163 -191Q56 -191 38 -194H29V-132H98V113V284Q98 330 97 348T93 370T83 376Q69 380 42 380H29V442H32ZM457 224Q457 303 427 349T350 395Q282 395 235 352L227 345V104L233 97Q274 45 337 45Q383 45 420 86T457 224Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(672,-150) scale(0.707)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(8519.9,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(8908.9,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(9186.9,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(11353.6,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(11992.4,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(13048.1,0)"><g data-mml-node="mrow" transform="translate(880.4,676)"><g data-mml-node="mo"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(778,0)"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(523,-150) scale(0.707)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mrow" transform="translate(220,-686)"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(523,-150) scale(0.707)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1098.6,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2098.8,0)"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(523,-150) scale(0.707)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g></g><rect width="3245.9" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">v</mi></mrow><mo>=</mo><msub><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">p</mi></mrow><mi>a</mi></msub><mo>+</mo><mi>t</mi><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mo stretchy="false">(</mo><msub><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">p</mi></mrow><mi>b</mi></msub><mo>−</mo><msub><mrow data-mjx-texclass="ORD"><mi mathvariant="bold">p</mi></mrow><mi>a</mi></msub><mo stretchy="false">)</mo><mo>,</mo><mstyle scriptlevel="0"><mspace width="2em"/></mstyle><mi>t</mi><mo>=</mo><mfrac><mrow><mo>−</mo><msub><mi>f</mi><mi>a</mi></msub></mrow><mrow><msub><mi>f</mi><mi>b</mi></msub><mo>−</mo><msub><mi>f</mi><mi>a</mi></msub></mrow></mfrac></math></mjx-assistive-mml></mjx-container><p>which is the fraction along the edge where the field hits zero. Because <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="2.143ex" height="2.059ex" role="img" focusable="false" viewBox="0 -705 947.1 910" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(523,-150) scale(0.707)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>f</mi><mi>a</mi></msub></math></mjx-assistive-mml></mjx-container> and <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="1.983ex" height="2.059ex" role="img" focusable="false" viewBox="0 -705 876.3 910" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(523,-150) scale(0.707)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>f</mi><mi>b</mi></msub></math></mjx-assistive-mml></mjx-container> have opposite signs on a crossing edge, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="0.817ex" height="1.441ex" role="img" focusable="false" viewBox="0 -626 361 637" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>t</mi></math></mjx-assistive-mml></mjx-container> always lands in <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="4.526ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 2000.7 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(278,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(778,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1222.7,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1722.7,0)"><path data-c="5D" d="M22 710V750H159V-250H22V-210H119V710H22Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">[</mo><mn>0</mn><mo>,</mo><mn>1</mn><mo stretchy="false">]</mo></math></mjx-assistive-mml></mjx-container>. We ran this on four active 64-cubed chunks simultaneously and tested animated SDF edits with per-frame remesh.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/12-webgpu-marching-cubes/" title="Spike 12 WebGPU Marching Cubes" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/12-webgpu-marching-cubes/" target="_blank">Open Spike 12 in a new tab ↗</a> · <a href="/blog/spike-source?spike=12-webgpu-marching-cubes">View source</a></p>
<p>The first win was confidence in the compute pipeline itself. A single dispatch could evaluate the SDF, classify cells, and emit vertices into a GPU buffer without any CPU readback. The second win was discovering how fast &quot;it works&quot; turns into artifact hunting. Missing triangles were rarely a marching cubes theory problem. They were table index mismatches, incorrect draw ranges reading past the active vertex count, or edge-case interactions near chunk boundaries where neighboring SDF samples weren't available.</p>
<p>This spike forced us to think in zones. Near the camera, you want volumetric freedom so players can carve, dig, and see caves. Far from the camera, you want clipmap efficiency where a flat heightmap is cheaper and perfectly adequate. That duality became the backbone of the architecture we kept refining from Spike 13 onward.</p>
<p>One of my favorite debugging moments was the wireframe toggle while edits were running. Watching topology form and dissolve in real time made quality tradeoffs immediately visible. You could see where vertex density was high enough, where it got too coarse, and exactly where LOD transitions would eventually need Transvoxel support to avoid cracks.</p>
<p>In part 8 we cover the integration challenge. Keeping raw compute-driven meshes and Three.js scene graph logic in one stable rendering pipeline was harder than the isolated demo suggested.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Marching cubes.</strong> An algorithm for extracting a triangle mesh from a 3D scalar field (Lorensen and Cline, 1987). Each cell in a regular 3D grid is classified by sampling the field at its 8 corners. The sign pattern produces a case index (0-255), and a lookup table maps each case to a set of triangles. Vertices are placed on grid edges by interpolating between the two corners. The algorithm is embarrassingly parallel since each cell processes independently, making it ideal for GPU compute. See our <a href="/guides/landscape-generation-browser.html#signed-distance-fields-sdfs">landscape guide on SDFs and marching cubes</a>.</p>
<p><strong>Signed Distance Fields (SDFs).</strong> A volumetric representation that stores, at every point in 3D space, the signed distance to the nearest surface. Positive values are outside, negative are inside, and the zero-crossing is the surface. SDFs can represent arbitrary 3D shapes: caves, arches, overhangs, and floating geometry that heightmaps can't express. Editing is natural set algebra on the field. Adding material (union of two shapes) is <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="15.06ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 6656.3 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(797.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1853.6,0)"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1111,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3520.6,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3909.6,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(553,-150) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(4866.1,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(5310.8,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(553,-150) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(6267.3,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>d</mi><mo>=</mo><mo data-mjx-texclass="OP" movablelimits="true">min</mo><mo stretchy="false">(</mo><msub><mi>d</mi><mn>1</mn></msub><mo>,</mo><msub><mi>d</mi><mn>2</mn></msub><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>, removing (digging) is <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="17.259ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 7628.3 1000" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(797.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1853.6,0)"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="78" d="M201 0Q189 3 102 3Q26 3 17 0H11V46H25Q48 47 67 52T96 61T121 78T139 96T160 122T180 150L226 210L168 288Q159 301 149 315T133 336T122 351T113 363T107 370T100 376T94 379T88 381T80 383Q74 383 44 385H16V431H23Q59 429 126 429Q219 429 229 431H237V385Q201 381 201 369Q201 367 211 353T239 315T268 274L272 270L297 304Q329 345 329 358Q329 364 327 369T322 376T317 380T310 384L307 385H302V431H309Q324 428 408 428Q487 428 493 431H499V385H492Q443 385 411 368Q394 360 377 341T312 257L296 236L358 151Q424 61 429 57T446 50Q464 46 499 46H516V0H510H502Q494 1 482 1T457 2T432 2T414 3Q403 3 377 3T327 1L304 0H295V46H298Q309 46 320 51T331 63Q331 65 291 120L250 175Q249 174 219 133T185 88Q181 83 181 74Q181 63 188 55T206 46Q208 46 208 23V0H201Z" transform="translate(1333,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(3714.6,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(4103.6,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(553,-150) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(5060.1,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(5504.8,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(6282.8,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(553,-150) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(7239.3,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>d</mi><mo>=</mo><mo data-mjx-texclass="OP" movablelimits="true">max</mo><mo stretchy="false">(</mo><msub><mi>d</mi><mn>1</mn></msub><mo>,</mo><mo>−</mo><msub><mi>d</mi><mn>2</mn></msub><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container> with the carve shape negated, and a smooth blend uses a soft minimum such as</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -1.577ex;" xmlns="http://www.w3.org/2000/svg" width="64.842ex" height="4.993ex" role="img" focusable="false" viewBox="0 -1509.9 28660.1 2206.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" style="stroke-width: 3;"/><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(394,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(1227,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1505,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2061,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2450,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(553,-150) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3406.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3851.2,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(553,-150) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(4807.8,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5252.4,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(5773.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(6440.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(7496,0)"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1111,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(9163,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(9552,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(553,-150) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(10508.5,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(10953.2,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(553,-150) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(11909.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(12521,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(13521.2,0)"><g data-mml-node="msup" transform="translate(224.2,676)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(609,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mrow" transform="translate(220,-686)"><g data-mml-node="mn"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(500,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g></g><rect width="1221" height="60" x="120" y="220"/></g><g data-mml-node="mo" transform="translate(14982.2,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(15260.2,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(17426.9,0)"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(18280.7,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(19336.4,0)"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="78" d="M201 0Q189 3 102 3Q26 3 17 0H11V46H25Q48 47 67 52T96 61T121 78T139 96T160 122T180 150L226 210L168 288Q159 301 149 315T133 336T122 351T113 363T107 370T100 376T94 379T88 381T80 383Q74 383 44 385H16V431H23Q59 429 126 429Q219 429 229 431H237V385Q201 381 201 369Q201 367 211 353T239 315T268 274L272 270L297 304Q329 345 329 358Q329 364 327 369T322 376T317 380T310 384L307 385H302V431H309Q324 428 408 428Q487 428 493 431H499V385H492Q443 385 411 368Q394 360 377 341T312 257L296 236L358 151Q424 61 429 57T446 50Q464 46 499 46H516V0H510H502Q494 1 482 1T457 2T432 2T414 3Q403 3 377 3T327 1L304 0H295V46H298Q309 46 320 51T331 63Q331 65 291 120L250 175Q249 174 219 133T185 88Q181 83 181 74Q181 63 188 55T206 46Q208 46 208 23V0H201Z" transform="translate(1333,0)" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="OPEN" transform="translate(21197.4,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M152 251Q152 646 388 850H416Q422 844 422 841Q422 837 403 816T357 753T302 649T255 482T236 250Q236 124 255 19T301 -147T356 -251T403 -315T422 -340Q422 -343 416 -349H388Q359 -325 332 -296T271 -213T212 -97T170 56T152 251Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(21655.4,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(22398.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(23398.9,0) translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(23676.9,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(553,-150) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(24855.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(25855.9,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(553,-150) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(26812.4,0) translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(27090.4,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(27368.4,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mn" transform="translate(27702.1,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" data-mjx-texclass="CLOSE" transform="translate(28202.1,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M305 251Q305 -145 69 -349H56Q43 -349 39 -347T35 -338Q37 -333 60 -307T108 -239T160 -136T204 27T221 250T204 473T160 636T108 740T60 807T35 839Q35 850 50 850H56H69Q197 743 256 566Q305 425 305 251Z" style="stroke-width: 3;"/></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>smin</mi><mo stretchy="false">(</mo><msub><mi>d</mi><mn>1</mn></msub><mo>,</mo><msub><mi>d</mi><mn>2</mn></msub><mo>,</mo><mi>k</mi><mo stretchy="false">)</mo><mo>=</mo><mo data-mjx-texclass="OP" movablelimits="true">min</mo><mo stretchy="false">(</mo><msub><mi>d</mi><mn>1</mn></msub><mo>,</mo><msub><mi>d</mi><mn>2</mn></msub><mo stretchy="false">)</mo><mo>−</mo><mfrac><msup><mi>h</mi><mn>2</mn></msup><mrow><mn>4</mn><mi>k</mi></mrow></mfrac><mo>,</mo><mstyle scriptlevel="0"><mspace width="2em"/></mstyle><mi>h</mi><mo>=</mo><mo data-mjx-texclass="OP" movablelimits="true">max</mo><mrow data-mjx-texclass="OPEN"><mo minsize="1.2em" maxsize="1.2em">(</mo></mrow><mi>k</mi><mo>−</mo><mo data-mjx-texclass="ORD" stretchy="false">|</mo><msub><mi>d</mi><mn>1</mn></msub><mo>−</mo><msub><mi>d</mi><mn>2</mn></msub><mo data-mjx-texclass="ORD" stretchy="false">|</mo><mo>,</mo><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><mn>0</mn><mrow data-mjx-texclass="CLOSE"><mo minsize="1.2em" maxsize="1.2em">)</mo></mrow></math></mjx-assistive-mml></mjx-container><p>where <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.179ex" height="1.595ex" role="img" focusable="false" viewBox="0 -694 521 705" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>k</mi></math></mjx-assistive-mml></mjx-container> controls the blend radius. As <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="5.829ex" height="1.62ex" role="img" focusable="false" viewBox="0 -694 2576.6 716" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(798.8,0)"><path data-c="2192" d="M56 237T56 250T70 270H835Q719 357 692 493Q692 494 692 496T691 499Q691 511 708 511H711Q720 511 723 510T729 506T732 497T735 481T743 456Q765 389 816 336T935 261Q944 258 944 250Q944 244 939 241T915 231T877 212Q836 186 806 152T761 85T740 35T732 4Q730 -6 727 -8T711 -11Q691 -11 691 0Q691 7 696 25Q728 151 835 230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2076.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>k</mi><mo accent="false" stretchy="false">→</mo><mn>0</mn></math></mjx-assistive-mml></mjx-container> this collapses back to a hard <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="3.771ex" height="1.514ex" role="img" focusable="false" viewBox="0 -669 1667 669" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(833,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1111,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo data-mjx-texclass="OP" movablelimits="true">min</mo></math></mjx-assistive-mml></mjx-container>. See <a href="/guides/landscape-generation-browser.html#signed-distance-fields-sdfs">SDF terrain representation</a>.</p>
<p><strong>WebGPU compute shaders.</strong> GPU programs that run general-purpose computation, not tied to the rasterization pipeline. A compute shader dispatches workgroups of threads that execute in parallel. For marching cubes, each thread processes one grid cell: sample the SDF, classify the cell, look up triangulation, interpolate edge vertices, and append to a mesh buffer using atomic counters. No CPU readback is needed because the output buffer is used directly as vertex data for rendering. Will Usher's <a href="https://www.willusher.io/webgpu-marching-cubes/" target="_blank" rel="noreferrer">webgpu-marching-cubes</a> demonstrates real-time 256^3 grid processing in the browser. See our <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">landscape guide on WebGPU-driven LOD</a>.</p>
<p><strong>Hybrid heightmap + SDF architecture.</strong> The practical approach for browser terrain: heightmaps cover the entire world (cheap, compact), while SDF volumes exist only in chunks that need caves, overhangs, or creator-carved features (5-10% of chunks). Near the camera, volumetric freedom allows carving and caves. Far away, heightmaps provide efficient flat terrain. See <a href="/guides/landscape-generation-browser.html#hybrid-heightmap-base--volumetric-overlays">hybrid terrain representation</a>.</p>
<hr>
<p>Part 7 of 12.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-06-clipmaps.html">Part 6 - Clipmaps changed the plot</a><br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-08-webgpu-integration.html">Part 8 - Integration without losing our baseline</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 8: Integration without losing our baseline]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-08-webgpu-integration</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-08-webgpu-integration</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spikes 13 and 14 were less about features and more about process discipline. We froze a clean baseline, then hardened one layer at a time.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-8-integration-without-losing-our-baseline" tabindex="-1">Building an open world in the browser, part 8: Integration without losing our baseline <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-8-integration-without-losing-our-baseline" aria-label="Permalink to &quot;Building an open world in the browser, part 8: Integration without losing our baseline&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>Integration is where projects get messy. You have working pieces in isolation. You connect them and suddenly every bug looks like it could be anywhere.</p>
<p>Spikes 13 and 14 were our answer to that trap. Spike 13 established a clean Three.js WebGPU baseline. Just a renderer, a scene, a camera, and a simple mesh. No terrain, no compute, no effects. We confirmed that Three.js's WebGPU backend initialized correctly, that the render loop was stable, and that TSL (Three.js Shading Language) node materials worked as expected. Only after that checkpoint passed did we start adding layers.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/13-threejs-webgpu/" title="Spike 13 Three.js WebGPU Baseline" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/13-threejs-webgpu/" target="_blank">Open Spike 13 in a new tab ↗</a> · <a href="/blog/spike-source?spike=13-threejs-webgpu">View source</a></p>
<p>Spike 14 was incremental hardening. We added one capability at a time: first camera controls, then lighting, then the compute-generated mesh from the marching cubes pipeline, then buffer plumbing to feed GPU output directly into Three.js geometry attributes. After each addition, we verified that the previous layer still behaved correctly.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/14-threejs-webgpu-incremental/" title="Spike 14 Incremental Hardening" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/14-threejs-webgpu-incremental/" target="_blank">Open Spike 14 in a new tab ↗</a> · <a href="/blog/spike-source?spike=14-threejs-webgpu-incremental">View source</a></p>
<p>That sounds slow. It was slow for exactly one day, and it saved us multiple days soon after when seam logic and policy switching got complicated.</p>
<p>The specific category of bug that justified this discipline was hairline artifacts. Thin slivers that looked like geometry corruption but were actually stale data. The compute shader would write N vertices into a buffer, but the draw call would still be configured to render N+M vertices from the previous frame. Those extra vertices contained garbage from the old dispatch. The visual result was flickering razor-thin triangles that appeared and vanished unpredictably.</p>
<p>You don't beat that class of bug with intuition. You beat it with controlled deltas where you know exactly what changed between the last working state and the current broken state.</p>
<p>The WebGPU integration also taught us about buffer lifecycle. GPU buffers in WebGPU are immutable once mapped for a specific usage. If you need to resize a vertex buffer because the marching cubes output grew, you have to create a new buffer and update the binding. There's no <code>realloc</code>. Getting that lifecycle right, destroying old buffers without racing against in-flight GPU work, required explicit fence management that doesn't exist in WebGL.</p>
<p>In part 9 we move into Transvoxel seam work. That chapter starts with a scaffold on purpose. By this point we had fully internalized the lesson that rushing integration produces mysteries, and controlled setup produces debuggable problems.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>WebGPU.</strong> The successor to WebGL, providing low-level GPU access in the browser with compute shaders and indirect rendering. WebGPU's two critical features for open worlds: compute shaders enable GPU-side terrain generation, foliage placement, and culling; indirect rendering lets the GPU decide what to draw based on compute output, eliminating CPU bottlenecks in dense scenes. Available in Chrome, Edge, and Firefox on desktop. See <a href="/guides/browser-3d-open-world-tech.html#webgpu-the-performance-unlock">WebGPU as a performance unlock</a>.</p>
<p><strong>Three.js Shading Language (TSL).</strong> Three.js's node-based shader system that replaces raw GLSL/WGSL with composable JavaScript expressions. TSL nodes like <code>texture()</code>, <code>positionWorld</code>, <code>smoothstep()</code>, and <code>fog()</code> build a shader graph at runtime that compiles to the appropriate backend (WebGL GLSL or WebGPU WGSL). TSL makes it possible to write material logic once and target both renderers. The node graph is evaluated per-frame, so dynamic uniforms and conditional branching work naturally.</p>
<p><strong>GPU buffer lifecycle in WebGPU.</strong> WebGPU buffers are created with specific usage flags (<code>VERTEX</code>, <code>STORAGE</code>, <code>COPY_DST</code>, etc.) and can't be resized after creation. If a marching cubes dispatch produces more vertices than the buffer can hold, you must create a new buffer, update the binding, and destroy the old one. Destroying a buffer that's still referenced by an in-flight GPU command causes errors. Explicit fence management (via <code>device.queue.onSubmittedWorkDone()</code>) ensures the old buffer isn't destroyed until the GPU finishes using it. This lifecycle discipline doesn't exist in WebGL, where the driver manages memory implicitly.</p>
<p><strong>Incremental hardening.</strong> A process discipline for integration: establish a known-good baseline, add one capability at a time, and verify the previous layer still works after each addition. This approach is slower for one day and saves days during later debugging because each regression can be traced to a specific, controlled change. The baseline-then-increment pattern is common in <a href="/guides/browser-3d-open-world-tech.html#what-we-d-build-first">AAA open world development</a> where systems are integrated in a specific order to manage risk.</p>
<hr>
<p>Part 8 of 12.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-07-marching-cubes.html">Part 7 - Marching cubes and the first real caves</a><br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-09-transvoxel-first-cut.html">Part 9 - Transvoxel started with a scaffold</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 9: Transvoxel started with a scaffold]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-09-transvoxel-first-cut</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-09-transvoxel-first-cut</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We didn't jump into full seam coverage. We built a seam test rig first, then validated one face with reference data.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-9-transvoxel-started-with-a-scaffold" tabindex="-1">Building an open world in the browser, part 9: Transvoxel started with a scaffold <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-9-transvoxel-started-with-a-scaffold" aria-label="Permalink to &quot;Building an open world in the browser, part 9: Transvoxel started with a scaffold&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>Seams are where confidence goes to die.</p>
<p>Everything can look stable until two resolutions meet. A chunk at LOD 0 sits next to a chunk at LOD 1. Their meshes are generated independently. Where they share a boundary, the vertex positions don't match because the lower-resolution chunk has half the grid density. The result is visible cracks, T-junctions, and flickering edges.</p>
<p>The Transvoxel algorithm solves this by generating special transition cells along the boundary face between two chunks at different resolutions. These cells sample from both the high-res and low-res grids simultaneously and produce triangles that stitch the two surfaces together. The algorithm uses its own lookup tables, separate from the regular marching cubes tables, with 512 transition cell cases.</p>
<p>We had enough integration scars by this point to know better than to rush the implementation.</p>
<p>Spike 15 had one job: build a seam test rig we could trust before touching the full algorithm. We set up a controlled environment where two chunks of known SDF data sat side by side at different resolutions, with visualization controls to toggle the main mesh, the seam mesh, wireframe, and normals independently.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/15-transvoxel-seam/" title="Spike 15 Transvoxel Seam Scaffold" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/15-transvoxel-seam/" target="_blank">Open Spike 15 in a new tab ↗</a> · <a href="/blog/spike-source?spike=15-transvoxel-seam">View source</a></p>
<p>Once the test rig was stable, Spike 16 validated table-driven transition cell generation on a single face. We picked one axis-aligned face (+X boundary), implemented the transition cell evaluation for that face only, and compared the output against reference data from the Transvoxel paper.</p>
<p><a href="/spikes/16-transvoxel-face/" target="_blank">Open Spike 16 in a new tab ↗</a> · <a href="/blog/spike-source?spike=16-transvoxel-face">View source</a></p>
<p>We tested one face at a time on purpose, because transition table wiring has many independent failure modes. Case index computation depends on sampling the correct vertices from both grids. Vertex indexing within a transition cell uses a different numbering scheme than regular marching cubes cells. Winding order has to be consistent with the main mesh or backface culling will eat your seam triangles. If you test all six faces at once, every symptom looks random. If you test one face thoroughly, you get meaningful, debuggable failures.</p>
<p>Another subtle win from this phase was tooling investment. We built visibility toggles, seam-only rendering, and color-coded LOD indicators early. Those controls felt like overhead at the time. Later, when corner cases got ugly, they paid for themselves over and over because we could isolate exactly which seam cells were misbehaving.</p>
<p>By the end of this chapter we weren't &quot;done with seams.&quot; We were in a position where seam bugs could be reasoned about instead of feared.</p>
<p>In part 10 the real roller coaster starts. Mixed LOD corners, winding flips, partial overdraw ghosts, and those moments where you're sure the algorithm is wrong and then discover the bug is a draw range reading past the active vertex count.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>The Transvoxel algorithm.</strong> Designed by Eric Lengyel (<a href="https://transvoxel.org/" target="_blank" rel="noreferrer">transvoxel.org</a>), Transvoxel solves the hardest problem in volumetric terrain LOD: seams between chunks at different resolutions. When a high-res chunk sits next to a low-res chunk, the marching cubes meshes don't align at the boundary, producing visible cracks. Transvoxel inserts special transition cells along boundary faces that bridge the resolution difference with additional triangles matching both sides. The algorithm uses its own lookup tables (separate from regular marching cubes) with 512 transition cell cases, reduced to 73 equivalence classes. It's patent-free and has been used in shipped games (Space Engineers, Astroneer). See our <a href="/guides/landscape-generation-browser.html#the-transvoxel-algorithm">landscape guide on Transvoxel</a>.</p>
<p><strong>Transition cells.</strong> Special cells generated at the face between two LOD levels. Unlike regular marching cubes cells that sample 8 corners from one grid, transition cells sample from both the high-res and low-res grids simultaneously. The high-res face has <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="9.176ex" height="1.692ex" role="img" focusable="false" viewBox="0 -666 4056 748" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1722.4,0)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2500.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3556,0)"><path data-c="39" d="M352 287Q304 211 232 211Q154 211 104 270T44 396Q42 412 42 436V444Q42 537 111 606Q171 666 243 666Q245 666 249 666T257 665H261Q273 665 286 663T323 651T370 619T413 560Q456 472 456 334Q456 194 396 97Q361 41 312 10T208 -22Q147 -22 108 7T68 93T121 149Q143 149 158 135T173 96Q173 78 164 65T148 49T135 44L131 43Q131 41 138 37T164 27T206 22H212Q272 22 313 86Q352 142 352 280V287ZM244 248Q292 248 321 297T351 430Q351 508 343 542Q341 552 337 562T323 588T293 615T246 625Q208 625 181 598Q160 576 154 546T147 441Q147 358 152 329T172 282Q197 248 244 248Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>3</mn><mo>×</mo><mn>3</mn><mo>=</mo><mn>9</mn></math></mjx-assistive-mml></mjx-container> sample points, while the low-res face has <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="9.176ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 4056 759" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(1722.4,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2500.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(3556,0)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mn>2</mn><mo>×</mo><mn>2</mn><mo>=</mo><mn>4</mn></math></mjx-assistive-mml></mjx-container>. Those 9 samples are what give the algorithm its <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="8.53ex" height="2.072ex" role="img" focusable="false" viewBox="0 -833.9 3770.1 915.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(533,363) scale(0.707)"><path data-c="39" d="M352 287Q304 211 232 211Q154 211 104 270T44 396Q42 412 42 436V444Q42 537 111 606Q171 666 243 666Q245 666 249 666T257 665H261Q273 665 286 663T323 651T370 619T413 560Q456 472 456 334Q456 194 396 97Q361 41 312 10T208 -22Q147 -22 108 7T68 93T121 149Q143 149 158 135T173 96Q173 78 164 65T148 49T135 44L131 43Q131 41 138 37T164 27T206 22H212Q272 22 313 86Q352 142 352 280V287ZM244 248Q292 248 321 297T351 430Q351 508 343 542Q341 552 337 562T323 588T293 615T246 625Q208 625 181 598Q160 576 154 546T147 441Q147 358 152 329T172 282Q197 248 244 248Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1214.3,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2270.1,0)"><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" style="stroke-width: 3;"/><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(1000,0)" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mn>2</mn><mn>9</mn></msup><mo>=</mo><mn>512</mn></math></mjx-assistive-mml></mjx-container> raw cases, which collapse to 73 distinct triangulations once you fold out rotations and reflections. The cell classification and triangulation use dedicated tables that produce triangles connecting vertices at both resolutions. The vertex numbering scheme is different from regular MC cells, which is a common source of implementation bugs.</p>
<p><strong>LOD transition seams.</strong> The boundary between two mesh resolutions where topology mismatch causes visual artifacts. Without stitching, a LOD 0 chunk (1m grid) next to a LOD 1 chunk (2m grid) produces T-junctions: the fine mesh has vertices along the boundary that the coarse mesh doesn't share, causing cracks where light bleeds through. Transvoxel, geomorphing, and skirt geometry are three approaches to fixing these seams. For volumetric terrain, Transvoxel is the standard solution because geomorphing only works for heightmaps. See <a href="/guides/landscape-generation-browser.html#lod-for-volumetric-terrain">LOD for volumetric terrain</a>.</p>
<hr>
<p>Part 9 of 12.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-08-webgpu-integration.html">Part 8 - Integration without losing our baseline</a><br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-10-seam-chaos.html">Part 10 - Seam chaos and the corner boss fight</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 10: Seam chaos and the corner boss fight]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-10-seam-chaos</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-10-seam-chaos</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spikes 17 to 22 were where theory met edge cases. We chased winding bugs, missing triangles, overdraw ghosts, and hybrid fallback logic.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-10-seam-chaos-and-the-corner-boss-fight" tabindex="-1">Building an open world in the browser, part 10: Seam chaos and the corner boss fight <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-10-seam-chaos-and-the-corner-boss-fight" aria-label="Permalink to &quot;Building an open world in the browser, part 10: Seam chaos and the corner boss fight&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>If the earlier parts felt methodical, this chapter felt like combat.</p>
<p>Spikes 17 through 22 were our corner-case era. Dual-LOD marching cubes, heightmap-to-MC boundary seams, mixed-resolution corner chunks where three or four LOD levels meet, GPU seam generation, and fallback mode behavior. Each spike addressed a specific failure scenario we'd encountered or anticipated.</p>
<p>Spike 17 tested dual marching cubes with two LOD levels active simultaneously. The challenge was that a single chunk's neighbors could be at different resolutions on different faces. The transition cell logic from Spike 16 worked for one face at a time, but when a chunk needed transition cells on multiple faces, the vertex buffer management got complicated. Each face's transition cells had to be generated and appended without overwriting the others.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/19-transvoxel-corner-grid/" title="Spike 19 Corner Grid Transvoxel" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/19-transvoxel-corner-grid/" target="_blank">Open Spike 19 in a new tab ↗</a> · <a href="/blog/spike-source?spike=19-transvoxel-corner-grid">View source</a></p>
<p>The first repeated villain was winding order. Several times we thought we had topology problems, then found orientation problems. Backface culling was eating valid seam triangles because the winding was flipped relative to the main mesh. Same root cause, different visual symptom depending on camera angle. The fix was enforcing a consistent winding convention in the transition cell emission code and verifying it with a double-sided material toggle.</p>
<p>The second villain was false confidence from partial correctness. A seam could look perfect from one camera angle and break when LOD roles swapped between higher and lower resolution chunks. The transition cell is asymmetric. It samples from the high-res side and the low-res side differently. If you get the &quot;which side is high-res&quot; logic backwards for one configuration, you only see the bug when the camera moves to a specific position.</p>
<p>Then came one of our favorite recoveries. We were chasing a seam cutoff artifact on heightmap tiles and blaming the transition logic. Burned two days on it. The real culprit was stale overdraw. Higher-resolution geometry from a previous frame was still living in the buffer tail after the chunk downscaled to a lower LOD. The draw range was still set to the old, larger vertex count. Once we clipped the draw range to the active vertex count reported by the compute shader's atomic counter, the &quot;mystery seam issue&quot; disappeared.</p>
<p>That was a great reminder that rendering bugs often masquerade as meshing bugs. The geometry was correct the whole time. The draw call was just reading past the end of valid data.</p>
<p>By Spike 22 we were testing hybrid fallback where chunks could switch from marching cubes to heightmap mode under specific conditions, like when the chunk contains no volumetric edits and sits far enough from the camera. This gave us a more practical path than an all-or-nothing policy. Near-field edited chunks use MC for volumetric freedom. Far-field unedited chunks use heightmaps for efficiency.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/22-gpu-mc-heightmap-fallback/" title="Spike 22 MC Heightmap Fallback" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/22-gpu-mc-heightmap-fallback/" target="_blank">Open Spike 22 in a new tab ↗</a> · <a href="/blog/spike-source?spike=22-gpu-mc-heightmap-fallback">View source</a></p>
<p>This chapter was the steep drop of the roller coaster. Frustrating and productive at the same time. Many of the individual fixes were small, sometimes a single line changing a comparison operator or an offset. But the understanding they produced about how LOD transitions, buffer management, and draw ranges interact wasn't small at all.</p>
<p>In part 11 we cover the stabilization layer that came after the chaos: policy-based chunk modes and the transition from reactive bug-fixing to explicit system rules.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Dual-LOD marching cubes.</strong> Running marching cubes at two resolution levels simultaneously, with transition cells stitching the boundary. The challenge is that a single chunk's neighbors can be at different resolutions on different faces, requiring independent transition cell generation per face. Each face's transition cells are appended to the vertex buffer without overwriting the others. Atomic counters track the total active vertex count across all faces.</p>
<p><strong>Winding order.</strong> The vertex order within each triangle determines which side is the &quot;front&quot; face. Consistent winding (typically counter-clockwise when viewed from outside) is required for backface culling. When transition cells emit triangles, the winding must match the main mesh's convention. Getting it backwards causes backface culling to eat valid seam triangles, which looks like missing surfaces from certain camera angles. A common debugging technique is toggling <code>side: THREE.DoubleSide</code> on the material to confirm whether artifacts are winding issues or genuine topology gaps.</p>
<p><strong>Heightmap-to-MC fallback.</strong> A hybrid chunk mode where far or unedited chunks use heightmap terrain (cheap, flat surface) while near or edited chunks use marching cubes (volumetric, supports caves). The fallback decision depends on distance from camera and whether the chunk contains SDF edits. The seam between a heightmap chunk and an MC chunk requires its own transition geometry, similar to Transvoxel but bridging two different representations rather than two LOD levels. See <a href="/guides/landscape-generation-browser.html#hybrid-heightmap-base--volumetric-overlays">hybrid heightmap + volumetric overlays</a>.</p>
<p><strong>Draw range and atomic counters.</strong> In GPU-driven mesh generation, the compute shader writes vertices into a buffer and increments an atomic counter to track how many vertices were emitted. The draw call must use this counter as the vertex count, not the buffer capacity. If the draw range isn't clipped to the active count, stale vertices from previous frames (still living in the buffer tail) produce ghost geometry: thin slivers and flickering triangles that look like topology errors but are actually rendering artifacts from reading past valid data.</p>
<hr>
<p>Part 10 of 12.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-09-transvoxel-first-cut.html">Part 9 - Transvoxel started with a scaffold</a><br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-11-policy-modes.html">Part 11 - Policy mode, not hardcoded mode</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 11: Policy mode, not hardcoded mode]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-11-policy-modes</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-11-policy-modes</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 23 shifted us from reactive fixes to explicit policy. Distance-driven LOD, edit-driven HM/MC switching, and better seam debugging controls.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-11-policy-mode-not-hardcoded-mode" tabindex="-1">Building an open world in the browser, part 11: Policy mode, not hardcoded mode <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-11-policy-mode-not-hardcoded-mode" aria-label="Permalink to &quot;Building an open world in the browser, part 11: Policy mode, not hardcoded mode&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>After the seam chaos chapter, we needed to stop reacting and start governing.</p>
<p>Spike 23 replaced ad-hoc behavior with explicit policy rules. Instead of chunks doing whatever their local state suggested, a central policy system now made the decisions. Which LOD level does this chunk get? Is it rendered as heightmap or marching cubes? Does it need transition cells, and on which faces? The answers came from a policy function that evaluated distance to camera, edit history, and neighbor resolution states.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/23-policy-chunk-modes/" title="Spike 23 Policy Chunk Modes" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/23-policy-chunk-modes/" target="_blank">Open Spike 23 in a new tab ↗</a> · <a href="/blog/spike-source?spike=23-policy-chunk-modes">View source</a></p>
<p>The distance-based LOD assignment used concentric rings around the camera, similar to the clipmap concept but applied to the chunk grid. Ring 0 chunks get full-resolution MC. Ring 1 gets half-resolution MC. Ring 2 and beyond get heightmap mode. The adjacency constraint was critical: for any two neighboring chunks we require <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.666ex;" xmlns="http://www.w3.org/2000/svg" width="12.84ex" height="2.361ex" role="img" focusable="false" viewBox="0 -749.5 5675.3 1043.7" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(278,0)"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(714,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1508.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2508.4,0)"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(714,-150) scale(0.707)"><path data-c="1D457" d="M297 596Q297 627 318 644T361 661Q378 661 389 651T403 623Q403 595 384 576T340 557Q322 557 310 567T297 596ZM288 376Q288 405 262 405Q240 405 220 393T185 362T161 325T144 293L137 279Q135 278 121 278H107Q101 284 101 286T105 299Q126 348 164 391T252 441Q253 441 260 441T272 442Q296 441 316 432Q341 418 354 401T367 348V332L318 133Q267 -67 264 -75Q246 -125 194 -164T75 -204Q25 -204 7 -183T-12 -137Q-12 -110 7 -91T53 -71Q70 -71 82 -81T95 -112Q95 -148 63 -167Q69 -168 77 -168Q111 -168 139 -140T182 -74L193 -32Q204 11 219 72T251 197T278 308T289 365Q289 372 288 376Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3563.7,0) translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4119.5,0)"><path data-c="2264" d="M674 636Q682 636 688 630T694 615T687 601Q686 600 417 472L151 346L399 228Q687 92 691 87Q694 81 694 76Q694 58 676 56H670L382 192Q92 329 90 331Q83 336 83 348Q84 359 96 365Q104 369 382 500T665 634Q669 636 674 636ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(5175.3,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo data-mjx-texclass="ORD" stretchy="false">|</mo><msub><mi>L</mi><mi>i</mi></msub><mo>−</mo><msub><mi>L</mi><mi>j</mi></msub><mo data-mjx-texclass="ORD" stretchy="false">|</mo><mo>≤</mo><mn>1</mn></math></mjx-assistive-mml></mjx-container>. The resolution ratio across a face is <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="6.754ex" height="2.02ex" role="img" focusable="false" viewBox="0 -893 2985.2 893" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(278,0)"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(714,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1286,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2064,0)"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(714,-150) scale(0.707)"><path data-c="1D457" d="M297 596Q297 627 318 644T361 661Q378 661 389 651T403 623Q403 595 384 576T340 557Q322 557 310 567T297 596ZM288 376Q288 405 262 405Q240 405 220 393T185 362T161 325T144 293L137 279Q135 278 121 278H107Q101 284 101 286T105 299Q126 348 164 391T252 441Q253 441 260 441T272 442Q296 441 316 432Q341 418 354 401T367 348V332L318 133Q267 -67 264 -75Q246 -125 194 -164T75 -204Q25 -204 7 -183T-12 -137Q-12 -110 7 -91T53 -71Q70 -71 82 -81T95 -112Q95 -148 63 -167Q69 -168 77 -168Q111 -168 139 -140T182 -74L193 -32Q204 11 219 72T251 197T278 308T289 365Q289 372 288 376Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3119.3,0) translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g></g></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mn>2</mn><mrow data-mjx-texclass="ORD"><mo data-mjx-texclass="ORD" stretchy="false">|</mo><msub><mi>L</mi><mi>i</mi></msub><mo>−</mo><msub><mi>L</mi><mi>j</mi></msub><mo data-mjx-texclass="ORD" stretchy="false">|</mo></mrow></msup></math></mjx-assistive-mml></mjx-container>, and the Transvoxel transition tables are defined for the 2:1 case only (<mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.666ex;" xmlns="http://www.w3.org/2000/svg" width="12.84ex" height="2.361ex" role="img" focusable="false" viewBox="0 -749.5 5675.3 1043.7" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(278,0)"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(714,-150) scale(0.707)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(1508.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2508.4,0)"><g data-mml-node="mi"><path data-c="1D43F" d="M228 637Q194 637 192 641Q191 643 191 649Q191 673 202 682Q204 683 217 683Q271 680 344 680Q485 680 506 683H518Q524 677 524 674T522 656Q517 641 513 637H475Q406 636 394 628Q387 624 380 600T313 336Q297 271 279 198T252 88L243 52Q243 48 252 48T311 46H328Q360 46 379 47T428 54T478 72T522 106T564 161Q580 191 594 228T611 270Q616 273 628 273H641Q647 264 647 262T627 203T583 83T557 9Q555 4 553 3T537 0T494 -1Q483 -1 418 -1T294 0H116Q32 0 32 10Q32 17 34 24Q39 43 44 45Q48 46 59 46H65Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Q285 635 228 637Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(714,-150) scale(0.707)"><path data-c="1D457" d="M297 596Q297 627 318 644T361 661Q378 661 389 651T403 623Q403 595 384 576T340 557Q322 557 310 567T297 596ZM288 376Q288 405 262 405Q240 405 220 393T185 362T161 325T144 293L137 279Q135 278 121 278H107Q101 284 101 286T105 299Q126 348 164 391T252 441Q253 441 260 441T272 442Q296 441 316 432Q341 418 354 401T367 348V332L318 133Q267 -67 264 -75Q246 -125 194 -164T75 -204Q25 -204 7 -183T-12 -137Q-12 -110 7 -91T53 -71Q70 -71 82 -81T95 -112Q95 -148 63 -167Q69 -168 77 -168Q111 -168 139 -140T182 -74L193 -32Q204 11 219 72T251 197T278 308T289 365Q289 372 288 376Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3563.7,0) translate(0 -0.5)"><path data-c="7C" d="M139 -249H137Q125 -249 119 -235V251L120 737Q130 750 139 750Q152 750 159 735V-235Q151 -249 141 -249H139Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(4119.5,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(5175.3,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mo data-mjx-texclass="ORD" stretchy="false">|</mo><msub><mi>L</mi><mi>i</mi></msub><mo>−</mo><msub><mi>L</mi><mi>j</mi></msub><mo data-mjx-texclass="ORD" stretchy="false">|</mo><mo>=</mo><mn>1</mn></math></mjx-assistive-mml></mjx-container>). A jump of 2 levels would need a 4:1 transition the tables can't express, so the policy forces the lower-detail chunk to upgrade until the constraint holds.</p>
<p>The HM/MC switching logic checked each chunk's edit bitmap. If a chunk had any volumetric edits (caves, tunnels, terrain sculpts), it stayed in MC mode regardless of distance. Unedited chunks could drop to heightmap mode when they moved far enough away. This hybrid approach gave us volumetric freedom where it mattered and efficiency where it didn't.</p>
<p>This spike looked smaller from the outside than some earlier ones. In practice it was a major quality-of-life improvement for development.</p>
<p>When your system can explain why a chunk switched mode, you spend less time guessing. We added color-coded overlays: green for heightmap chunks, blue for MC chunks, orange for transition-active faces. When seam visibility has dedicated render controls, visual debugging ambiguity drops. When draw ranges are explicitly tied to active vertex counts from the policy system, stale geometry ghosts stop wasting your afternoon.</p>
<p>We also overhauled camera behavior in this spike. Earlier spikes had simple orbit controls fine for screenshots but useless for reproducing bugs. Spike 23 added WASD fly camera with configurable speed, altitude lock toggle, and position readout. That sounds minor. It meant the difference between &quot;I saw a bug somewhere near that ridge&quot; and &quot;the bug appears at position (142, 12, -67) facing northwest.&quot;</p>
<p>The key insight from this chapter is that policy didn't reduce complexity. It organized complexity. The same number of edge cases existed. But now each edge case had a name, a trigger condition, and a place in the code where you could set a breakpoint. That's a different kind of win, and it's what determines whether a system can keep evolving or collapses under its own weight.</p>
<p>By the end of Spike 23, we had a near-field behavior layer that was predictable enough to connect to a far-field clipmap ring strategy without constant fear of interaction bugs.</p>
<p>In part 12 we cover Spike 24, where ring transitions, skybox fog, and Three.js version-level shader integration close out this chapter of the project.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Distance-based LOD policy.</strong> A central function that assigns LOD level and rendering mode to each chunk based on distance to camera, edit history, and neighbor states. Concentric distance rings determine the base LOD: ring 0 = full-res MC, ring 1 = half-res MC, ring 2+ = heightmap mode. The policy function runs per frame as the camera moves and triggers chunk transitions. This replaces ad-hoc per-chunk decisions with a predictable, debuggable rule system. See <a href="/guides/landscape-generation-browser.html#gpu-driven-lod-with-webgpu">GPU-driven LOD selection</a> for the compute shader equivalent.</p>
<p><strong>Adjacency constraints.</strong> The Transvoxel algorithm handles 2:1 resolution ratios only. If two neighboring chunks differ by more than one LOD level (e.g., LOD 0 next to LOD 2), the transition tables can't produce valid seam geometry. The policy system enforces this by upgrading the lower-detail chunk when the LOD difference exceeds 1. This constraint propagation can cascade: upgrading one chunk may force its neighbors to upgrade as well. The implementation is a simple iterative pass that converges in 2-3 iterations for typical grid configurations.</p>
<p><strong>Edit bitmap for mode selection.</strong> Each chunk maintains a bitmap recording whether it contains volumetric SDF edits (caves, tunnels, sculpts). Chunks with any edits stay in marching cubes mode regardless of distance, preserving the creator's modifications. Unedited chunks drop to heightmap mode when far enough from the camera, saving compute and memory. The bitmap is a single flag per chunk but can be extended to track edit density for more granular mode decisions.</p>
<p><strong>Debug visualization overlays.</strong> Color-coded chunk rendering where green = heightmap mode, blue = MC mode, orange = transition-active faces. Per-chunk overlays with LOD level numbers, mode labels, and wireframe toggles. These are development tools, not shipped features, but they pay for themselves repeatedly when debugging LOD transitions and seam artifacts. Combined with a WASD fly camera that reports exact world position, they turn &quot;I saw a bug somewhere&quot; into &quot;the bug appears at (142, 12, -67) with this LOD configuration.&quot;</p>
<hr>
<p>Part 11 of 12.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-10-seam-chaos.html">Part 10 - Seam chaos and the corner boss fight</a><br>
Next: <a href="/blog/2026-02-25-open-world-browser-part-12-lessons.html">Part 12 - Rings, sky fog, and what we would do again</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Building an open world in the browser, part 12: Rings, sky fog, and what we would do again]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-12-lessons</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-part-12-lessons</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Spike 24 closed this chapter with clipmap ring transitions, skybox fog integration, and one long list of hard-earned lessons.]]></description>
            <content:encoded><![CDATA[<h1 id="building-an-open-world-in-the-browser-part-12-rings-sky-fog-and-what-we-would-do-again" tabindex="-1">Building an open world in the browser, part 12: Rings, sky fog, and what we would do again <a class="header-anchor" href="#building-an-open-world-in-the-browser-part-12-rings-sky-fog-and-what-we-would-do-again" aria-label="Permalink to &quot;Building an open world in the browser, part 12: Rings, sky fog, and what we would do again&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>New here? Use the <a href="/blog/2026-02-25-open-world-browser-series-guide.html">series guide</a>. It explains what a spike is and links all parts.</p>
<p>Spike 24 was supposed to be &quot;add clipmap rings to the terrain.&quot; It became a full finale that touched rendering, shaders, module infrastructure, and visual integration all at once.</p>
<p>The core terrain task was generating concentric clipmap rings in the vertex shader. Each ring is a flat grid mesh centered on the camera, with vertices displaced by heightmap samples. The inner ring uses full resolution. Each subsequent ring doubles the vertex spacing and covers a larger area. The tricky part is the boundary between rings: where a high-resolution ring meets a low-resolution ring, edge vertices on the finer mesh need to snap to the midpoint of the coarser mesh's edge. We did 2:1 edge morphing by detecting boundary vertices (those whose grid coordinate is odd along the ring edge) and snapping their height to the midpoint of their two even neighbors, <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.781ex;" xmlns="http://www.w3.org/2000/svg" width="19.274ex" height="2.737ex" role="img" focusable="false" viewBox="0 -864.9 8519.1 1209.9" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(853.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(1909.6,0)"><g data-mml-node="mfrac"><g data-mml-node="mn" transform="translate(220,394) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(220,-345) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><rect width="553.6" height="60" x="120" y="220"/></g></g><g data-mml-node="mo" transform="translate(2703.1,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3092.1,0)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(609,-150) scale(0.707)"><path data-c="6C" d="M42 46H56Q95 46 103 60V68Q103 77 103 91T103 124T104 167T104 217T104 272T104 329Q104 366 104 407T104 482T104 542T103 586T103 603Q100 622 89 628T44 637H26V660Q26 683 28 683L38 684Q48 685 67 686T104 688Q121 689 141 690T171 693T182 694H185V379Q185 62 186 60Q190 52 198 49Q219 46 247 46H263V0H255L232 1Q209 2 183 2T145 3T107 3T57 1L34 0H26V46H42Z" style="stroke-width: 3;"/><path data-c="65" d="M28 218Q28 273 48 318T98 391T163 433T229 448Q282 448 320 430T378 380T406 316T415 245Q415 238 408 231H126V216Q126 68 226 36Q246 30 270 30Q312 30 342 62Q359 79 369 104L379 128Q382 131 395 131H398Q415 131 415 121Q415 117 412 108Q393 53 349 21T250 -11Q155 -11 92 58T28 218ZM333 275Q322 403 238 411H236Q228 411 220 410T195 402T166 381T143 340T127 274V267H333V275Z" transform="translate(278,0)" style="stroke-width: 3;"/><path data-c="66" d="M273 0Q255 3 146 3Q43 3 34 0H26V46H42Q70 46 91 49Q99 52 103 60Q104 62 104 224V385H33V431H104V497L105 564L107 574Q126 639 171 668T266 704Q267 704 275 704T289 705Q330 702 351 679T372 627Q372 604 358 590T321 576T284 590T270 627Q270 647 288 667H284Q280 668 273 668Q245 668 223 647T189 592Q183 572 182 497V431H293V385H185V225Q185 63 186 61T189 57T194 54T199 51T206 49T213 48T222 47T231 47T241 46T251 46H282V0H273Z" transform="translate(722,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(1028,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(4975.3,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(5975.5,0)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(609,-150) scale(0.707)"><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(392,0)" style="stroke-width: 3;"/><path data-c="67" d="M329 409Q373 453 429 453Q459 453 472 434T485 396Q485 382 476 371T449 360Q416 360 412 390Q410 404 415 411Q415 412 416 414V415Q388 412 363 393Q355 388 355 386Q355 385 359 381T368 369T379 351T388 325T392 292Q392 230 343 187T222 143Q172 143 123 171Q112 153 112 133Q112 98 138 81Q147 75 155 75T227 73Q311 72 335 67Q396 58 431 26Q470 -13 470 -72Q470 -139 392 -175Q332 -206 250 -206Q167 -206 107 -175Q29 -140 29 -75Q29 -39 50 -15T92 18L103 24Q67 55 67 108Q67 155 96 193Q52 237 52 292Q52 355 102 398T223 442Q274 442 318 416L329 409ZM299 343Q294 371 273 387T221 404Q192 404 171 388T145 343Q142 326 142 292Q142 248 149 227T179 192Q196 182 222 182Q244 182 260 189T283 207T294 227T299 242Q302 258 302 292T299 343ZM403 -75Q403 -50 389 -34T348 -11T299 -2T245 0H218Q151 0 138 -6Q118 -15 107 -34T95 -74Q95 -84 101 -97T122 -127T170 -155T250 -167Q319 -167 361 -139T403 -75Z" transform="translate(670,0)" style="stroke-width: 3;"/><path data-c="68" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 124T102 167T103 217T103 272T103 329Q103 366 103 407T103 482T102 542T102 586T102 603Q99 622 88 628T43 637H25V660Q25 683 27 683L37 684Q47 685 66 686T103 688Q120 689 140 690T170 693T181 694H184V367Q244 442 328 442Q451 442 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1170,0)" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(1726,0)" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(8130.1,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>h</mi><mo>=</mo><mstyle displaystyle="false" scriptlevel="0"><mfrac><mn>1</mn><mn>2</mn></mfrac></mstyle><mo stretchy="false">(</mo><msub><mi>h</mi><mtext>left</mtext></msub><mo>+</mo><msub><mi>h</mi><mtext>right</mtext></msub><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container>. That's exactly the point the coarse ring's single edge would sit at, so the fine and coarse edges describe the same line. This produces watertight seams without transition geometry.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.12)">
<iframe src="/spikes/24-gpu-clipmap-rings/" title="Spike 24 Clipmap Rings and Sky Fog" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:#000" loading="lazy" allowfullscreen></iframe>
</div>
<p><a href="/spikes/24-gpu-clipmap-rings/" target="_blank">Open Spike 24 in a new tab ↗</a> · <a href="/blog/spike-source?spike=24-gpu-clipmap-rings">View source</a></p>
<p>Then came the fog and sky integration. We wanted distant terrain to fade into the actual sky color, not a flat constant. That meant the fog shader needed to know what color the sky would be in the direction of each fragment. We loaded an equirectangular HDR skybox texture and sampled it in the fragment shader using the view direction from camera to fragment, converted to equirectangular UV coordinates via TSL's <code>equirectUV</code> node. The fog factor was distance-based using <code>positionView.z.negate()</code> for camera-space depth, blended with <code>smoothstep</code> between a near and far distance.</p>
<p>Module wiring turned out to be more annoying than any of the geometry. We upgraded to Three.js 0.183.1, which restructured the build outputs. The <code>three/tsl</code> import needed to resolve to <code>three.tsl.js</code>, and TSL internally imported <code>three/webgpu</code> as a bare specifier. Both mappings had to be explicit in the HTML import map. Missing either one produced cryptic &quot;does not provide an export&quot; or &quot;failed to resolve module specifier&quot; errors with no indication of which mapping was wrong. Once both were in the import map, the shader graph loaded correctly.</p>
<p>We also had a skybox orientation issue where the texture rendered upside down. The fix was <code>flipY = true</code> on the equirectangular texture, which is the Three.js default for loaded textures but was set to <code>false</code> in our initial code.</p>
<p>The original fog implementation sampled the sky at a near-constant direction, producing a thin horizon-colored band instead of a natural gradient. The fix was computing the actual camera-to-fragment world direction per pixel using <code>positionWorld.sub(cameraPosition).normalize()</code> and passing that into <code>equirectUV</code> for the fog color lookup. This made terrain fragments fade into the sky color that's actually behind them, which looks correct from any camera angle.</p>
<p>Underneath all the individual fixes, the core outcome held. We now have a terrain system that combines near-field volumetric editing (marching cubes with Transvoxel seams), mid-field heightmap chunks, and far-field clipmap rings, all governed by a policy layer that decides mode, LOD, and transition behavior.</p>
<p>If I had to name the patterns I'd repeat on the next project, they'd be these:</p>
<p>Start with risk spikes before feature work. Spike 1 killed the &quot;can we even render fast enough&quot; question before we invested in content pipelines.</p>
<p>Freeze known-good baselines before integration jumps. Spikes 13 and 14 saved us days of bisecting regressions.</p>
<p>Force policy and observability before optimization marathons. Spike 23 turned mystery bugs into named conditions with trigger rules.</p>
<p>Test under motion, not screenshots. Clipmap pops, seam flicker, and streaming hitches all hide in still frames.</p>
<p>Measure frame-time cost per feature, not average FPS. Averages hide the spikes that users actually feel.</p>
<p>And publish the messy parts. The wrong turns, the stale-buffer ghost hunts, the two days blaming transition logic when the draw range was wrong. Those are the parts people can actually learn from.</p>
<h2 id="external-reality-check-vuntra-city-devlogs" tabindex="-1">External reality check: Vuntra City devlogs <a class="header-anchor" href="#external-reality-check-vuntra-city-devlogs" aria-label="Permalink to &quot;External reality check: Vuntra City devlogs&quot;"></a></h2>
<p>After finishing this series, we reviewed the <code>@VuntraCity</code> devlogs as an external implementation check against our own open-world assumptions. It's a native UE5 project, not a browser stack, but the system patterns map well enough that the comparison is useful.</p>
<p>The first signal is that traversal speed has to be treated as a streaming control, not just gameplay. In Vuntra City, high-speed transit is intentionally routed above most interiors, and detail range scales with movement speed to avoid spawn churn and stalls (<a href="https://www.youtube.com/watch?v=KKeBElJS6-M" target="_blank" rel="noreferrer">transport system</a>, <a href="https://www.youtube.com/watch?v=aPuYXyJet38" target="_blank" rel="noreferrer">performance techniques</a>). That matches our policy-layer direction: movement mode should directly influence chunk radius, interior activation, and allowed work per frame.</p>
<p>The second signal is architecture. Their maps and address system required separating world topology from rendered objects so global queries can run for unloaded regions (<a href="https://www.youtube.com/watch?v=6dLn1GQpu2c" target="_blank" rel="noreferrer">maps and addresses</a>). That's the same separation we need in browser for world search, quest routing, moderation scans, and POI indexing without forcing render-bound data paths.</p>
<p>The third signal is simulation tiering. Their million-NPC design keeps coarse schedule state cheap and global, then spends expensive behavior budget only near the player (<a href="https://www.youtube.com/watch?v=nBV0yAAJUf0" target="_blank" rel="noreferrer">million-NPC overview</a>, <a href="https://www.youtube.com/watch?v=eUi7DB1ar3s" target="_blank" rel="noreferrer">system deep dive</a>). That reinforces our own AOI-first simulation model, where near-field fidelity and far-field determinism are separate concerns with separate budgets.</p>
<p>And the fourth signal is design quality, not raw scale. Their strongest exploration moments come from weighted distributions, rare outliers, and diegetic navigation clues instead of constant UI overlays (<a href="https://www.youtube.com/watch?v=4MZ5-KQW3pc" target="_blank" rel="noreferrer">procedural environment notes</a>, <a href="https://www.youtube.com/watch?v=ixR1hqZJlv4" target="_blank" rel="noreferrer">no minimap loop</a>). For us, this is a reminder that technical systems should be tuned to produce discoverable variation, not just maximal throughput.</p>
<h2 id="technology-referenced-in-this-chapter" tabindex="-1">Technology referenced in this chapter <a class="header-anchor" href="#technology-referenced-in-this-chapter" aria-label="Permalink to &quot;Technology referenced in this chapter&quot;"></a></h2>
<p><strong>Clipmap ring geometry.</strong> Each ring is a flat grid mesh centered on the camera with vertices displaced by heightmap samples. The inner ring uses full resolution. Each subsequent ring doubles vertex spacing and covers a larger area. The tricky part is the boundary: where a high-res ring meets a low-res ring, edge vertices on the finer mesh snap to the midpoint of the coarser mesh's edge. The technique originates from Losasso and Hoppe's SIGGRAPH 2004 paper (<a href="https://hhoppe.com/geomclipmap.pdf" target="_blank" rel="noreferrer">PDF</a>) and is detailed in <a href="https://developer.nvidia.com/gpugems/gpugems2/part-i-geometric-complexity/chapter-2-terrain-rendering-using-gpu-based-geometry" target="_blank" rel="noreferrer">GPU Gems 2, Chapter 2</a>. See our <a href="/guides/landscape-generation-browser.html#geometry-clipmaps">landscape guide on geometry clipmaps</a>.</p>
<p><strong>2:1 edge morphing.</strong> At the boundary between two clipmap rings, the finer ring has vertices at positions the coarser ring doesn't share. Boundary vertices whose grid coordinate is odd along the ring edge are detected and their height is interpolated between the two neighboring even vertices. This produces watertight seams without dedicated transition geometry. The interpolation runs in the vertex shader: <code>morphedHeight = mix(heightLeft, heightRight, 0.5)</code> for boundary vertices, using the same geomorphing framework described in <a href="/guides/landscape-generation-browser.html#geomorphing-pop-free-lod-transitions">our guide</a>.</p>
<p><strong>Equirectangular skybox mapping.</strong> A single 2D image that maps the full sphere of sky directions using longitude-latitude projection. The horizontal axis covers 0-360 degrees, the vertical axis covers 0-180 degrees. A normalized view direction <mjx-container class="MathJax" jax="SVG" style="direction: ltr; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -0.667ex;" xmlns="http://www.w3.org/2000/svg" width="14.501ex" height="3.07ex" role="img" focusable="false" viewBox="0 -1062 6409.6 1357" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(427,268) translate(-250 0)"><path data-c="5E" d="M112 560L249 694L257 686Q387 562 387 560L361 531Q359 532 303 581L250 627L195 580Q182 569 169 557T148 538L140 532Q138 530 125 546L112 560Z" style="stroke-width: 3;"/></g></g></g><g data-mml-node="mo" transform="translate(797.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(1853.6,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2242.6,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(553,-150) scale(0.707)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3250,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3694.7,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(553,-150) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(4644.2,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(5088.8,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(553,-150) scale(0.707)"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(6020.6,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="inline" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; width: auto; overflow: hidden;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow data-mjx-texclass="ORD"><mover><mi>d</mi><mo stretchy="false">^</mo></mover></mrow><mo>=</mo><mo stretchy="false">(</mo><msub><mi>d</mi><mi>x</mi></msub><mo>,</mo><msub><mi>d</mi><mi>y</mi></msub><mo>,</mo><msub><mi>d</mi><mi>z</mi></msub><mo stretchy="false">)</mo></math></mjx-assistive-mml></mjx-container> converts to UV coordinates by reading off its longitude and latitude:</p>
<mjx-container tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -1.577ex;" xmlns="http://www.w3.org/2000/svg" width="48.882ex" height="4.982ex" role="img" focusable="false" viewBox="0 -1505 21605.8 2202" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D462" d="M21 287Q21 295 30 318T55 370T99 420T158 442Q204 442 227 417T250 358Q250 340 216 246T182 105Q182 62 196 45T238 27T291 44T328 78L339 95Q341 99 377 247Q407 367 413 387T427 416Q444 431 463 431Q480 431 488 421T496 402L420 84Q419 79 419 68Q419 43 426 35T447 26Q469 29 482 57T512 145Q514 153 532 153Q551 153 551 144Q550 139 549 130T540 98T523 55T498 17T462 -8Q454 -10 438 -10Q372 -10 347 46Q345 45 336 36T318 21T296 6T267 -6T233 -11Q189 -11 155 7Q103 38 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(849.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(1905.6,0)"><g data-mml-node="mn" transform="translate(220,676)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(220,-686)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><rect width="700" height="60" x="120" y="220"/></g><g data-mml-node="mo" transform="translate(3067.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(4068,0)"><g data-mml-node="mrow" transform="translate(220,710)"><g data-mml-node="TeXAtom" data-mjx-texclass="OP"><g data-mml-node="mi"><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" style="stroke-width: 3;"/><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(889,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1389,0)" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(2111.7,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(2611.7,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(3000.7,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(553,-150) scale(0.707)"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3932.5,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(4210.5,0)"><g data-mml-node="mspace"/></g><g data-mml-node="msub" transform="translate(4544.1,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(553,-150) scale(0.707)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(5551.6,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mrow" transform="translate(2655.3,-686)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(500,0)"><path data-c="1D70B" d="M132 -11Q98 -11 98 22V33L111 61Q186 219 220 334L228 358H196Q158 358 142 355T103 336Q92 329 81 318T62 297T53 285Q51 284 38 284Q19 284 19 294Q19 300 38 329T93 391T164 429Q171 431 389 431Q549 431 553 430Q573 423 573 402Q573 371 541 360Q535 358 472 358H408L405 341Q393 269 393 222Q393 170 402 129T421 65T431 37Q431 20 417 5T381 -10Q370 -10 363 -7T347 17T331 77Q330 86 330 121Q330 170 339 226T357 318T367 358H269L268 354Q268 351 249 275T206 114T175 17Q164 -11 132 -11Z" style="stroke-width: 3;"/></g></g><rect width="6140.6" height="60" x="120" y="220"/></g><g data-mml-node="mo" transform="translate(10448.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z" style="stroke-width: 3;"/></g><g data-mml-node="mstyle" transform="translate(10726.6,0)"><g data-mml-node="mspace"/></g><g data-mml-node="mi" transform="translate(12893.3,0)"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(13656,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(14711.8,0)"><g data-mml-node="mn" transform="translate(220,676)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" style="stroke-width: 3;"/></g><g data-mml-node="mn" transform="translate(220,-686)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" style="stroke-width: 3;"/></g><rect width="700" height="60" x="120" y="220"/></g><g data-mml-node="mo" transform="translate(15874,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(16874.3,0)"><g data-mml-node="mrow" transform="translate(220,755)"><g data-mml-node="mi"><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" style="stroke-width: 3;"/><path data-c="72" d="M36 46H50Q89 46 97 60V68Q97 77 97 91T98 122T98 161T98 203Q98 234 98 269T98 328L97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 60 434T96 436Q112 437 131 438T160 441T171 442H174V373Q213 441 271 441H277Q322 441 343 419T364 373Q364 352 351 337T313 322Q288 322 276 338T263 372Q263 381 265 388T270 400T273 405Q271 407 250 401Q234 393 226 386Q179 341 179 207V154Q179 141 179 127T179 101T180 81T180 66V61Q181 59 183 57T188 54T193 51T200 49T207 48T216 47T225 47T235 46T245 46H276V0H267Q249 3 140 3Q37 3 28 0H20V46H36Z" transform="translate(500,0)" style="stroke-width: 3;"/><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" transform="translate(892,0)" style="stroke-width: 3;"/><path data-c="73" d="M295 316Q295 356 268 385T190 414Q154 414 128 401Q98 382 98 349Q97 344 98 336T114 312T157 287Q175 282 201 278T245 269T277 256Q294 248 310 236T342 195T359 133Q359 71 321 31T198 -10H190Q138 -10 94 26L86 19L77 10Q71 4 65 -1L54 -11H46H42Q39 -11 33 -5V74V132Q33 153 35 157T45 162H54Q66 162 70 158T75 146T82 119T101 77Q136 26 198 26Q295 26 295 104Q295 133 277 151Q257 175 194 187T111 210Q75 227 54 256T33 318Q33 357 50 384T93 424T143 442T187 447H198Q238 447 268 432L283 424L292 431Q302 440 314 448H322H326Q329 448 335 442V310L329 304H301Q295 310 295 316Z" transform="translate(1336,0)" style="stroke-width: 3;"/><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(1730,0)" style="stroke-width: 3;"/><path data-c="6E" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q450 438 463 329Q464 322 464 190V104Q464 66 466 59T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(2008,0)" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2564,0)"><path data-c="2061" d="" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(2564,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z" style="stroke-width: 3;"/></g><g data-mml-node="msub" transform="translate(2953,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(553,-150) scale(0.707)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mo" transform="translate(3902.5,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(2080.7,-686)"><path data-c="1D70B" d="M132 -11Q98 -11 98 22V33L111 61Q186 219 220 334L228 358H196Q158 358 142 355T103 336Q92 329 81 318T62 297T53 285Q51 284 38 284Q19 284 19 294Q19 300 38 329T93 391T164 429Q171 431 389 431Q549 431 553 430Q573 423 573 402Q573 371 541 360Q535 358 472 358H408L405 341Q393 269 393 222Q393 170 402 129T421 65T431 37Q431 20 417 5T381 -10Q370 -10 363 -7T347 17T331 77Q330 86 330 121Q330 170 339 226T357 318T367 358H269L268 354Q268 351 249 275T206 114T175 17Q164 -11 132 -11Z" style="stroke-width: 3;"/></g><rect width="4491.5" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>u</mi><mo>=</mo><mfrac><mn>1</mn><mn>2</mn></mfrac><mo>+</mo><mfrac><mrow><mrow data-mjx-texclass="OP"><mi>atan</mi><mn>2</mn></mrow><mo stretchy="false">(</mo><msub><mi>d</mi><mi>z</mi></msub><mo>,</mo><mstyle scriptlevel="0"><mspace width="0.167em"/></mstyle><msub><mi>d</mi><mi>x</mi></msub><mo stretchy="false">)</mo></mrow><mrow><mn>2</mn><mi>π</mi></mrow></mfrac><mo>,</mo><mstyle scriptlevel="0"><mspace width="2em"/></mstyle><mi>v</mi><mo>=</mo><mfrac><mn>1</mn><mn>2</mn></mfrac><mo>−</mo><mfrac><mrow><mi>arcsin</mi><mo data-mjx-texclass="NONE">⁡</mo><mo stretchy="false">(</mo><msub><mi>d</mi><mi>y</mi></msub><mo stretchy="false">)</mo></mrow><mi>π</mi></mfrac></math></mjx-assistive-mml></mjx-container><p>In Three.js, setting <code>texture.mapping = EquirectangularReflectionMapping</code> with <code>SRGBColorSpace</code> enables this as a scene background. In TSL, <code>equirectUV(direction)</code> applies that same conversion, turning a 3D view direction into the 2D UV coordinates for sampling the texture.</p>
<p><strong>Per-fragment fog color from sky.</strong> Standard fog blends fragments toward a single constant color. For a scene with a detailed skybox, this looks wrong because the sky color varies by direction. The fix is to compute the camera-to-fragment world direction per pixel (<code>positionWorld.sub(cameraPosition).normalize()</code>) and sample the skybox at that direction for the fog color. Each fragment fades toward the sky color that's actually behind it, producing correct blending from any camera angle. The fog factor uses <code>smoothstep(nearDist, farDist, viewDepth)</code> with <code>positionView.z.negate()</code> for camera-space depth.</p>
<p><strong>Import maps for ES modules.</strong> A browser-native mechanism (<code>&lt;script type=&quot;importmap&quot;&gt;</code>) that maps bare module specifiers (like <code>three/tsl</code>) to actual URLs. When Three.js 0.183.1 restructured its build outputs, <code>three/tsl</code> needed to resolve to <code>three.tsl.js</code> and TSL internally imported <code>three/webgpu</code> as a bare specifier. Both mappings had to be explicit in the import map, or the browser produced &quot;does not provide an export&quot; or &quot;failed to resolve module specifier&quot; errors.</p>
<h2 id="further-reading" tabindex="-1">Further reading <a class="header-anchor" href="#further-reading" aria-label="Permalink to &quot;Further reading&quot;"></a></h2>
<p>For deeper coverage of the technologies used throughout this series, see our companion guides:</p>
<ul>
<li><a href="/guides/landscape-generation-browser.html">Landscape Generation with Dynamic LOD and Streaming for Browser Open Worlds</a> covers heightmaps, SDFs, marching cubes, Transvoxel, geometry clipmaps, geomorphing, streaming architecture, terrain materials, and vegetation rendering.</li>
<li><a href="/guides/browser-3d-open-world-tech.html">Browser 3D Open World Tech for Multiplayer Creator Worlds</a> covers rendering stacks, WebGPU, physics, networking, multiplayer architecture, and lessons from Skyrim, The Witcher 3, Breath of the Wild, and GTA V.</li>
</ul>
<p>Thank you for following this twelve-part ride.</p>
<p>Part 1: <a href="/blog/2026-02-25-open-world-browser-part-01-risk-first.html">We started by trying to break it</a><br>
Part 2: <a href="/blog/2026-02-25-open-world-browser-part-02-worker-physics.html">Worker physics and the input lag fear</a><br>
Part 3: <a href="/blog/2026-02-25-open-world-browser-part-03-the-unflashy-spikes.html">The unflashy spikes that saved us</a><br>
Part 4: <a href="/blog/2026-02-25-open-world-browser-part-04-streaming-before-fancy.html">Streaming before fancy terrain</a><br>
Part 5: <a href="/blog/2026-02-25-open-world-browser-part-05-budgeting-the-pretty.html">Budgeting the pretty stuff</a><br>
Part 6: <a href="/blog/2026-02-25-open-world-browser-part-06-clipmaps.html">Clipmaps changed the plot</a><br>
Part 7: <a href="/blog/2026-02-25-open-world-browser-part-07-marching-cubes.html">Marching cubes and the first real caves</a><br>
Part 8: <a href="/blog/2026-02-25-open-world-browser-part-08-webgpu-integration.html">Integration without losing our baseline</a><br>
Part 9: <a href="/blog/2026-02-25-open-world-browser-part-09-transvoxel-first-cut.html">Transvoxel started with a scaffold</a><br>
Part 10: <a href="/blog/2026-02-25-open-world-browser-part-10-seam-chaos.html">Seam chaos and the corner boss fight</a><br>
Part 11: <a href="/blog/2026-02-25-open-world-browser-part-11-policy-modes.html">Policy mode, not hardcoded mode</a></p>
<hr>
<p>Part 12 of 14.<br>
Previous: <a href="/blog/2026-02-25-open-world-browser-part-11-policy-modes.html">Part 11 - Policy mode, not hardcoded mode</a><br>
Next: <a href="/blog/2026-04-13-open-world-browser-part-13-terrain-sculpting.html">Part 13 - Terrain sculpting and the death of the math function</a><br>
Series guide: <a href="/blog/2026-02-25-open-world-browser-series-guide.html">/blog/2026-02-25-open-world-browser-series-guide</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Open world in browser series guide]]></title>
            <link>https://app.cinevva.com/blog/2026-02-25-open-world-browser-series-guide</link>
            <guid>https://app.cinevva.com/blog/2026-02-25-open-world-browser-series-guide</guid>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Reading order for the 29-part open world browser series, plus a plain-English spike glossary and live demo map.]]></description>
            <content:encoded><![CDATA[<h1 id="open-world-in-browser-series-guide" tabindex="-1">Open world in browser series guide <a class="header-anchor" href="#open-world-in-browser-series-guide" aria-label="Permalink to &quot;Open world in browser series guide&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO and Co-Founder of Cinevva</em></p>
<p>This page is the navigation hub for the full series.</p>
<p>If you arrived from one random part, start here.</p>
<p>March 2026 update: we added an external implementation cross-check from the Vuntra City devlogs in <a href="/blog/2026-02-25-open-world-browser-part-12-lessons.html">Part 12</a> and expanded the companion <a href="/guides/browser-3d-open-world-tech.html">browser open world tech guide</a> with the same findings.</p>
<p>April 2026 update: Parts 13-14 cover terrain sculpting (Spikes 25-27) and the visual/gameplay layer: multi-material texturing, instanced vegetation, and a character controller with HM+SDF collision (Spikes 28-30).</p>
<p>May 2026 update: the series ran to 29 parts. Parts 15-29 cover the long middle of the build, multiplayer terrain and water, a code-structure refactor, animations and AI scattering, imposters and parallax, the visibility buffer that didn't pay off, clouds and meshlet culling, batched avatars and proximity voice, world persistence and GPU wind, universal characters, three takes on water, a procedural island, grass and occlusion, and a pluggable character controller that drives any body (Spikes 31-60).</p>
<p>June 2026 update: Part 30 adds a collision-aware camera, a spring-arm boom with a pluggable collider contract and a signed-distance probe that stops the view against overhangs and cave ceilings, not just hills.</p>
<h2 id="what-spike-means-in-this-series" tabindex="-1">What &quot;spike&quot; means in this series <a class="header-anchor" href="#what-spike-means-in-this-series" aria-label="Permalink to &quot;What &quot;spike&quot; means in this series&quot;"></a></h2>
<p>A spike is a short, focused experiment.</p>
<p>Each spike tests one risky question. We keep scope tight, measure what matters, and decide what to do next. Think of it as a learning instrument, not a polished feature.</p>
<p>In this project, spikes are mostly standalone pages under <code>/spikes/*</code> that you can run live.</p>
<h2 id="read-the-series-in-order" tabindex="-1">Read the series in order <a class="header-anchor" href="#read-the-series-in-order" aria-label="Permalink to &quot;Read the series in order&quot;"></a></h2>
<ol>
<li><a href="/blog/2026-02-25-open-world-browser-part-01-risk-first.html">Part 1: We started by trying to break it</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-02-worker-physics.html">Part 2: Worker physics and the input lag fear</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-03-the-unflashy-spikes.html">Part 3: The unflashy spikes that saved us</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-04-streaming-before-fancy.html">Part 4: Streaming before fancy terrain</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-05-budgeting-the-pretty.html">Part 5: Budgeting the pretty stuff</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-06-clipmaps.html">Part 6: Clipmaps changed the plot</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-07-marching-cubes.html">Part 7: Marching cubes and the first real caves</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-08-webgpu-integration.html">Part 8: Integration without losing our baseline</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-09-transvoxel-first-cut.html">Part 9: Transvoxel started with a scaffold</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-10-seam-chaos.html">Part 10: Seam chaos and the corner boss fight</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-11-policy-modes.html">Part 11: Policy mode, not hardcoded mode</a></li>
<li><a href="/blog/2026-02-25-open-world-browser-part-12-lessons.html">Part 12: Rings, sky fog, and what we would do again</a></li>
<li><a href="/blog/2026-04-13-open-world-browser-part-13-terrain-sculpting.html">Part 13: Terrain sculpting and the death of the math function</a></li>
<li><a href="/blog/2026-04-13-open-world-browser-part-14-world-comes-alive.html">Part 14: The world comes alive</a></li>
<li><a href="/blog/2026-04-13-open-world-browser-part-15-multiplayer-and-water.html">Part 15: Replace the baseline, then sync it</a></li>
<li><a href="/blog/2026-04-28-open-world-browser-part-16-structure-and-authoring.html">Part 16: Structure for a world that keeps growing</a></li>
<li><a href="/blog/2026-05-01-open-world-browser-part-17-animations-and-search.html">Part 17: Animations that didn't need retargeting, and a live asset search</a></li>
<li><a href="/blog/2026-05-02-open-world-browser-part-18-ai-scattering.html">Part 18: A scatter brush that feels AI-placed</a></li>
<li><a href="/blog/2026-05-04-open-world-browser-part-19-imposters.html">Part 19: The imposter that has to survive a forest</a></li>
<li><a href="/blog/2026-05-05-open-world-browser-part-20-parallax-occlusion.html">Part 20: Faking depth on a flat plane</a></li>
<li><a href="/blog/2026-05-06-open-world-browser-part-21-visibility-buffer.html">Part 21: A faster renderer that wasn't faster</a></li>
<li><a href="/blog/2026-05-07-open-world-browser-part-22-clouds-and-meshlets.html">Part 22: Clouds you can light, and culling that has to be fed</a></li>
<li><a href="/blog/2026-05-08-open-world-browser-part-23-avatars-and-voice.html">Part 23: Fifty avatars and a voice in the room</a></li>
<li><a href="/blog/2026-05-09-open-world-browser-part-24-persistence-and-wind.html">Part 24: Saving a world, and wind you can see</a></li>
<li><a href="/blog/2026-05-10-open-world-browser-part-25-universal-characters.html">Part 25: One skeleton, every outfit</a></li>
<li><a href="/blog/2026-05-12-open-world-browser-part-26-water.html">Part 26: Three ways to make water</a></li>
<li><a href="/blog/2026-05-13-open-world-browser-part-27-island-and-terrain.html">Part 27: An island from noise, ground that looks like ground</a></li>
<li><a href="/blog/2026-05-14-open-world-browser-part-28-grass-and-occlusion.html">Part 28: Grass to the horizon, and ground that hides itself</a></li>
<li><a href="/blog/2026-05-15-open-world-browser-part-29-pluggable-character.html">Part 29: One controller, any body</a></li>
<li><a href="/blog/2026-06-04-open-world-browser-part-30-collision-aware-camera.html">Part 30: A camera that respects walls</a></li>
</ol>
<h2 id="live-spike-map" tabindex="-1">Live spike map <a class="header-anchor" href="#live-spike-map" aria-label="Permalink to &quot;Live spike map&quot;"></a></h2>
<p>Start with these if you want to click through the technical timeline directly.</p>
<p><a href="/spikes/01-terrain/" target="_blank">Spike 1</a> terrain and instancing · <a href="/blog/spike-source?spike=01-terrain">source</a><br>
<a href="/spikes/02-rapier-worker/" target="_blank">Spike 2</a> worker physics · <a href="/blog/spike-source?spike=02-rapier-worker">source</a><br>
Spike 3 Durable Object broadcast load test ran as a service script, not a standalone page<br>
Spike 4 mobile quality constraints, tested via Spike 1 query param<br>
Spike 5 LLM behavior reliability, ran as a script, not a standalone page<br>
<a href="/spikes/06-chunk-streaming/" target="_blank">Spike 6</a> chunk load and swap · <a href="/blog/spike-source?spike=06-chunk-streaming">source</a><br>
<a href="/spikes/07-gpu-vegetation/" target="_blank">Spike 7</a> vegetation from density maps · <a href="/blog/spike-source?spike=07-gpu-vegetation">source</a><br>
<a href="/spikes/08-terrain-material/" target="_blank">Spike 8</a> terrain material cost · <a href="/blog/spike-source?spike=08-terrain-material">source</a><br>
<a href="/spikes/09-csm-shadows/" target="_blank">Spike 9</a> cascaded shadows budget · <a href="/blog/spike-source?spike=09-csm-shadows">source</a><br>
<a href="/spikes/10-clipmap-geomorph/" target="_blank">Spike 10</a> clipmaps and geomorphing · <a href="/blog/spike-source?spike=10-clipmap-geomorph">source</a><br>
<a href="/spikes/11-chunk-streaming/" target="_blank">Spike 11</a> heightmap chunk streaming · <a href="/blog/spike-source?spike=11-chunk-streaming">source</a><br>
<a href="/spikes/12-webgpu-marching-cubes/" target="_blank">Spike 12</a> WebGPU marching cubes · <a href="/blog/spike-source?spike=12-webgpu-marching-cubes">source</a><br>
<a href="/spikes/13-threejs-webgpu/" target="_blank">Spike 13</a> WebGPU integration baseline · <a href="/blog/spike-source?spike=13-threejs-webgpu">source</a><br>
<a href="/spikes/14-threejs-webgpu-incremental/" target="_blank">Spike 14</a> incremental hardening · <a href="/blog/spike-source?spike=14-threejs-webgpu-incremental">source</a><br>
<a href="/spikes/15-transvoxel-seam/" target="_blank">Spike 15</a> seam scaffold · <a href="/blog/spike-source?spike=15-transvoxel-seam">source</a><br>
<a href="/spikes/16-transvoxel-face/" target="_blank">Spike 16</a> first Transvoxel face · <a href="/blog/spike-source?spike=16-transvoxel-face">source</a><br>
<a href="/spikes/17-dual-mc-lod/" target="_blank">Spike 17</a> dual MC LOD · <a href="/blog/spike-source?spike=17-dual-mc-lod">source</a><br>
<a href="/spikes/18-transvoxel-heightmap-seam/" target="_blank">Spike 18</a> heightmap seam · <a href="/blog/spike-source?spike=18-transvoxel-heightmap-seam">source</a><br>
<a href="/spikes/19-transvoxel-corner-grid/" target="_blank">Spike 19</a> mixed-resolution corner · <a href="/blog/spike-source?spike=19-transvoxel-corner-grid">source</a><br>
<a href="/spikes/20-gpu-transvoxel-corner/" target="_blank">Spike 20</a> GPU corner seam · <a href="/blog/spike-source?spike=20-gpu-transvoxel-corner">source</a><br>
<a href="/spikes/21-gpu-mc-transvoxel-corner/" target="_blank">Spike 21</a> GPU MC corner seam · <a href="/blog/spike-source?spike=21-gpu-mc-transvoxel-corner">source</a><br>
<a href="/spikes/22-gpu-mc-heightmap-fallback/" target="_blank">Spike 22</a> MC to HM fallback · <a href="/blog/spike-source?spike=22-gpu-mc-heightmap-fallback">source</a><br>
<a href="/spikes/23-policy-chunk-modes/" target="_blank">Spike 23</a> policy-based chunk modes · <a href="/blog/spike-source?spike=23-policy-chunk-modes">source</a><br>
<a href="/spikes/24-gpu-clipmap-rings/" target="_blank">Spike 24</a> clipmap rings and sky fog · <a href="/blog/spike-source?spike=24-gpu-clipmap-rings">source</a><br>
<a href="/spikes/25-heightmap-brush/" target="_blank">Spike 25</a> heightmap brush on sampled data · <a href="/blog/spike-source?spike=25-heightmap-brush">source</a><br>
<a href="/spikes/26-sdf-brush/" target="_blank">Spike 26</a> SDF brush via GPU compute · <a href="/blog/spike-source?spike=26-sdf-brush">source</a><br>
<a href="/spikes/27-hm-mc-brush/" target="_blank">Spike 27</a> HM + MC + brush integration · <a href="/blog/spike-source?spike=27-hm-mc-brush">source</a><br>
<a href="/spikes/28-multi-material/" target="_blank">Spike 28</a> multi-material texturing · <a href="/blog/spike-source?spike=28-multi-material">source</a><br>
<a href="/spikes/29-vegetation/" target="_blank">Spike 29</a> vegetation and shader grass · <a href="/blog/spike-source?spike=29-vegetation">source</a><br>
<a href="/spikes/30-physics/" target="_blank">Spike 30</a> terrain physics and character controller · <a href="/blog/spike-source?spike=30-physics">source</a><br>
<a href="/spikes/31-multiplayer-sync/" target="_blank">Spike 31</a> multiplayer terrain sync · <a href="/blog/spike-source?spike=31-multiplayer-sync">source</a><br>
<a href="/spikes/32-water-swimming/" target="_blank">Spike 32</a> water and swimming · <a href="/blog/spike-source?spike=32-water-swimming">source</a><br>
<a href="/spikes/33-code-structure/" target="_blank">Spike 33</a> code structure refactor · <a href="/blog/spike-source?spike=33-code-structure">source</a><br>
<a href="/spikes/34-world-authoring/" target="_blank">Spike 34</a> in-world authoring · <a href="/blog/spike-source?spike=34-world-authoring">source</a><br>
<a href="/spikes/35-combat-animations/" target="_blank">Spike 35</a> combat animations and retargeting · <a href="/blog/spike-source?spike=35-combat-animations">source</a><br>
<a href="/spikes/36-polyhaven-models/" target="_blank">Spike 36</a> live Poly Haven asset search · <a href="/blog/spike-source?spike=36-polyhaven-models">source</a><br>
<a href="/spikes/37-ai-scattering/" target="_blank">Spike 37</a> AI-assisted prop scattering · <a href="/blog/spike-source?spike=37-ai-scattering">source</a><br>
<a href="/spikes/38-imposters/" target="_blank">Spike 38</a> octahedral imposters · <a href="/blog/spike-source?spike=38-imposters">source</a><br>
<a href="/spikes/39-parallax-pom/" target="_blank">Spike 39</a> parallax occlusion mapping · <a href="/blog/spike-source?spike=39-parallax-pom">source</a><br>
<a href="/spikes/40-visibility-buffer/" target="_blank">Spike 40</a> visibility buffer renderer · <a href="/blog/spike-source?spike=40-visibility-buffer">source</a><br>
<a href="/spikes/41-imposter-forest/" target="_blank">Spike 41</a> imposters in a forest · <a href="/blog/spike-source?spike=41-imposter-forest">source</a><br>
<a href="/spikes/42-imposter-flicker/" target="_blank">Spike 42</a> imposter flicker fix · <a href="/blog/spike-source?spike=42-imposter-flicker">source</a><br>
<a href="/spikes/43-clouds-atmosphere/" target="_blank">Spike 43</a> atmosphere and volumetric clouds · <a href="/blog/spike-source?spike=43-clouds-atmosphere">source</a><br>
<a href="/spikes/44-meshlet-clusters/" target="_blank">Spike 44</a> meshlet Hi-Z culling · <a href="/blog/spike-source?spike=44-meshlet-clusters">source</a><br>
<a href="/spikes/45-avatar-network-sync/" target="_blank">Spike 45</a> batched avatar skinning and network sync · <a href="/blog/spike-source?spike=45-avatar-network-sync">source</a><br>
<a href="/spikes/46-proximity-voice/" target="_blank">Spike 46</a> proximity voice chat · <a href="/blog/spike-source?spike=46-proximity-voice">source</a><br>
<a href="/spikes/47-world-persistence/" target="_blank">Spike 47</a> server-authoritative world persistence · <a href="/blog/spike-source?spike=47-world-persistence">source</a><br>
Spike 48 avatar rig integration, reused as shared code by later spikes (no standalone page)<br>
<a href="/spikes/49-gpu-wind-props/" target="_blank">Spike 49</a> GPU wind on props and foliage · <a href="/blog/spike-source?spike=49-gpu-wind-props">source</a><br>
<a href="/spikes/50-universal-characters/" target="_blank">Spike 50</a> universal characters and wearables · <a href="/blog/spike-source?spike=50-universal-characters">source</a><br>
<a href="/spikes/51-water-ssr-caustics/" target="_blank">Spike 51</a> screen-space reflection water · <a href="/blog/spike-source?spike=51-water-ssr-caustics">source</a><br>
<a href="/spikes/52-water-planar/" target="_blank">Spike 52</a> planar mirror water · <a href="/blog/spike-source?spike=52-water-planar">source</a><br>
<a href="/spikes/53-water-pro/" target="_blank">Spike 53</a> production water library · <a href="/blog/spike-source?spike=53-water-pro">source</a><br>
<a href="/spikes/54-procgen-island/" target="_blank">Spike 54</a> procedural island generation · <a href="/blog/spike-source?spike=54-procgen-island">source</a><br>
<a href="/spikes/55-terrain-shading/" target="_blank">Spike 55</a> terrain shading and tiling breakup · <a href="/blog/spike-source?spike=55-terrain-shading">source</a><br>
<a href="/spikes/56-grass/" target="_blank">Spike 56</a> GPU cross-quad grass · <a href="/blog/spike-source?spike=56-grass">source</a><br>
<a href="/spikes/57-terrain-occlusion/" target="_blank">Spike 57</a> terrain occlusion culling · <a href="/blog/spike-source?spike=57-terrain-occlusion">source</a><br>
<a href="/spikes/58-pluggable-character/" target="_blank">Spike 58</a> pluggable character controller · <a href="/blog/spike-source?spike=58-pluggable-character">source</a><br>
<a href="/spikes/59-pluggable-with-avatar/" target="_blank">Spike 59</a> controller with a real avatar · <a href="/blog/spike-source?spike=59-pluggable-with-avatar">source</a><br>
<a href="/spikes/60-polygon-animations/" target="_blank">Spike 60</a> Synty polygon animations · <a href="/blog/spike-source?spike=60-polygon-animations">source</a></p>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/why-webgpu-engine-hero.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[A Breaker Belt: Snake meets Arkanoid, vibe coded in three days]]></title>
            <link>https://app.cinevva.com/blog/2026-02-18-a-breaker-belt</link>
            <guid>https://app.cinevva.com/blog/2026-02-18-a-breaker-belt</guid>
            <pubDate>Wed, 18 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A cosmic serpent breaks bricks across 50 waves with reactive music, AI narration, and 23 brick types. Two people. Three days. Web, mobile, and PC.]]></description>
            <content:encoded><![CDATA[<h1 id="a-breaker-belt-snake-meets-arkanoid-vibe-coded-in-three-days" tabindex="-1">A Breaker Belt: Snake meets Arkanoid, vibe coded in three days <a class="header-anchor" href="#a-breaker-belt-snake-meets-arkanoid-vibe-coded-in-three-days" aria-label="Permalink to &quot;A Breaker Belt: Snake meets Arkanoid, vibe coded in three days&quot;"></a></h1>
<p><em>By <a href="/about.html">Mariana Muntean</a>, CEO of Cinevva</em></p>
<img src="https://cdn.cinevva.com/blog/breaker-belt-cover.webp" alt="A Breaker Belt gameplay: a neon cosmic serpent breaking bricks in space" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>We made a game where your snake is the paddle and the bricks fight back. It shipped on web, mobile, and PC. It took two of us about three days. On and off, not crunching. And it's genuinely fun to play.</p>
<p>That last part is the thing worth paying attention to.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0">
<iframe src="https://www.youtube.com/embed/VaFkfCT3OuU" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0" allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture" allowfullscreen></iframe>
</div>
<h2 id="the-mashup-nobody-asked-for" tabindex="-1">The mashup nobody asked for <a class="header-anchor" href="#the-mashup-nobody-asked-for" aria-label="Permalink to &quot;The mashup nobody asked for&quot;"></a></h2>
<p>Snake is about growth and spatial awareness. Arkanoid is about reflexes and angle prediction. They come from completely different design philosophies, and mashing them together sounds like the kind of pitch that gets politely declined.</p>
<p>But <a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">A Breaker Belt</a> makes it work. You're a cosmic serpent, a living arc of neon current stitched from drift-metal vertebrae and comet silk, threading through an asteroid field of breakable blocks. Your head is the paddle. Your growing tail is both your greatest weapon and your most constant threat. The orbs ricochet off your body to shatter bricks, but one wrong turn into your own tail and you're done.</p>
<p>It's the kind of weird cross-genre experiment that usually dies before anyone gets to play it, because the development cost of finding out whether a weird idea works has traditionally been measured in months. Here it was measured in afternoons.</p>
<h2 id="what-actually-shipped" tabindex="-1">What actually shipped <a class="header-anchor" href="#what-actually-shipped" aria-label="Permalink to &quot;What actually shipped&quot;"></a></h2>
<p>The scope is what makes this interesting. This isn't a game jam prototype with placeholder rectangles and no sound.</p>
<p>The game runs 50 waves deep. That's not 50 variations of the same brick wall. The formations evolve from gentle onboarding arcs into fortress rings that demand angled shots through side gaps, then into layered diagonal mazes with one-tile-wide openings that require precision steering. Explosive bricks blow their neighbors apart. Phantom bricks flicker in and out of existence. Regenerating bricks heal back after you break them, forcing you to prioritize targets instead of sweeping left to right. Portal bricks teleport your orbs across the arena. Gravity wells bend your shots into slow, curving hymns. Laser emitters sketch red lines across the void. Mimic bricks look harmless until they decide they're not. By wave 15, you're navigating something that feels less like a puzzle and more like a living system that's learning your habits.</p>
<p>The music isn't a loop. It's a reactive soundtrack that builds with your gameplay. The underlying system runs a bass line, lead synth, pad, kick, snare, and hi-hat in E minor at 140 BPM, and as the action intensifies, additional percussion and synth layers fade in. When things calm down between waves, ambient pads take over. The music breathes with you. A dedicated composer would spend weeks tuning that kind of responsiveness. Here it was part of the creative flow.</p>
<p>The sound effects aren't samples pulled from a free pack. Every brick shatter, power-up pickup, orb bounce, and collision is synthesized in real time. Different pitch for head contacts versus tail contacts. Warm reverb for the spacey feel. Lowpass filtering to keep things satisfying without getting harsh. When you chain a combo, the audio tells you before the screen does.</p>
<p>And then there's the storytelling. Each wave opens with a narrative beat delivered by AI-generated voice. The writing has genuine personality. Wave one: &quot;They call it the Breaker Belt: a ribbon of engineered debris that circles the old star like a warning.&quot; By wave 38: &quot;The Belt stops feeling like a wall and starts feeling like a mind. It tests not your reflexes, but your habits. You break the habit. The Belt notices.&quot; It's 50 chapters of cosmic mythology that makes you care about why you're a snake breaking bricks in space.</p>
<p>The backgrounds evolve too. Early waves are calm indigo starfields with soft meteor rain. By mid-game, aurora bands and nebula clouds appear. Late game goes full Ion Storm with sharp cyan streaks against near-black space. Wave 15 and beyond, you're in Event Horizon territory: deep void punctuated by slow-moving nebula filaments and faint black hole lensing. The game communicates progression through atmosphere as much as difficulty.</p>
<p>All of this runs on keyboard, gamepad, or touchscreen. Published to web, mobile, and PC from one codebase. No separate builds. No porting.</p>
<h2 id="the-team-that-wasn-t-needed" tabindex="-1">The team that wasn't needed <a class="header-anchor" href="#the-team-that-wasn-t-needed" aria-label="Permalink to &quot;The team that wasn't needed&quot;"></a></h2>
<p>Here's the part that should make any game developer pause.</p>
<p>A game with this depth of content would typically need a game designer, a couple of programmers, a 2D artist, a sound designer, a music composer, a level designer, a narrative writer, and QA. That's eight or nine people. At a modest indie pace, you're looking at three to six months of coordinated work. Standups, Jira tickets, asset pipelines, platform-specific debugging.</p>
<p>Two of us made this in a long weekend on the <a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">Cinevva Engine</a>.</p>
<p>The talent required didn't change. The ratio between creative intent and implementation overhead did. The time was spent deciding what the game should feel like, not fighting tools to make it happen.</p>
<h2 id="why-this-matters-if-you-make-things" tabindex="-1">Why this matters if you make things <a class="header-anchor" href="#why-this-matters-if-you-make-things" aria-label="Permalink to &quot;Why this matters if you make things&quot;"></a></h2>
<p>The interesting question isn't whether AI tools can help make games faster. That's been answered. The interesting question is what happens to ideas that used to be too risky to try.</p>
<p>&quot;Snake but you're the paddle in an Arkanoid arena&quot; is not something a producer greenlights. A few indie devs have tried variations of this mashup on itch.io (<a href="https://newdron.itch.io/breaksnake" target="_blank" rel="noreferrer">BreakSnake</a>, <a href="https://neop87.itch.io/snakeout" target="_blank" rel="noreferrer">SnakeOut</a>, <a href="https://merrak.itch.io/snake-break" target="_blank" rel="noreferrer">Snake Break</a>), but they're all small game jam experiments. The genre fusion has never been given a real production pass with reactive music, AI narration, dozens of brick types, and 50 waves of escalating level design. In traditional development, that kind of polish on a risky concept gets killed in a meeting before anyone writes a line of code.</p>
<p>When trying something weird costs an afternoon instead of a quarter's budget, the strange ideas get built. Some of them turn out to be genuinely good. A Breaker Belt is one of those.</p>
<h2 id="play-it-or-make-your-own" tabindex="-1">Play it. Or make your own. <a class="header-anchor" href="#play-it-or-make-your-own" aria-label="Permalink to &quot;Play it. Or make your own.&quot;"></a></h2>
<p><a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">A Breaker Belt</a> is playable right now on web, mobile, and PC. If it makes you want to build something, the <a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">Cinevva Engine</a> is free to use. Describe what you want, iterate on what comes back, ship when it's ready. The <a href="https://app.cinevva.com/tools/music" target="_blank" rel="noreferrer">music</a>, <a href="https://app.cinevva.com/tools/sfx" target="_blank" rel="noreferrer">sound effects</a>, <a href="https://app.cinevva.com/tools/flux" target="_blank" rel="noreferrer">art</a>, and <a href="https://app.cinevva.com/tools/hunyuan3d" target="_blank" rel="noreferrer">3D models</a> are all built in.</p>
<p>Your weird game idea might be three days away from existing.</p>
<hr>
<p><em><a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">Play A Breaker Belt</a> | <a href="https://app.cinevva.com/engine" target="_blank" rel="noreferrer">Build your own game</a> | <a href="https://cinevva.com/charts" target="_blank" rel="noreferrer">Browse community games</a></em></p>
<p><strong>Related:</strong></p>
<ul>
<li><a href="/signals/2026-03-13-vibe-coding-new-game-jam.html">Vibe coding is the new game jam</a> — how AI tools collapse the gap between idea and prototype</li>
<li><a href="/guides/game-jams-hackathons.html">Game Jams &amp; Hackathons</a> — the jam format that makes weird mashups possible</li>
<li><a href="/guides/web-game-engines-comparison.html">Web Game Engines Comparison</a> — engines for shipping to web, mobile, and PC</li>
</ul>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/open-world-browser-series-cover.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[We didn't expect to build a radio station]]></title>
            <link>https://app.cinevva.com/blog/2026-02-17-we-didnt-expect-a-radio-station</link>
            <guid>https://app.cinevva.com/blog/2026-02-17-we-didnt-expect-a-radio-station</guid>
            <pubDate>Tue, 17 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We have a music generator on Cinevva so game creators can make soundtracks. People started making music just because they could. 362 tracks later, Cinevva Radio is live.]]></description>
            <content:encoded><![CDATA[<h1 id="we-didn-t-expect-to-build-a-radio-station" tabindex="-1">We didn't expect to build a radio station <a class="header-anchor" href="#we-didn-t-expect-to-build-a-radio-station" aria-label="Permalink to &quot;We didn't expect to build a radio station&quot;"></a></h1>
<p><em>By <a href="/about.html">Mariana Muntean</a>, CEO of Cinevva</em></p>
<img src="https://cdn.cinevva.com/blog/cinevva-radio-lofi-cafe.png" alt="Cinevva Community Radio with 8 stations including Soundstage, Voltage, Lo-Fi Cafe, Main Stage, Ivory Tower, and Electric Dreams, currently playing Quiet Flute Drone" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<p>We have a <a href="https://app.cinevva.com/tools/music" target="_blank" rel="noreferrer">music generator</a> on the Cinevva platform so game creators can make game soundtracks and SFX without licensing headaches. What happened next, we did not plan for. People started making music because they wanted to make music. Not for games or projects. Just music production.</p>
<h2 id="bella-bay-made-over-100-tracks-in-two-weeks" tabindex="-1">Bella Bay made over 100 tracks in two weeks <a class="header-anchor" href="#bella-bay-made-over-100-tracks-in-two-weeks" aria-label="Permalink to &quot;Bella Bay made over 100 tracks in two weeks&quot;"></a></h2>
<p>I'm not exaggerating. One of our creators, Bella Bay, generated over 100 tracks in about two weeks. When you listen through them on <a href="https://cinevva.com/radio" target="_blank" rel="noreferrer">Cinevva Radio</a>, you can hear the progression. The early tracks are experiments. The later ones sound like someone who found their style. That kind of creative acceleration doesn't happen when you're fighting your tools. It happens when the tools get out of the way.</p>
<p>Bella Bay isn't an outlier in spirit, just in volume. All across the platform, people started treating the music generator not as a utility for game assets but as a creative instrument. Producers experimenting with genres they'd never tried. People iterating on a sound until it clicked. Bedroom artists who'd never had access to a full production toolkit suddenly had one and went all in.</p>
<h2 id="so-we-built-a-radio" tabindex="-1">So we built a radio <a class="header-anchor" href="#so-we-built-a-radio" aria-label="Permalink to &quot;So we built a radio&quot;"></a></h2>
<p>It had to be done.</p>
<p>We had all these tracks sitting in people's accounts with no collective space. No way to discover what others were making. No way to stumble onto a track that changes your afternoon.</p>
<p><a href="https://cinevva.com/radio" target="_blank" rel="noreferrer">Cinevva Radio</a> is now live. Eight stations. 362 community-created tracks and growing every day. Every single song was made by someone on the platform. You open it and something is playing. You don't have to search. You don't have to decide. You just listen.</p>
<p><strong><a href="https://cinevva.com/radio#cinematic" target="_blank" rel="noreferrer">Soundstage</a></strong> is our biggest station. 111 tracks of cinematic and epic scores. I honestly didn't see that coming. People love making dramatic, film-style music.</p>
<p><strong><a href="https://cinevva.com/radio#mixed" target="_blank" rel="noreferrer">Discovery</a></strong> caught everything that doesn't fit neatly into a genre. 78 tracks. Latin beats next to Afrobeats next to country next to something that defies description. This station is wild.</p>
<p><strong><a href="https://cinevva.com/radio#pop" target="_blank" rel="noreferrer">Main Stage</a></strong> is pop and vocal tracks. 56 tracks with full lyrics and vocals. People are writing love songs, breakup songs, worship music, ballads. Some of it is genuinely catchy.</p>
<p><strong><a href="https://cinevva.com/radio#rock" target="_blank" rel="noreferrer">Voltage</a></strong> is rock and metal. 53 tracks. Guitar riffs, punk energy, grunge, hard rock. I keep this one on while working.</p>
<p><strong><a href="https://cinevva.com/radio#lofi" target="_blank" rel="noreferrer">Lo-Fi Cafe</a></strong> is exactly what it sounds like. 25 chill ambient tracks for studying, working, or just vibing.</p>
<p><strong><a href="https://cinevva.com/radio#electronic" target="_blank" rel="noreferrer">Electric Dreams</a></strong> has 22 electronic and synth tracks. Synthwave, techno, EDM, drum and bass.</p>
<p><strong><a href="https://cinevva.com/radio#classical" target="_blank" rel="noreferrer">Ivory Tower</a></strong> is classical and orchestral. 10 tracks. Piano pieces, string arrangements, symphonic stuff. Small but growing.</p>
<p><strong><a href="https://cinevva.com/radio#hiphop" target="_blank" rel="noreferrer">The Cipher</a></strong> is hip-hop and rap. 7 tracks so far. Newest station, still finding its voice.</p>
<h2 id="what-you-see-when-you-listen" tabindex="-1">What you see when you listen <a class="header-anchor" href="#what-you-see-when-you-listen" aria-label="Permalink to &quot;What you see when you listen&quot;"></a></h2>
<p>When you open <a href="https://cinevva.com/radio" target="_blank" rel="noreferrer">Cinevva Radio</a>, it's a live stream. Think of it less like a playlist app and more like an actual radio station. Tracks play continuously. You can react in real time, chat with other listeners, and see who made each track.</p>
<p>Creators get credited on screen while their track plays. Their name, their prompt, the genre. If you hear something you like, you know who made it. It's community-created content with a discovery layer on top. No algorithms deciding what's worthy. No gatekeepers. You made a track, it goes on the station that matches its genre, and people hear it.</p>
<h2 id="this-is-bigger-than-a-feature" tabindex="-1">This is bigger than a feature <a class="header-anchor" href="#this-is-bigger-than-a-feature" aria-label="Permalink to &quot;This is bigger than a feature&quot;"></a></h2>
<p>For decades, music production required expensive software, years of training, and access to equipment most people couldn't afford. What we're seeing is a different kind of creator. People who think in descriptions and feelings rather than notes and time signatures.</p>
<p>&quot;Rainy night jazz with a broken piano&quot; is a creative direction. The person who wrote that prompt made an artistic choice. They directed the mood, the instrumentation, the emotional register. They just don't happen to play piano.</p>
<p>I don't think that makes their output less valid. I think it means the definition of &quot;musician&quot; is stretching. And watching someone like Bella Bay go from zero to a hundred tracks in two weeks tells me the creative drive was always there. The tools just weren't.</p>
<h2 id="what-s-next" tabindex="-1">What's next <a class="header-anchor" href="#what-s-next" aria-label="Permalink to &quot;What's next&quot;"></a></h2>
<p>We're working on community profiles where producers can showcase their catalog and build a following. The <a href="https://cinevva.com/charts" target="_blank" rel="noreferrer">charts page</a> already shows community creations across all tools, and we want to bring that same energy specifically to music.</p>
<p>But right now, the radio is live. <a href="https://cinevva.com/radio" target="_blank" rel="noreferrer">Go listen</a>. And if you want to make your own tracks, the <a href="https://app.cinevva.com/tools/music" target="_blank" rel="noreferrer">music generator</a> is free to use. Your track might end up on the air.</p>
<hr>
<p><em><a href="https://cinevva.com/radio" target="_blank" rel="noreferrer">Listen to Cinevva Radio</a> | <a href="https://app.cinevva.com/tools/music" target="_blank" rel="noreferrer">Make music</a> | <a href="https://cinevva.com/charts" target="_blank" rel="noreferrer">Browse community creations</a></em></p>
<p><strong>Related:</strong></p>
<ul>
<li><a href="/guides/frontier-gen-ai-models.html">Frontier Open-Source Gen AI Models</a> — the AI music and audio models behind the scenes</li>
<li><a href="/tutorials/web-audio-api-games.html">Web Audio API for games</a> — building interactive audio in the browser</li>
<li><a href="/signals/2026-03-13-vibe-coding-new-game-jam.html">Vibe coding is the new game jam</a> — how describing intent becomes the primary creative input</li>
</ul>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/cinevva-radio-lofi-cafe.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[The intuitive mind in an age of AI]]></title>
            <link>https://app.cinevva.com/blog/2026-02-10-the-intuitive-mind</link>
            <guid>https://app.cinevva.com/blog/2026-02-10-the-intuitive-mind</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Jensen Huang says raw math skills are now a commodity. Here's why that resonates with someone who failed the SAT, ignored the rules, and built something people actually wanted.]]></description>
            <content:encoded><![CDATA[<h1 id="the-intuitive-mind-in-an-age-of-ai" tabindex="-1">The intuitive mind in an age of AI <a class="header-anchor" href="#the-intuitive-mind-in-an-age-of-ai" aria-label="Permalink to &quot;The intuitive mind in an age of AI&quot;"></a></h1>
<p><em>By <a href="/about.html">Mariana Muntean</a>, CEO of Cinevva</em></p>
<img src="https://cdn.cinevva.com/blog/game-jam-houston-2018.png" alt="Mariana Muntean with classmates during a 48 hours game jam in Houston 2018" style="width:100%;border-radius:8px;margin:1.5rem 0" />
<small>With classmates during a 48-hour game jam in Houston, 2018</small>
<p>AI has essentially commoditized raw logic and computational ability. The skills we used to worship, the mental math, the pattern recognition, the ability to grind through complex algorithms, AI does all of that now. Faster. Better. Without getting tired. Do we still need to know it? Absolutely, but perceptions change. Now everyone needs to adjust to the new era - an era of intuitive intreaction and outcomes.</p>
<p>According to <a href="https://business.columbia.edu/insights/digital-future/nvidia-ceo-jensen-huang-reveals-keys-ai-and-leadership" target="_blank" rel="noreferrer">Jensen Huang</a>, NVIDIA's CEO, what matters now is the ability to sense a &quot;vibe&quot; and see around corners before the data appears. The intersection of technical literacy and deep empathy. The intuitive understanding that silicon can't touch.</p>
<p>I've never heard anyone with his credibility say something that validated my entire life trajectory quite so directly.</p>
<h2 id="the-sat-and-the-system-that-wasn-t-built-for-me" tabindex="-1">The SAT and the system that wasn't built for me <a class="header-anchor" href="#the-sat-and-the-system-that-wasn-t-built-for-me" aria-label="Permalink to &quot;The SAT and the system that wasn't built for me&quot;"></a></h2>
<p>A few years back I took the SAT and failed. I'm not proud of it, but I also didn't try again. Something felt fundamentally wrong about the whole thing, and I couldn't shake that feeling no matter how much people told me to just study harder and take it again.</p>
<p>Looking back, I think I was right.</p>
<p>The SAT is designed for the American education system. That sounds obvious, but the implications run deep. American high schools teach to specific patterns, question types, and ways of framing problems. Students grow up marinating in that style of standardized testing from elementary school onward. By the time they hit the SAT, they've internalized the rhythm.</p>
<p>International students don't have that advantage. We come from systems with different educational philosophies. European schools often emphasize depth over breadth, essay-based examination over multiple choice, oral defense over bubble sheets. Asian systems have their own standardized tests, but they measure different things in different ways. South American, African, Middle Eastern educational traditions each carry their own logic.</p>
<p>When you drop an international student into the SAT, you're not just testing their knowledge. You're testing how quickly they can adapt to a foreign testing culture while simultaneously demonstrating mastery of content. You're testing cultural fluency as much as academic ability.</p>
<p>I was expected to prep for this over a summer and ace it. Learn an entire testing culture, unlearn my own educational instincts, and perform at a level that would impress American admissions officers. All in a few months.</p>
<p>I chose not to.</p>
<p>When you're young, you're smart in different ways. Intuitive ways. I was clearing a path for myself, even if I couldn't articulate exactly why at the time. Something in me knew this system wasn't mine.</p>
<h2 id="following-the-creative-thread-instead" tabindex="-1">Following the creative thread instead <a class="header-anchor" href="#following-the-creative-thread-instead" aria-label="Permalink to &quot;Following the creative thread instead&quot;"></a></h2>
<p>I studied game development and design because I loved the idea of using creativity and visual effects to build virtual worlds people could play and interact with. I loved the intersection of art and technology, storytelling and interactivity.</p>
<p>What I found shocked me.</p>
<p>Games require serious technical depth. Physics simulations, collision detection, vector math, lighting calculations, optimization. I knew that going in. The math and engineering aren't obstacles to game development. They're part of what makes games work.</p>
<p>The problem was the gap between creative vision and implementation. The engines and tools dominating the industry were designed by engineers for engineers. Everything ran on &quot;ifs&quot; and &quot;thens&quot; and node-based blueprints. You wanted a vortex effect? Learn shader programming. Specific lighting mood? Dig into material graphs. Character movement that feels right? Debug character controller or physics parameters for hours.</p>
<p>If you're a visual person, if you think in vivid colors and moving images, if ideas come to you as fully formed scenes with sound and texture and emotional weight, you had to translate all of that into technical language before you could build any of it. You see a world in your mind, complete with lighting and atmosphere and the way characters move through space. Then you sit down at your computer and spend the next six hours debugging why your character falls through the floor.</p>
<p>The technical foundation matters. But the tools forced creators to live in implementation details instead of abstracting that complexity away. Game creation should feel like telling a story or directing a movie where people get to participate. That's the magic of the medium. Instead it felt like taking an engineering exam before you could even start.</p>
<p>Movie directors don't spend years learning physics engines before they can express their vision. They get a budget and a team that handles the technical execution. But in indie game development, you rarely have a budget. What you have is time and access to tools. And if you're a technical person, you can build a killer game. But only if.</p>
<p>This is how we got titles like Limbo, made by a small team with a singular artistic vision and the technical chops to execute it. Or Undertale, created largely by one person who happened to have the right combination of creative instinct and programming ability. Or Stardew Valley, where Eric Barone spent years teaching himself everything from pixel art to music composition to C# programming.</p>
<p>These games succeeded against enormous odds. But for every Limbo there are millions of creative visions that died because the tools demanded technical fluency their creators couldn't provide. Less than 3% of indie game developers ever achieve meaningful success. How many brilliant games never got made because their creators hit a wall of &quot;ifs&quot; and &quot;thens&quot; and gave up?</p>
<p>The barrier to entry wasn't creativity. It was technical gatekeeping built into the tools themselves.</p>
<h2 id="building-what-should-have-existed" tabindex="-1">Building what should have existed <a class="header-anchor" href="#building-what-should-have-existed" aria-label="Permalink to &quot;Building what should have existed&quot;"></a></h2>
<p>So 5 years ago I started building something different.</p>
<p>The pitch was simple: game development should be accessible to anyone with a creative vision. You shouldn't need a computer science degree to express yourself through interactive media. The tools should adapt to how creative people actually think, not the other way around.</p>
<p>VCs from Sequoia, Pear, Draper, and dozens of other firms told me it wouldn't work. It's B2C. The market isn't there. Indies don't pay for anything. You can't simplify game development without sacrificing capability. Gamers want complex games, and complex games require complex tools. Millions of excuses dressed up as market analysis.</p>
<p>People told me I was crazy. Maybe I was. But I kept coming back to the same question: why should multimillion dollar budgets be a prerequisite for creative expression? Movie directors and celebrity game producers have teams and resources. Everyone else gets a code editor and a prayer. I wanted to build the thing that closes that gap. You describe what you want, and it happens in front of your eyes. Every part of me knew this was right. I could feel it in every cell of my body.</p>
<p>Today thousands of people use Cinevva daily for 3D game assets, games, music, levels and experiences. Millions of views on projects created so far. Growing every single day. A two-minute pitch to Sand Hill Road isn't exactly the format for &quot;I failed the SAT but trust my intuition.&quot;</p>
<h2 id="what-intuition-actually-means" tabindex="-1">What intuition actually means <a class="header-anchor" href="#what-intuition-actually-means" aria-label="Permalink to &quot;What intuition actually means&quot;"></a></h2>
<p><a href="https://singjupost.com/transcript-jensen-huangs-interview-cisco-ai-summit-2026/" target="_blank" rel="noreferrer">Huang</a> wasn't just making a philosophical point. He was describing a real shift in what constitutes valuable intelligence.</p>
<p>For decades, we optimized for the wrong things. We built educational systems that rewarded memorization and calculation. We designed standardized tests that measured pattern-matching against previously seen problems. We hired people based on credentials that proved they could survive four years of academic gatekeeping. AI just made all of that less special.</p>
<p>What AI can't do, at least not yet, is sense what's missing. Feel when something is off. Intuit what people need before they can articulate it themselves. Read a room. Understand context that isn't captured in any dataset.</p>
<p>I poured my time, international life experience, money, and intuitive knowledge into building Cinevva. That's a mix hard to obtain in college. Hard to test for on the SAT. Hard to capture in any credential system designed before AI made raw cognitive horsepower abundant.</p>
<p>Sixteen or twenty years ago, computer scientists decided what tools should look like and how they should work. They built for themselves, for people who thought like them. The rest of us were expected to adapt. That era is ending. The people who will shape what comes next are the ones who understand what humans actually want. Who can feel when something is wrong and when something is right. Who build for people instead of for technical elegance.</p>
<p>I trusted something in myself that the system told me was worthless. And I was right.</p>
<hr>
<p><strong>Related:</strong></p>
<ul>
<li><a href="/blog/2026-01-18-skills-over-degrees.html">The job market is transforming — from credentials to skills</a></li>
<li><a href="/guides/game-dev-courses.html">Online Game Development Courses</a> — skills-first paths that bypass traditional gatekeeping</li>
<li><a href="/signals/2026-03-04-everyone-wants-ai-game-engine.html">Everyone wants to be the AI game engine now</a> — the industry shift toward tools that adapt to how people think</li>
</ul>
]]></content:encoded>
            <enclosure url="https://cdn.cinevva.com/blog/game-jam-houston-2018.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[AI controversy, trust, and the post‑AI economy for games]]></title>
            <link>https://app.cinevva.com/blog/2026-01-18-ai-controversy-and-post-ai-economy</link>
            <guid>https://app.cinevva.com/blog/2026-01-18-ai-controversy-and-post-ai-economy</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A practical view: AI isn't the point—trust is. Why labeling, filters, and fair distribution matter more than 'pro' vs 'anti' AI debates.]]></description>
            <content:encoded><![CDATA[<h1 id="ai-controversy-trust-and-the-post‐ai-economy-for-games" tabindex="-1">AI controversy, trust, and the post‑AI economy for games <a class="header-anchor" href="#ai-controversy-trust-and-the-post‐ai-economy-for-games" aria-label="Permalink to &quot;AI controversy, trust, and the post‑AI economy for games&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO of Cinevva</em></p>
<p>AI in games turned into a genuine minefield somewhere around mid-2024. Jobs disappearing. People convinced they're next. Copyright questions that not even the lawyers can untangle. Endless aesthetic debates that loop back on themselves. Steam drowning in stuff nobody asked for. And underneath everything, this nagging worry every creator has now: &quot;am I actually making something here, or just... typing prompts?&quot;</p>
<p>We run a platform. You learn one thing fast doing that.</p>
<p><strong>Nobody likes feeling tricked.</strong></p>
<h2 id="these-numbers-surprised-us" tabindex="-1">These numbers surprised us <a class="header-anchor" href="#these-numbers-surprised-us" aria-label="Permalink to &quot;These numbers surprised us&quot;"></a></h2>
<p>We figured AI adoption would grow. Not like this though. Here's where things landed by late 2025:</p>
<table tabindex="0">
<thead>
<tr>
<th>What we tracked</th>
<th>2024</th>
<th>2025</th>
<th>The shift</th>
</tr>
</thead>
<tbody>
<tr>
<td>Steam games disclosing AI</td>
<td>~1,000</td>
<td>7,818</td>
<td>7x jump</td>
</tr>
<tr>
<td>New Steam releases using AI</td>
<td>~3%</td>
<td>~20%</td>
<td>One in five</td>
</tr>
<tr>
<td>Devs who think AI hurts quality</td>
<td>34%</td>
<td>47%</td>
<td>Thirteen points higher</td>
</tr>
<tr>
<td>Devs who think AI helps quality</td>
<td>—</td>
<td>11%</td>
<td>Not many</td>
</tr>
<tr>
<td>Revenue from AI-disclosed games</td>
<td>—</td>
<td>$660M</td>
<td>Twelve games broke 8 figures</td>
</tr>
</tbody>
</table>
<p>Sources: Tom's Hardware / Totally Human Media analysis, GDC 2025 Developer Survey, Unity 2025 Gaming Report</p>
<h2 id="what-actually-happened-to-make-players-this-skeptical" tabindex="-1">What actually happened to make players this skeptical <a class="header-anchor" href="#what-actually-happened-to-make-players-this-skeptical" aria-label="Permalink to &quot;What actually happened to make players this skeptical&quot;"></a></h2>
<p>This didn't come from nowhere. Real things went wrong. And people remember.</p>
<h3 id="voice-actors-found-out-they-d-been-cloned-—-on-launch-day" tabindex="-1">Voice actors found out they'd been cloned — on launch day <a class="header-anchor" href="#voice-actors-found-out-they-d-been-cloned-—-on-launch-day" aria-label="Permalink to &quot;Voice actors found out they'd been cloned — on launch day&quot;"></a></h3>
<p><em>Tomb Raider 4-6 Remastered</em> shipped with AI-generated versions of the original voice performances. The actors learned about it the same moment everyone else did. Launch day. The publisher eventually patched the voices out after legal pressure built up. That eleven-month SAG-AFTRA strike? This exact scenario was driving it.</p>
<h3 id="ai-slop-became-a-thing-people-say-now" tabindex="-1">&quot;AI slop&quot; became a thing people say now <a class="header-anchor" href="#ai-slop-became-a-thing-people-say-now" aria-label="Permalink to &quot;&quot;AI slop&quot; became a thing people say now&quot;"></a></h3>
<p><em>Call of Duty: Black Ops 6</em> quietly added AI disclosure to its Steam page. After it was already out. Players had noticed the weirdness — visual glitches scattered everywhere. One loading screen character had six fingers. &quot;AI slop&quot; turned into shorthand for anything that looks... off. Hollow. Like nobody cared enough to check.</p>
<h3 id="an-award-nomination-vanished" tabindex="-1">An award nomination vanished <a class="header-anchor" href="#an-award-nomination-vanished" aria-label="Permalink to &quot;An award nomination vanished&quot;"></a></h3>
<p><em>Clair Obscur: Expedition 33</em> got its Game of the Year nomination pulled at the Indie Game Awards. The shipped game didn't use AI though. The problem was AI placeholders during development — internal stuff that never made it to players. Final assets were entirely human-made. Didn't matter. Weeks of arguing about where exactly the line should be.</p>
<h3 id="teams-that-never-touched-ai-had-to-prove-it" tabindex="-1">Teams that never touched AI had to prove it <a class="header-anchor" href="#teams-that-never-touched-ai-had-to-prove-it" aria-label="Permalink to &quot;Teams that never touched AI had to prove it&quot;"></a></h3>
<p><em>Chessplus</em> and <em>Peak</em> both got hit with AI accusations. Neither used any. Both were award nominees. Both development teams ended up digging through old screenshots and layer files just to demonstrate their work was handmade. The <em>Peak</em> team put it best: &quot;We might be slop, but we're human-made, locally-sourced artisanal slop.&quot;</p>
<h3 id="studios-went-in-completely-opposite-directions" tabindex="-1">Studios went in completely opposite directions <a class="header-anchor" href="#studios-went-in-completely-opposite-directions" aria-label="Permalink to &quot;Studios went in completely opposite directions&quot;"></a></h3>
<p><strong>SNK</strong> — Discord moderators walked out after AI-looking visuals showed up in a <em>Fatal Fury: City of the Wolves</em> trailer. <strong>Games Workshop</strong> — banned AI entirely across all Warhammer properties. <strong>Larian Studios</strong> — said they used AI only for very early concept sketches on Baldur's Gate 3, nothing in the final game. <strong>Tim Sweeney</strong> — declared Steam's AI labels worthless and said to scrap them. <strong>Valve</strong> — shot back that devs complaining about AI labels usually worry their work looks &quot;low effort.&quot;</p>
<h2 id="finding-games-turned-into-a-trust-problem" tabindex="-1">Finding games turned into a trust problem <a class="header-anchor" href="#finding-games-turned-into-a-trust-problem" aria-label="Permalink to &quot;Finding games turned into a trust problem&quot;"></a></h2>
<p>Stuff gets made faster than anyone can properly evaluate it now. The questions changed:</p>
<p>What am I even looking at here?
Who made this?
Am I going to regret the time I spend on it?
Does any of this match what the trailer promised?</p>
<p>There's probably no grand resolution coming. No definitive ruling on AI versus human-made.</p>
<p>What's more likely: <strong>filters, real transparency, and incentive structures that push toward honesty</strong>.</p>
<h2 id="small-studios-stuck-in-the-middle-of-all-this" tabindex="-1">Small studios stuck in the middle of all this <a class="header-anchor" href="#small-studios-stuck-in-the-middle-of-all-this" aria-label="Permalink to &quot;Small studios stuck in the middle of all this&quot;"></a></h2>
<p>Indie developers landed in a strange spot.</p>
<p><strong>The upside is genuinely hard to dismiss:</strong>
You iterate faster. Costs drop when four people are making everything. Solo devs can actually finish things now. Localization stops eating your entire budget.</p>
<p><strong>The downside is just as real:</strong>
Output that feels generic. Training data and IP concerns that lawyers won't touch yet. Shipping systems you don't fully understand — some people call it comprehension debt. Getting grouped in with the flood of low-effort releases. Nearly half of surveyed developers think AI makes games worse overall.</p>
<p>Unity's 2025 report says 79% of developers feel positive about AI tools. Sounds pretty definitive. The reality is messier. Teams doing this well tend to use AI for the boring parts — grunt work, rough passes, QA tedium. Creative direction? That stays human.</p>
<h2 id="where-cinevva-lands-neutral-on-ai-strict-on-honesty" tabindex="-1">Where Cinevva lands: neutral on AI, strict on honesty <a class="header-anchor" href="#where-cinevva-lands-neutral-on-ai-strict-on-honesty" aria-label="Permalink to &quot;Where Cinevva lands: neutral on AI, strict on honesty&quot;"></a></h2>
<p>We don't turn games away for using AI. We don't give them special treatment either.</p>
<p>The rule is simple: <strong>if AI was involved, say so.</strong> Players decide what they care about. Filters actually work then.</p>
<ul>
<li><a href="/ai-content.html">AI-generated content policy</a></li>
</ul>
<h2 id="filters-work-better-than-arguments" tabindex="-1">Filters work better than arguments <a class="header-anchor" href="#filters-work-better-than-arguments" aria-label="Permalink to &quot;Filters work better than arguments&quot;"></a></h2>
<p>The AI debate in games isn't reaching consensus anytime soon. Probably never will. But individual preferences? Those are clear enough.</p>
<p>Some players specifically want human-directed art. Writing that came from a person. Visible craft.</p>
<p>Others genuinely don't care. Fun is fun.</p>
<p>Filters let both groups find what they're looking for. Nobody has to win.</p>
<h2 id="payment-models-matter-more-than-anyone-s-opinion" tabindex="-1">Payment models matter more than anyone's opinion <a class="header-anchor" href="#payment-models-matter-more-than-anyone-s-opinion" aria-label="Permalink to &quot;Payment models matter more than anyone's opinion&quot;"></a></h2>
<p>When revenue ties to <strong>playtime</strong> instead of unit sales, you make money by:</p>
<p>Getting players hooked fast. Keeping their attention. Delivering what your marketing said you would.</p>
<p>Quality becomes the obvious path. How you made it matters less.</p>
<p>Overpromise in your trailer? Players leave immediately. Retention tanks. Show them exactly what they're getting? They stick around. Playtime builds. Revenue comes. The economics sort themselves out.</p>
<h2 id="what-2026-probably-brings" tabindex="-1">What 2026 probably brings <a class="header-anchor" href="#what-2026-probably-brings" aria-label="Permalink to &quot;What 2026 probably brings&quot;"></a></h2>
<p><strong>More rules</strong> — EU AI Act keeps expanding. US protections for voice and likeness growing, especially after SAG-AFTRA.</p>
<p><strong>Specialized tools</strong> — AI built specifically for small teams. Designed to keep humans steering.</p>
<p><strong>Smarter labels</strong> — &quot;Made with AI&quot; is too crude. Expect distinctions between AI-assisted workflows and AI-generated final assets.</p>
<p><strong>Audiences splitting</strong> — Some players will deliberately seek out traditionally-made games. Others won't give it a thought. Both groups are large enough to build for.</p>
<p><strong>Platform competition</strong> — How storefronts handle transparency and discovery becomes a real differentiator.</p>
<h2 id="what-actually-moves-things-forward" tabindex="-1">What actually moves things forward <a class="header-anchor" href="#what-actually-moves-things-forward" aria-label="Permalink to &quot;What actually moves things forward&quot;"></a></h2>
<p>Arguing won't settle the AI question. What does:</p>
<ol>
<li><strong>Players</strong> picking based on what genuinely matters to them</li>
<li><strong>Creators</strong> being honest about how they work</li>
<li><strong>Platforms</strong> building tools that help both groups find each other</li>
</ol>
<p>That's the post-AI economy. Not warring camps. Trust holding the whole thing together.</p>
<hr>
<p><strong>Related:</strong></p>
<ul>
<li><a href="/ai-content.html">AI-generated content policy</a></li>
<li><a href="/creators.html">For game creators</a></li>
<li><a href="/faq.html">FAQ</a></li>
<li><a href="/guides/frontier-gen-ai-models.html">Frontier Open-Source Gen AI Models</a> — the specific models and how they work</li>
<li><a href="/signals/2026-03-06-open-source-ai-pollution.html">Open source has an AI pollution problem</a> — what happens when AI output floods open-source projects</li>
</ul>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/blog/ai-controversy-trust.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[The job market is transforming — from credentials to skills]]></title>
            <link>https://app.cinevva.com/blog/2026-01-18-skills-over-degrees</link>
            <guid>https://app.cinevva.com/blog/2026-01-18-skills-over-degrees</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[90% of companies now prioritize skills over degrees. Here's what this means for fresh graduates entering tech and game development.]]></description>
            <content:encoded><![CDATA[<h1 id="the-job-market-is-transforming-—-from-credentials-to-skills" tabindex="-1">The job market is transforming — from credentials to skills <a class="header-anchor" href="#the-job-market-is-transforming-—-from-credentials-to-skills" aria-label="Permalink to &quot;The job market is transforming — from credentials to skills&quot;"></a></h1>
<p><em>By <a href="/about.html">Oleg Sidorkin</a>, CTO of Cinevva</em></p>
<p>I watched my cousin graduate with honors in 2023. Computer science degree from a good state school. Solid GPA. He spent fourteen months applying to jobs before landing one. Meanwhile, a friend who dropped out sophomore year to build indie games had three offers within a month of deciding to look.</p>
<p>That's not an anomaly anymore. That's the pattern.</p>
<h2 id="the-numbers-finally-caught-up-to-what-we-ve-been-feeling" tabindex="-1">The numbers finally caught up to what we've been feeling <a class="header-anchor" href="#the-numbers-finally-caught-up-to-what-we-ve-been-feeling" aria-label="Permalink to &quot;The numbers finally caught up to what we've been feeling&quot;"></a></h2>
<p>By 2025, <strong>90% of HR leaders</strong> reported hiring outside traditional four-year degrees (<a href="https://fortune.com/2025/07/01/90-percent-hr-leaders-looking-to-hire-outside-of-traditional-college-degrees-as-they-prioritize-skills/" target="_blank" rel="noreferrer">Fortune</a>). A quarter of U.S. companies dropped bachelor's requirements entirely (<a href="https://www.hrdive.com/news/employer-eliminate-degree-requirements-2025/748998/" target="_blank" rel="noreferrer">HR Dive</a>). And here's the part that stings if you just finished paying off loans: <strong>94% of employers say skills-based hires outperform those hired on credentials</strong> (<a href="https://www.forbes.com/sites/cynthiapong/2024/12/26/90-of-companies-make-better-hires-based-on-skills-over-degrees/" target="_blank" rel="noreferrer">Forbes</a>).</p>
<p>Verizon now says 99% of their roles don't require a degree. Sergey Brin admitted Google hires &quot;tons&quot; of people without bachelor's degrees. IBM made half their U.S. openings degree-optional through their &quot;New Collar&quot; program.</p>
<p>This isn't companies being charitable. They figured out that degrees weren't predicting who'd actually be good at the job.</p>
<h2 id="why-the-old-system-broke" tabindex="-1">Why the old system broke <a class="header-anchor" href="#why-the-old-system-broke" aria-label="Permalink to &quot;Why the old system broke&quot;"></a></h2>
<p>The honest answer? Degrees became a lazy filter.</p>
<p>When you're hiring and you've got 400 applications, requiring a bachelor's degree cuts the pile in half. It doesn't tell you who can actually do the work. It tells you who had the money, the time, and the family stability to sit in classrooms for four years. That correlation with ability was always weaker than we pretended.</p>
<p>And now three things happened at once:</p>
<p><strong>The skills moved too fast.</strong> Game engines, AI tools, web frameworks. By the time a curriculum committee approves a course on something, that thing is already outdated. A four-year degree teaches you theory from four years ago. In tech, that's ancient history.</p>
<p><strong>Portfolios became undeniable.</strong> Why guess whether someone can code when you can look at their GitHub? Why wonder if they can ship a game when their itch.io page has five finished projects with player reviews?</p>
<p><strong>Companies got desperate.</strong> The talent shortage is real. Excluding everyone without a degree means excluding people who might be exactly what you need. Some hiring managers figured this out the hard way, after watching self-taught developers run circles around their credentialed hires.</p>
<h2 id="the-game-industry-saw-this-coming" tabindex="-1">The game industry saw this coming <a class="header-anchor" href="#the-game-industry-saw-this-coming" aria-label="Permalink to &quot;The game industry saw this coming&quot;"></a></h2>
<p>I think games have been ahead of the curve here, and it's worth understanding why.</p>
<p>Studios never really cared where you went to school. They cared what you shipped. A polished 48-hour jam game tells a hiring manager more than a four-year game design degree ever could. It proves you can finish things under pressure. It proves you made hard decisions about scope. It proves the game is playable, not just theoretical.</p>
<p>If you're trying to break into games in 2026, here's what actually matters (<a href="https://www.dice.com/career-advice/aspiring-video-game-designers-in-2025-what-you-need-to-know" target="_blank" rel="noreferrer">Dice</a>, <a href="https://combinegr.com/2025-global-gaming-employment-outlook-trends-talent-strategy/" target="_blank" rel="noreferrer">CombineGR</a>): two or three polished, playable demos. Case studies explaining what you did and why. Evidence that you finish things. Fluency with at least one major engine, shown through real projects.</p>
<p>Hiring managers aren't looking for potential. They're looking for proof.</p>
<h2 id="the-uncomfortable-part-nobody-talks-about" tabindex="-1">The uncomfortable part nobody talks about <a class="header-anchor" href="#the-uncomfortable-part-nobody-talks-about" aria-label="Permalink to &quot;The uncomfortable part nobody talks about&quot;"></a></h2>
<p>Here's something that frustrated me when I dug into the research.</p>
<p>A Harvard/Burning Glass study found that companies dropping degree requirements often didn't actually hire more non-degreed candidates. The increase was only about 3.5 percentage points (<a href="https://www.forbes.com/sites/jenamcgregor/2024/02/14/companies-are-dropping-diploma-requirements-for-more-jobs-but-hiring-few-non-degreed-workers-to-fill-them/" target="_blank" rel="noreferrer">Forbes</a>). The policy changed. The hiring practices lagged.</p>
<p>That means if you're going the non-traditional route, you still have to work harder. The door is more open than it used to be, but you're not walking through on equal footing yet. Your portfolio needs to be undeniable. Your projects need to speak louder than someone else's credential.</p>
<p>It's not fair. But knowing it helps you prepare.</p>
<h2 id="what-i-d-tell-someone-starting-out-right-now" tabindex="-1">What I'd tell someone starting out right now <a class="header-anchor" href="#what-i-d-tell-someone-starting-out-right-now" aria-label="Permalink to &quot;What I'd tell someone starting out right now&quot;"></a></h2>
<p>If you're in school, don't stop. But understand that the degree alone isn't enough anymore. Build things on the side. Do game jams. Get a <a href="https://grow.google/certificates/" target="_blank" rel="noreferrer">Google Career Certificate</a> or Unity certification. Treat the degree as one credential among several, not the credential.</p>
<p>If you're not pursuing a degree, you have a different path but not necessarily a harder one. Build aggressively. Ship things. Document everything. Your <a href="https://github.com" target="_blank" rel="noreferrer">GitHub</a>, your <a href="https://itch.io" target="_blank" rel="noreferrer">itch.io</a> page, your personal site. That's your credential now.</p>
<p>Either way, practice explaining your work. Not just what you built, but why you made the choices you made. What you'd do differently. What you learned. Interviewers remember people who can articulate their thinking.</p>
<h2 id="this-is-bigger-than-hiring" tabindex="-1">This is bigger than hiring <a class="header-anchor" href="#this-is-bigger-than-hiring" aria-label="Permalink to &quot;This is bigger than hiring&quot;"></a></h2>
<p>What's happening here isn't just a shift in how companies fill roles. It's a shift in what we collectively value.</p>
<p>For decades, credentials served as a filter. Expensive, time-consuming, but legible. If someone had a degree, you could assume certain things. It was a signal, not a direct measure, but it was good enough.</p>
<p>That signal is breaking now. Technology makes it possible to see what someone can actually do. Remote work proved that outputs matter more than where you sat. AI is making theoretical knowledge less valuable than practical application.</p>
<p>I find this genuinely hopeful, even if the transition is messy. The question used to be &quot;where did you study?&quot; Now it's &quot;what can you build?&quot;</p>
<p>That's a better question.</p>
<hr>
<h2 id="sources" tabindex="-1">Sources <a class="header-anchor" href="#sources" aria-label="Permalink to &quot;Sources&quot;"></a></h2>
<ul>
<li><a href="https://fortune.com/2025/07/01/90-percent-hr-leaders-looking-to-hire-outside-of-traditional-college-degrees-as-they-prioritize-skills/" target="_blank" rel="noreferrer">Fortune: 90% of HR leaders hiring outside traditional degrees</a></li>
<li><a href="https://www.forbes.com/sites/cynthiapong/2024/12/26/90-of-companies-make-better-hires-based-on-skills-over-degrees/" target="_blank" rel="noreferrer">Forbes: 90% of companies make better hires based on skills</a></li>
<li><a href="https://www.forbes.com/sites/jenamcgregor/2024/02/14/companies-are-dropping-diploma-requirements-for-more-jobs-but-hiring-few-non-degreed-workers-to-fill-them/" target="_blank" rel="noreferrer">Forbes: Companies dropping degree requirements but hiring few non-degreed workers</a></li>
<li><a href="https://www.hrdive.com/news/employer-eliminate-degree-requirements-2025/748998/" target="_blank" rel="noreferrer">HR Dive: Employers eliminating degree requirements</a></li>
<li><a href="https://www.computerworld.com/article/1623286/no-degree-no-problem-tech-firms-move-away-from-college-requirement-for-new-hires.html" target="_blank" rel="noreferrer">Computerworld: Tech firms move away from college requirement</a></li>
<li><a href="https://www.dice.com/career-advice/aspiring-video-game-designers-in-2025-what-you-need-to-know" target="_blank" rel="noreferrer">Dice: Aspiring video game designers in 2025</a></li>
<li><a href="https://combinegr.com/2025-global-gaming-employment-outlook-trends-talent-strategy/" target="_blank" rel="noreferrer">CombineGR: 2025 Global Gaming Employment Outlook</a></li>
</ul>
<hr>
<p><strong>Related:</strong></p>
<ul>
<li><a href="/guides/game-jams-hackathons.html">How to succeed in game jams</a></li>
<li><a href="/guides/game-dev-courses.html">Online Game Development Courses</a> — skills-first learning paths for every budget</li>
<li><a href="/tutorials/agentic-code-tools.html">Agentic AI code tools</a> — the tools reshaping what skills matter</li>
<li><a href="/blog/2026-02-10-the-intuitive-mind.html">The intuitive mind</a> — why intuition matters more than credentials</li>
</ul>
]]></content:encoded>
            <enclosure url="https://app.cinevva.com/img/blog/skills-over-degrees.jpg" length="0" type="image/jpg"/>
        </item>
    </channel>
</rss>