<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[Acko.net]]></title>
  <link href="https://acko.net/atom.xml" rel="self"/>
  <link href="https://acko.net/"/>
  <updated>2026-03-05T12:10:39+01:00</updated>
  <id>https://acko.net</id>
  <author>
    <name><![CDATA[Steven Wittens]]></name>
    
  </author>

  
  <entry>
    <title type="html"><![CDATA[Teardown Frame Teardown]]></title>
    <link href="https://acko.net/blog/teardown-frame-teardown/"/>
    <updated>2023-01-24T00:00:00+01:00</updated>
    <id>https://acko.net/blog/teardown-frame-teardown</id>
    <content type="html"><![CDATA[<div class="g8 i2 first"><div class="pad">
  <h2 class="sub">Rendering analysis</h2>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>In this post I'll do a "one frame" breakdown of Tuxedo Labs' indie game <a href="http://teardowngame.com" target="_blank">Teardown</a>.</p>

<p>The game is unique for having a voxel-driven engine, which provides a fully destructible environment. It embraces this boon, by giving the player a multitude of tools that gleefully alter and obliterate the setting, to create shortcuts between spaces. This enables a kind of gameplay rarely seen: where the environment is not just a passive backdrop, but a fully interactive part of the experience.</p>

<p>This is highly notable. In today's landscape of Unity/Unreal-powered gaming titles, it illustrates a very old maxim: that novel gameplay is primarily the result of having a dedicated game engine to enable that play. In doing so, it manages to evoke a feeling that is both incredibly retro and yet unquestionably futuristic. But it's more than that: it shows that the path graphics development has been walking, in search of ever more realistic graphics, can be bent and subverted entirely. It creates something wholly unique and delightful, without seeking true photorealism.</p>

</div></div>

<div class="g12 mt1"><div class="pad">
  <img src="https://acko.net/files/teardown-teardown/lee-chemicals.jpg" alt="Lee Chemicals Level" />
</div></div>

<div class="g8 i2 mt1"><div class="pad">

<p>It utilizes raytracing, to present global illumination, with real-time reflections, and physically convincing smoke and fire. It not only has ordinary vehicles, like cars and vans, but also industrial machinery like bulldozers and cranes, as well as an assortment of weapons and explosives, to bring the entire experience together. Nevertheless, it does not require the latest GPU hardware: it is an "ordinary" OpenGL application. So how does it do it?</p>

<p>The classic way to analyze this would be to just fire up RenderDoc and present an analytical breakdown of every buffer rendered along the way. But that would be doing the game a disservice. Not only is it much more fun to try and figure it out on your own, the game actually gives you all the tools you need to do so. It would be negligent not to embrace it. RenderDoc is only part 2.</p>

<p>Teardown is, in my view, a love letter to decades of real-time games and graphics. It features a few winks and nods to those in the know, but on the whole its innovations have gone sadly unremarked. I'm disappointed we haven't seen an explosion of voxel-based games since. Maybe this will change that.</p>

<p>I will also indulge in some backseat graphics coding. This is not to say that any of this stuff is easy. Rather, I've been writing my own .vox renderer in Use.GPU, which draws heavily from Teardown's example.</p>


<h2 class="mt3">Hunting for Clues</h2>

<h3>The Voxels</h3>

<p>Let's start with the most obvious thing: the voxels. At a casual glance, every Teardown level is made out of a 3D grid. The various buildings and objects you encounter are made out of tiny cubes, all the same size, like this spiral glass staircase in the Villa Gordon:</p>

</div></div>

<div class="c"></div>

<div class="c mt1"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/bCK8zk45Qhw" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>However, closer inspection shows something curious. Behind the mansion is a ramp—placed there for obvious reasons—which does not conform to the strict voxel grid at all: it has diagonal surfaces. More detailed investigation of the levels will reveal various places where this is done.</p>

<p>The various dynamic objects, be they crates, vehicles or just debris, also don't conform to the voxel grid: they can be moved around freely. Therefore this engine is not strictly voxel-grid-based: rather, it utilizes cube-based voxels inside a freeform 3D environment.</p>

</div></div>

<div class="c"></div>

<div class="c mt2"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/QoAJ8voadAk" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>There is another highly salient clue here, in the form of the game's map screen. When you press M, the game zooms out to an overhead view. Not only is it able to display all these voxels from a first person view, it is able to show an entire level's worth of voxels, and transition smoothly to-and-fro, without any noticeable pop-in. Even on a vertical, labyrinthine 3D level like Quilez Security.</p>

<p>This implies that however this is implemented, the renderer largely does not care how many voxels are on screen in total. It somehow utilizes a rendering technique that is independent of the overall complexity of the environment, and simply focuses on what is needed to show whatever is currently in view.</p>

<h3 class="mt2">The Lighting</h3>

<p>The next big thing to notice is the lighting in this game, which appears to be fully real-time.</p>

</div></div>

<div class="c"></div>

<div class="c mt1"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/VSvzxxF3Pyw" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>Despite the chunky environment, shadows are cast convincingly. This casually includes features that are still a challenge in real-time graphics, such as lights which cast from a line, area or volume rather than a single point. But just how granular is&nbsp;it?</p>

<p>There are places where, to a knowing eye, this engine performs dark magic. Like the lighting around this elevator:</p>

</div></div>

<div class="c"></div>

<div class="c mt1"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/Sof-px1mGK4" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>Not only is it rendering real-time shadows, it is doing so for area-lights in the floor and ceiling. This means a simple 2D shadow-map, rendering depth from a single vantage point, is insufficient. It is also unimaginable that it would do so for every single light-emitting voxel, yet at first sight, it does.</p>

</div></div>

<div class="c"></div>

<div class="c mt2"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/X8oxbxtQGLw" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>This keeps working even if you pick up a portable light and wave it around in front of you. Even if the environment has been radically altered, the renderer casts shadows convincingly, with no noticeable lag. The only tell is the all-pervasive grain: clearly, it is using noise-techniques to deal with gradients and sampling.</p>


<h3 class="mt2">The Reflections</h3>

<p>It's more than just lights. The spiral staircase from before is in fact reflected clearly in the surrounding glass. This is consistent regardless of whether the staircase is itself visible:</p>

</div></div>

<div class="c"></div>

<div class="c mt1"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/BCKvU2HiByM" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>This is where the first limitations start to pop up. If you examine the sliding doors in the same area, you will notice something curious: while the doors slide smoothly, their reflections do not:</p>

</div></div>

<div class="c"></div>

<div class="c mt1"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/W1cZaTZeN_g" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">
  
<p>There are two interesting artifacts in this area:</p>

</div></div>

<div class="g4 i2"><div class="pad">

<img src="https://acko.net/files/teardown-teardown/reflect-steps.jpg" alt="Reflection jaggies" />

</div></div>

<div class="g4"><div class="pad">

<img src="https://acko.net/files/teardown-teardown/shadow-tear.jpg" alt="Reflection tearing" />

</div></div>

<div class="g8 i2 mt1"><div class="pad">

<p>The first is that glossy reflections of straight lines have a jagged appearance. The second is that you can sometimes catch moving reflections splitting before catching up, as if part of the reflection is not updated in sync with the rest.</p>

<p class="mt2">The game also has actual mirrors:</p>

</div></div>

<div class="c"></div>

<div class="c mt1"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/ym-lMRnOrsg" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>Here we can begin to dissect the tricks. Most obvious is that some of the reflections are screen-space: mirrors will only reflect objects in full-color if they are already on screen. If you turn away, the reflection becomes dark and murky. But this is not an iron rule: if you blast a hole in a wall, it will still be correctly reflected, no matter the angle. It is only the light cast onto the floor through that hole which fails to be reflected under all circumstances.</p>

<p class="mt2">
  <img src="https://acko.net/files/teardown-teardown/rounded-edges.jpg" alt="Rounded voxel edges" style="max-width: 500px; margin: 0 auto;" />
</p>

<p>This clip illustrates another subtle feature: up close, the voxels aren't just hard-edged cubes. Rather, they appear somewhat like plastic lego bricks, with rounded edges. These edges reflect the surrounding light smoothly, which should dispel the notion that what we are seeing is mere simple vector geometry.</p>

</div></div>

<div class="c"></div>

<div class="c mt2"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/3KCb5SI2UN8" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>There is a large glass surface nearby which we can use to reveal more. If we hold an object above a mirror, the reflection does not move smoothly. Rather, it is visibly discretized into cubes, only moving on a rough global grid, regardless of its own angle.</p>

<p>This explains the sliding doors. In order to reflect objects, the renderer utilizes some kind of coarse voxel map, which can only accommodate a finite resolution.</p>

<p class="mt2">There is only one objectionable artifact which we can readily observe: whenever looking through a transparent surface like a window, and moving sideways, the otherwise smooth image suddenly becomes a jittery mess. Ghost trails appear behind the direction of motion:</p>

</div></div>

<div class="c"></div>

<div class="c mt1"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/XmlpxT1x8PY" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>This suggests that however the renderer is dealing with transparency, it is a poor fit for the rest of its bag of tricks. There is in fact a very concise explanation for this, which we'll get to.</p>

<p>Still, this is all broadly black magic. According to the commonly publicized techniques, this should simply not be possible, not on hardware incapable of accelerated raytracing.</p>


<h2 class="mt3">The Solids</h2>

<p>Time for the meat-and-potatoes: a careful breakdown of a single frame. It is difficult to find one golden frame that includes every single thing the renderer does. Nevertheless, the following is mostly representative:</p>


</div></div>

<div class="g10 i1 mt1"><div class="pad">
  <a href="https://acko.net/files/teardown-teardown/00-final.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/00-final.jpg" alt="Marina Level" /></a>
</div></div>

<div class="g8 i2"><div class="pad">

<p><i>Captures were done at 1080p, with uncompressed PNGs linked. Alpha channels are separated where relevant. The inline images have been adjusted for optimal viewing, while the linked PNGs are left pristine unless absolutely necessary.</i></p>


<h3 class="mt2">G-buffer</h3>

<p>If we fire up RenderDoc, a few things will become immediately apparent. Teardown uses a typical deferred G-buffer, with an unusual 5 render targets, plus the usual Z-buffer, laid out as follows:</p>

<p><img src="https://acko.net/files/teardown-teardown/gbuffer.png" alt="gbuffer layout" /></p>

</div></div>

<div class="g6 mt1"><div class="pad">
  <a href="https://acko.net/files/teardown-teardown/01-color-pass-albedo.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/01-color-pass-albedo.jpg" alt="Albedo" /></a>
  <p class="tc"><i>Albedo (RT0)</i></p>
</div></div>

<div class="g6 mt1"><div class="pad">
  <a href="https://acko.net/files/teardown-teardown/02-color-pass-normal.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/02-color-pass-normal.jpg" alt="Normal" /></a>
  <p class="tc"><i>Normal (RT1)</i></p>
</div></div>

<div class="g6 mt1"><div class="pad">
  <a href="https://acko.net/files/teardown-teardown/03-color-pass-mat-rgb.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/03-color-pass-mat-rgb.jpg" alt="Material (RGB)" /></a>
  <p class="tc"><i>Material (RT2 RGB)</i></p>
</div></div>

<div class="g6 mt1"><div class="pad">
  <a href="https://acko.net/files/teardown-teardown/03-color-pass-mat-alpha.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/03-color-pass-mat-alpha.jpg" alt="Material (RGB)" /></a>
  <p class="tc"><i>Emissive (RT2 Alpha)</i></p>
</div></div>

<div class="g6 mt1"><div class="pad">
  <a href="https://acko.net/files/teardown-teardown/04-color-pass-vel2.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/04-color-pass-vel2.jpg" alt="Velocity + Water" /></a>
  <p class="tc"><i>Velocity + Water (RT3)</i></p>
</div></div>

<div class="g6 mt1"><div class="pad">
  <a href="https://acko.net/files/teardown-teardown/05-color-pass-linear-depth.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/05-color-pass-linear-depth.jpg" alt="Linear Depth" /></a>
  <p class="tc"><i>Linear Depth (RT4)</i></p>
</div></div>

<div class="c"></div>
<div class="c mt2"></div>

<div class="g4"><div class="pad mt2">
  <img src="https://acko.net/files/teardown-teardown/renderdoc-calls.png" alt="Renderdoc Calls" />
</div></div>

<div class="g8"><div class="pad">

<h3 class="mt0">Draw calls</h3>

<p>Every draw call renders exactly 36 vertices, i.e. 12 triangles, making up a box. But these are not voxels: each object in Teardown is rendered by drawing the shape's bounding box. All the individual cubes you see don't really exist as geometry. Rather, each object is stored as a 3D volume texture, with one byte per voxel.</p>

<p>Thus, the primary rendering stream consists of one draw call per object, each with a unique 3D texture bound. Each indexes into a 256-entry palette consisting of both color and material properties. The green car looks like this:</p>

<p>
  <img src="https://acko.net/files/teardown-teardown/car-volume.png" alt="Car Volume" />
</p>

<p>This only covers the chassis, as the wheels can move independently, handled as 4 separate objects.</p>

</div></div>

<div class="g8 i2"><div class="pad">

<p>The color and material palettes for all the objects are packed into one large texture&nbsp;each:</p>

</div></div>

<div class="g4 i2"><div class="pad">

<img src="https://acko.net/files/teardown-teardown/00-palette-rgb.png" alt="Palette" />

</div></div>

<div class="g4"><div class="pad">

<img src="https://acko.net/files/teardown-teardown/00-material-rgb.png" alt="Material" />

</div></div>

<div class="g8 i2 mt1"><div class="pad">

<p>Having reflectivity separate from metallicness might seem odd, as they tend to be highly correlated. Some materials are reflective without being metallic, such as water and wet surfaces. Some materials are metallic without being fully reflective, perhaps to simulate dirt.</p>

<p>You may notice a lot of yellow in the palette: this is because of the game's yellow spray can, detailed in <a target="_blank" href="https://blog.voxagon.se/2020/12/03/spraycan.html">this blog post</a>. It requires a blend of each color towards yellow, as it is applied smoothly. This is in fact the main benefit of this approach: as each object is just a 3D "sprite", it is easy and quick to remove individual voxels, or re-paint them for e.g. vehicle skid marks or bomb scorching.</p>

<p>When objects are blasted apart, the engine will separate them into disconnected chunks, and make a new individual object for each. This can be repeated indefinitely.</p>

<p>Rendering proceeds front-to-back, as follows:</p>

</div></div>

<div class="g4 mt1"><a href="https://acko.net/files/teardown-teardown/07-color-pass-checkpoint1.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/07-color-pass-checkpoint1.jpg" alt="Color pass checkpoint 1/8" /></a></div>

<div class="g4 mt1"><a href="https://acko.net/files/teardown-teardown/07-color-pass-checkpoint5.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/07-color-pass-checkpoint5.jpg" alt="Color pass checkpoint 5/8" /></a></div>

<div class="g4 mt1"><a href="https://acko.net/files/teardown-teardown/07-color-pass-checkpoint8.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/07-color-pass-checkpoint8.jpg" alt="Color pass checkpoint 8/8" /></a></div>

<div class="g8 i2 mt1"><div class="pad">

<p><img src="https://acko.net/files/teardown-teardown/raytrace.png" alt="raytrace diagram" /></p>

<p>The shader for this is tightly optimized and quite simple. It will raytrace through each volume, starting at the boundary, until it hits a solid voxel. It will repeatedly take a step in the X, Y or Z direction, whichever is less.</p>

<p>To speed up this process, the renderer uses 2 additional MIP maps, at half and quarter size, which allow it to skip over 2×2×2 or 4×4×4 empty voxels at a time. It will jump up and down MIP levels as it encounters solid or empty areas. Because MIP map sizes are divided by 2 and then rounded down, all object dimensions must be a multiple of 4, to avoid misalignment. This means many objects have a border of empty voxels around them.</p>

<p>Curiously, Teardown centers each object inside its expanded volume, which means the extra border tends to be 1 or 2 voxels on each side, rather than 2 or 3 on one. This means its voxel-skipping mechanism cannot work as effectively. Potentially this issue could be avoided entirely by not using native MIP maps at all, and instead just using 3 separately sized 3D textures, with dimensions that are rounded up instead of&nbsp;down.</p>

<p class="mt2"><img src="https://acko.net/files/teardown-teardown/08-screen-door.png" alt="screendoor effect for transparency" /></p>

<p>As G-buffers can only handle solid geometry, the renderer applies a 50% screen-door effect to transparent surfaces. This explains the ghosting artifacts earlier, as it confuses the anti-aliasing logic that follows. To render transparency other than 50%, e.g. to ghost objects in third-person view, it uses a blue-noise texture with&nbsp;thresholding.</p>

<p>This might seem strange, as the typical way to render transparency in a deferred renderer is to apply it separately, at the very end. Teardown cannot easily do this however, as transparent voxels are mixed freely among solid ones.</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

<p class="mt2 mb0"><a href="https://acko.net/files/teardown-teardown/05-color-pass-linear-depth.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/05-color-pass-linear-depth.jpg" alt="Linear Depth" /></a></p>

</div></div>

<div class="g8 i2"><div class="pad">

<p>Another thing worth noting here: because each raytraced pixel sits somewhere inside its bounding box volume, the final Z-depth of each pixel cannot be known ahead of time. The pixel shader must calculate it as part of the raytracing, writing it out using the <code>gl_FragDepth</code> API. As the GPU does not assume that this depth is actually deeper than the initial depth, the native Z-buffer cannot do any early Z rejection. This would mean that even 100% obscured objects would have to be raytraced fully, only to be entirely discarded.</p>

<p>To avoid this, Teardown has its own early-Z mechanism, which uses the additional depth target in the RT4 slot. Before it starts raytracing a pixel, it checks to see if the front of the volume is already obscured. However, GPUs forbid reading and writing from the same render target, to avoid race conditions. So Teardown must periodically pause and copy the current RT4 state to another buffer. For the scene above, there are 8 such "checkpoints". This means that objects part of the same batch will always be raytraced in full, even if one of them is in front of the other.</p>

<p>Certain modern GPU APIs have extensions to signal that <code>gl_FragDepth</code> will always be deeper than the initial Z. If Teardown could make use of this, it could avoid this extra work. In fact, we can wonder why GPU makers didn't do this from the start, because pushing pixels closer to the screen, out of a bounding surface, doesn't really make sense: they would disappear at glancing angles.</p>

<p>Once all the voxel objects are drawn, there are two more draws. First the various cables, ropes and wires, drawn using a single call for the entire level. This is the only "classic" geometry in the entire scene, e.g. the masts and tethers on the boats here:</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

<p>
  <a href="https://acko.net/files/teardown-teardown/07-color-pass-checkpoint9-cables.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/07-color-pass-checkpoint9-cables.jpg" alt="Particles" /></a>
</p>

</div></div>

<div class="g8 i2"><div class="pad">

<p>Second, the various smoke particles. These are simulated on the CPU, so there are no real clues as to how. They appear to billow quite realistically. This <a href="https://ubm-twvideo01.s3.amazonaws.com/o1/vault/GDC2014/Presentations/Gustafsson_Dennis_Sprinkle_Fluids.pdf" target="_blank">presentation</a> by the creator offers some possible clues as to what it might be doing.</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

<p>
  <a href="https://acko.net/files/teardown-teardown/09-color-pass-particles.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/09-color-pass-particles.jpg" alt="Particles" /></a>
</p>

</div></div>

<div class="g8 i2"><div class="pad">

<p>Here too, the renderer makes eager use of blue-noise based screen door transparency. It will also alternate smoke pixels between forward-facing and backward-facing in the normal buffer, to achieve a faux light-scattering effect.</p>

<p><img src="https://acko.net/files/teardown-teardown/09-screen-door-normal.png" alt="screen door effect for particle normals" /></p>

<p class="mt2">Finally, the drawing finishes by adding the map-wide water surface. While the water is generally murky, objects near the surface do refract correctly. For this, the albedo buffer is first copied to a new buffer (again to avoid race conditions), and then used as a source for the refraction shader. Water pixels are marked in the unused blue channel of the motion vector buffer.</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

<p>
  <a href="https://acko.net/files/teardown-teardown/10-color-pass-water.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/10-color-pass-water.jpg" alt="Particles" /></a>
</p>

</div></div>

<div class="g8 i2"><div class="pad">

<p>The game also has dynamic foam ripples on the water, when swimming or driving a boat. For this, the last N ripples are stored and evaluated in the same water shader, expanding and fading out over time:</p>

</div></div>

<div class="g4 i2"><div class="pad">

<img src="https://acko.net/files/teardown-teardown/water-albedo.png" alt="Water ripples albedo" />

</div></div>

<div class="g4"><div class="pad">

<img src="https://acko.net/files/teardown-teardown/water-normal.png" alt="Water ripples normal" />

</div></div>

<div class="c"></div>

<div class="g8 i2 mt2"><div class="pad">
  
<p>While all draw calls are finished, Teardown still has one trick up its sleeve here. To smooth off the sharp edges of the voxel cubes... it simply blurs the final normal buffer. This is applied only to voxels that are close to the camera, and is limited to nearby pixels that have almost the same depth. In the view above, the only close-by voxels are those of the player's first-person weapon, so those are the only ones getting smoothed.</p>

</div></div>

<div class="g4 i2"><div class="pad">

<img src="https://acko.net/files/teardown-teardown/11-normal-pass-pre.png" alt="Unblurred normal" />

</div></div>

<div class="g4"><div class="pad">

<img src="https://acko.net/files/teardown-teardown/11-normal-pass-post.png" alt="Blurred normal" />

</div></div>

<div class="c"></div>

<div class="g8 i2 mt2"><div class="pad">

<h3>Puddles and Volumes</h3>

<p>Next up is the game's rain puddle effect. This is applied using a screen-wide shader, which uses perlin-like noise to create splotches in the material buffer. This applies on any upward facing surface, using the normal buffer, altering the roughness channel (zero roughness is stored as 1.0).</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

<p>
  <a href="https://acko.net/files/teardown-teardown/12-puddles.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/12-puddles.jpg" alt="Particles" /></a>
</p>

</div></div>

<div class="g8 i2"><div class="pad">

<p>This wouldn't be remarkable except for one detail: how the renderer avoids drawing puddles indoors and under awnings. This is where the big secret appears for the first time. Remember that coarse voxel map whose existence we inferred earlier?</p>

<p>Yeah it turns out, Teardown will actually maintain a volumetric shadow map of the <i>entire play area</i> at all times. For the Marina level, it's stored in a 1752×100×1500 3D texture, a 262MB chonker. Here's a scrub through part of it:</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

<p>
  <img src="https://acko.net/files/teardown-teardown/voxel-shadowmap.gif" alt="voxel shadowmap" />
</p>

</div></div>

<div class="g8 i2"><div class="pad">

<p>But wait, there's more. Unlike the regular voxel objects, this map is actually 1-bit. Each of its 8-bit texels stores 2×2×2 voxels. So it's actually a 3504×200×3000 voxel volume. Like the other 3D textures, this has 2 additional MIP levels to accelerate raytracing, but it has that additional "-1" MIP level inside the bits, which requires a custom loop to trace through it.</p>

<p>This map is updated using many small texture uploads in the middle of the render. So it's actually CPU-rendered. Presumably this happens on a dedicated thread, which might explain the desynchronization we saw before. The visible explosion in the frame created many small moving fragments, so there are ~50 individual updates here, multiplied by 3 for the 3 MIP levels.</p>

<p>Because the puddle effect is all procedural, they disappear locally when you hold something over them, and appear on the object instead, which is kinda hilarious:</p>

</div></div>

<div class="c"></div>

<div class="c mt1"></div>

<div class="g10 i1"><div class="pad">
  <div style="position: relative; width: 100%; padding-bottom: 56%;">
  <iframe style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/OgR6mPcCPbc" frameborder="0" allowfullscreen="allowfullscreen"></iframe>
  </div>
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>To know where to start tracing in world space, each pixel's position is reconstructed from the linear depth buffer. This is a pattern that reoccurs in everything that follows. A 16-bit depth buffer isn't very accurate, but it's good enough, and it doesn't use much bandwidth.</p>

<p>Unlike the object voxel tracing, the volumetric shadow map is always traced approximately. Rather than doing precise X/Y/Z steps, it will just skip ahead a certain distance until it finds itself inside a solid voxel. This works okay, but can miss voxels entirely. This is the reason why many reflections have a jagged appearance.</p>

</div></div>

<div class="c"></div>

<div class="g6"><div class="pad">

<p>
  <img src="https://acko.net/files/teardown-teardown/raytrace-2.png" alt="Sparse tracing" />
</p>

</div></div>

<div class="g6"><div class="pad">

<p>
  <img src="https://acko.net/files/teardown-teardown/raytrace-3.png" alt="Super sparse tracing" />
</p>

</div></div>

<div class="c"></div>

<div class="g8 i2"><div class="pad">

<p>There are in fact two tracing modes coded: sparse and "super sparse". The latter will only do a few steps in each MIP level, starting at -1, before moving to the next coarser one. This effectively does a very rough version of voxel cone tracing, and is the mode used for puddle visibility.</p>


<h2 class="mt3">The Lighting</h2>

<p>On to the next part: how the renderer actually pulls off its eerily good lighting.</p>

<p>Contrary to first impressions, it is not the voxels themselves that are casting the light: emissive voxels must be accompanied by a manually placed light to illuminate their surroundings. When destroyed, this light is then removed, and the emissive voxels are turned off as a group.</p>

<p>As is typical in a deferred renderer, each source of light is drawn individually into a light buffer, affecting only the pixels within the light's volume. For this, the renderer has various meshes which match each light type's shape. These are procedurally generated, so that e.g. each spotlight's mesh has the right cone angle, and each line light is enclosed by a capsule with the right length and radius:</p>

</div></div>

<div class="c"></div>

<div class="g4 i2 mt1"><div class="pad">
  <img src="https://acko.net/files/teardown-teardown/light-hemi.png" alt="hemisphere light" />
</div></div>

<div class="g4 mt1"><div class="pad">
  <img src="https://acko.net/files/teardown-teardown/light-sphere.png" alt="sphere light" />
</div></div>

<div class="g4 i2 mt1"><div class="pad">
  <img src="https://acko.net/files/teardown-teardown/light-capsule.png" alt="capsule light" />
</div></div>

<div class="g4 mt1"><div class="pad">
  <img src="https://acko.net/files/teardown-teardown/light-spot.png" alt="spot light" />
</div></div>

<div class="c"></div>

<div class="g8 i2 mt1"><div class="pad">

<p>The volumetric shadow map is once again the main star, helped by a generous amount of blue noise and stochastic sampling. This uses <a target="_blank" href="http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/">Martin Roberts' quasi-random sequences</a> to produce time-varying 1D, 2D and 3D noise from a static blue noise texture. The light itself is also split up, separated into diffuse, specular and volumetric irradiance components.</p>

<h3 class="mt2">Diffuse light</h3>

<p>It begins with ambient sky light:</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/14-light-pass-ao.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/14-light-pass-ao.jpg" alt="Ambient occlusion" /></a>

</div></div>

<div class="g8 i2"><div class="pad">

<p>This looks absolutely lovely, with large scale occlusion thanks to volumetric ray tracing in "super sparse" mode. This uses cosine-weighted sampling in a hemisphere around each point, with 2 samples per pixel. To render small scale occlusion, it will first do a single screen-space step one voxel-size out, using the linear depth buffer.</p>

<p>Notice that the tree tops at the very back do not have any large scale occlusion: they extend beyond the volume of the shadow map, which is vertically limited.</p>

<p class="mt2">Next up are the individual lights. These are not point lights, they have an area or volume. This includes support for "screen" lights, which display an image, used in other scenes. To handle this, each lit pixel picks a random point somewhere inside the light's extent. The shadows are handled with a raytrace between the surface and the chosen light point, with one ray per pixel.</p>

</div></div>

<div class="c"></div>

<div class="g6">

<p>
  <a href="https://acko.net/files/teardown-teardown/15-light-pass-checkpoint1.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/15-light-pass-checkpoint1.jpg" alt="Partial diffuse lighting" /></a>
</p>

</div>

<div class="g6">

<p>
  <a href="https://acko.net/files/teardown-teardown/13-light-pass-output.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/13-light-pass-output.jpg" alt="Completed diffuse lighting" /></a>
</p>

</div>

<div class="c"></div>

<div class="g8 i2"><div class="pad">

<p>As this is irradiance, it does not yet factor in the color of each surface. This allows for aggressive denoising, which is the next step. This uses a spiral-shaped blur filter around each point, weighted by distance. The weights are also attenuated by both depth and normal: the depth of each sample must lie within the tangent plane of the center, and its normal must match.</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

<p>
  <a href="https://acko.net/files/teardown-teardown/16-light-pass-temporal-blur-rgb.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/16-light-pass-temporal-blur-rgb.jpg" alt="Diffuse light after blurring" /></a>
</p>

</div></div>

<div class="g8 i2"><div class="pad">

<p>This blurred result is immediately blended with the result of the previous frame, which is shifted using the motion vectors rendered for each pixel.</p>

<p>Finally, the blurred diffuse <i>irradiance</i> is multiplied with the non-blurred albedo (i.e. color) of every surface, to produce outgoing diffuse <i>radiance</i>:</p>

</div></div>

<div class="c"></div>

<div class="g10 i1 mt1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/17-light-pass-compose.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/17-light-pass-compose.jpg" alt="Diffuse radiance after composing with albedo" /></a>

</div></div>

<div class="g8 i2"><div class="pad">


<h3 class="mt2">Specular light</h3>

<p>As the experiment with the mirror showed, the renderer doesn't really distinguish between glossy specular reflections and "real" mirror reflections. Both are handled as part of the same process, which uses the diffuse light buffer as an input. This is drawn using a single full-screen render.</p>

</div></div>

<div class="c"></div>

<div class="g10 i1 mt1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/20-reflect-pass.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/20-reflect-pass.jpg" alt="Specular irradiance" /></a>

</div></div>

<div class="g8 i2"><div class="pad">
  
<p>As we saw, there are both screen-space and global reflections. Unconventionally, the screen-space reflections are also traced using the volumetric shadow map, rather than the normal 2D Z-buffer. Glossyness is handled using... you guessed it... stochastic sampling based on blue noise. The rougher the surface, the more randomly the direction of the reflected ray is altered. Voxels with zero reflectivity are skipped entirely, creating obvious black spots.</p>

<p>If a voxel was hit, its position is converted to its 2D screen coordinates, and its color is used, but only if it sits at the right depth. This must also fade out to black at the screen edges. If no hit could be found within a certain distance, it instead uses a cube environment map, attenuated by fog, here a deep red.</p>

<p>The alpha channel is used to store the final reflectivity of each surface, factoring in fresnel effects and viewing angle:</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/21-reflect-pass-alpha.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/21-reflect-pass-alpha.jpg" alt="Specular reflectivity" /></a>

</div></div>

<div class="g8 i2 mt1"><div class="pad">

<p>This is then all denoised similar to the diffuse lighting, but without an explicit blur. It's blended only with the previous reprojected specular result, blending more slowly the glossier—and noisier—the surface is:</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/21-reflect-pass-blur.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/21-reflect-pass-blur.jpg" alt="Specular blurred irradiance" /></a>

</div></div>

<div class="g8 i2"><div class="pad">


<h3 class="mt2">Volumetric light</h3>

<p>Volumetric lights are the most expensive, hence this part is rendered on a buffer half the width and height. It uses the same light meshes as the diffuse lighting, only with a very different shader.</p>

<p><img src="https://acko.net/files/teardown-teardown/volume-trace.png" alt="raytrace diagram" style="max-width: 500px; margin: 0 auto;" /></p>

<p>For each pixel, the light position is again jittered stochastically. It will then raytrace through a volume around that position, to determine where the light contribution along the ray starts and ends. Finally it steps between those two points, accumulating in-scattered light along the way. As is common, it will also jitter the steps along the ray.</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/18-volumetric-pass-checkpoint2.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/18-volumetric-pass-checkpoint2.jpg" alt="Volumetric lights" /></a>

</div></div>

<div class="g8 i2"><div class="pad">

<p>This is expensive because at every step, it must trace a secondary ray towards the light, to determine volumetric shadowing. To cut down on the number of extra rays, this is only done if the potential light contribution is actually large enough to make a visible difference. To optimize the trace and keep the ray short, it will trace towards the closest point on the light, rather than the jittered point.</p>

<p>The resulting buffer is still the noisiest of them all, so once again, there is a blurring and temporal blending step. This uses the same spiral filter as the diffuse lighting, but lacks the extra weights of planar depth and normal. Instead, the depth buffer is only used to prevent the fog from spilling out in front of nearby occluders:</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/19-volumetric-pass-blur.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/19-volumetric-pass-blur.jpg" alt="Volumetric lights with blurring" /></a>

</div></div>

<div class="g8 i2"><div class="pad">


<h3 class="mt2">Compositing</h3>

<p>All the different light contributions are now added together, with depth fog and a skybox added to complete it. Interestingly, while it looks like a height-based fog which thins out by elevation, it is actually just based on vertical view <i>direction</i>. A clever trick, and a fair amount cheaper.</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/22-compose-volumetric-distance-fog.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/22-compose-volumetric-distance-fog.jpg" alt="Composed lights with fog" /></a>

</div></div>

<div class="g8 i2"><div class="pad">

<h2>The Post-Processing</h2>

<p>At this point we have a physically correct, lit, linear image. So now all that remains is to mess it up.</p>

<p>There are several effects in use:</p>

<ul class="indent">
  <li>Motion blur</li>
  <li>Depth of field</li>
  <li>Temporal anti-aliasing</li>
  <li>Bloom</li>
  <li>Lens distortion</li>
  <li>Lens dirt</li>
  <li>Vehicle outline</li>
</ul>

<p>Several of these are optional.</p>

<h3 class="mt2">Motion Blur</h3>

<p>If turned on, it is applied here using the saved motion vectors. This uses a variable number of steps per pixel, up to 10. Unfortunately it's extremely subtle and difficult to get a good capture of, so I don't have a picture.</p>

<h3 class="mt2">Depth of Field</h3>

<p>This effect requires a dedicated capture to properly show, as it is hardly noticeable on long-distance scenes. I will use this shot, where the DOF is extremely shallow because I'm holding the gate in the foreground:</p>

</div></div>

<div class="c"></div>

<div class="g6"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/dof-1.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/dof-1.jpg" alt="Depth of field - before" /></a>

</div></div>

<div class="g6"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/dof-6.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/dof-6.jpg" alt="Depth of field - after" /></a>

</div></div>

<div class="g8 i2"><div class="pad mt2">

<p>First, the renderer needs to know the average depth in the center of the image. To do so, it samples the linear depth buffer in the middle, again with a spiral blur filter. It's applied twice, one with a large radius and one small. This is done by rendering directly to a 1x1 size image, which is also blended over time with the previous result. This produces the average focal distance.</p>

<p>Next it will render a copy of the image, with the alpha channel (float) proportional to the amount of blur needed (the circle of confusion). This is essentially any depth past the focal point, though it will bias the center of the image to remain more in focus:</p>

</div></div>

<div class="g10 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/dof-alpha.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/dof-alpha.jpg" alt="Depth of field - alpha channel" /></a>

</div></div>

<div class="c"></div>

<div class="g8 i2"><div class="pad mt2">

<p>The renderer will now perform a 2x2 downscale, followed by a blue-noise jittered upscale. This is done even if DOF is turned off, which suggests the real purpose here is to even out the image and remove the effects of screen door transparency.</p>

</div></div>

<div class="c"></div>

<div class="g4">

  <a href="https://acko.net/files/teardown-teardown/dof-1.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/dof-mini-1.jpg" alt="DOF step 1" /></a>

</div>

<div class="g4">

  <a href="https://acko.net/files/teardown-teardown/dof-2.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/dof-mini-2.jpg" alt="DOF step 2" /></a>

</div>

<div class="g4">

  <a href="https://acko.net/files/teardown-teardown/dof-3.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/dof-mini-3.jpg" alt="DOF step 3" /></a>

</div>

<div class="c"></div>

<div class="g8 i2"><div class="pad mt2">

<p>Actual DOF will now follow, rendered again to a half-sized image, to cut down on the cost of the large blur radius. This again uses a spiral blur filter. This will use the alpha channel to mask out any foreground samples, to prevent them from bleeding onto the background. Such samples are instead replaced with the average color so far, a trick <a href="https://blog.voxagon.se/2018/05/04/bokeh-depth-of-field-in-single-pass.html" target="_blank">documented here</a>.</p>

</div></div>

<div class="c"></div>

<div class="g5 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/dof-4.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/dof-mini-4.jpg" alt="DOF step 4" /></a>

</div></div>

<div class="g5"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/dof-4.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/dof-mini-5.jpg" alt="DOF step 5" /></a>

</div></div>

<div class="c"></div>

<div class="g8 i2"><div class="pad mt2">

<p>Now it combines the sharp-but-jittered image with the blurry DOF image, using the alpha channel as the blending mask.</p>


<h3 class="mt2">Temporal anti-aliasing</h3>

<p>At this point the image is smoothed with a variant of temporal anti-aliasing (TXAA), to further eliminate any left-over jaggies and noise. This is now the fourth time that temporal reprojection and blending was applied in one frame: this is no surprise, given how much stochastic sampling was used to produce the image in the first&nbsp;place.</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/24-txaa-rgb.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/24-txaa-rgb.jpg" alt="TXAA" /></a>

</div></div>

<div class="g8 i2"><div class="pad">

<p>To help with anti-aliasing, as is usual, the view point itself is jittered by a tiny amount every frame, so that even if the camera doesn't move, it gets varied samples to average out.</p>


<h3 class="mt2">Exposure and bloom</h3>

<p>For proper display, the renderer will determine the appropriate exposure level to use. For this, it needs to know the average light value in the image.</p>

<p>First it will render a 256x256 grayscale image. It then progressively downsamples this by 2, until it reaches 1x1. This is then blended over time with the previous result to smooth out the changes.</p>

<p><img src="https://acko.net/files/teardown-teardown/23-exposure-gray.png" alt="exposure luminance" style="max-width: 300px; margin: 0 auto;" /></p>

<p>Using the exposure value, it then produces a bloom image: this is a heavily thresholded copy of the original, where all but the brightest areas are black. This image is half the size of the original.</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/25-bloom1.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/25-bloom1.jpg" alt="Bloom source" /></a>

</div></div>

<div class="g8 i2"><div class="pad">

<p>This half-size bloom image is then further downscaled and blurred more aggressively, by 50% each time, down to ~8px. At each step it does a separate horizontal and vertical blur, achieving a 2D gaussian filter:</p>

</div></div>

<div class="c"></div>

<div class="g6"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/25-bloom2.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/25-bloom2.jpg" alt="Bloom horizontal blur" /></a>

</div></div>

<div class="g6"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/25-bloom3.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/25-bloom3.jpg" alt="Bloom vertical blur" /></a>

</div></div>

<div class="g8 i2"><div class="pad mt2">

<p>The resulting stack of images is then composed together to produce a soft glow with a very large effective radius, here exaggerated for effect:</p>

<p><a href="https://acko.net/files/teardown-teardown/25-bloom-stack.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/25-bloom-stack.jpg" alt="Bloom stack" /></a></p>


<h3 class="mt2">Final composition</h3>

<p>Almost done: the DOF'd image is combined with bloom, multiplied with the desired exposure, and then gamma corrected. If lens distortion is enabled, it is applied too. It's pretty subtle, and here it is just turned off. Lens dirt is missing too: it is only used if the sun is visible, and then it's just a static overlay.</p>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/26-image-composed.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/26-image-composed.jpg" alt="Composed image + bloom" /></a>

</div></div>

<div class="g8 i2"><div class="pad mt2">

<p>All that remains is to draw the UI on top. For this it uses a signed-distance-field font atlas, and draws the crosshairs icon in the middle:</p>

<p><img src="https://acko.net/files/teardown-teardown/27-image-sdf-text.png" alt="sdf text atlas" style="max-width: 300px; margin: 0 auto;" /></p>

<p><img src="https://acko.net/files/teardown-teardown/27-image-ui.png" alt="sdf text atlas" style="max-width: 400px; margin: 0 auto;" /></p>


<h2 class="mt3">Bonus Shots</h2>

<p>To conclude, a few bonus images.</p>

<h3 class="mt2">Ghosting</h3>

<p>While in third person vehicle view, the renderer will ghost any objects in front of it. As a testament to the power of temporal smoothing, compare the noisy "before" image with the final "after" result:</p>

</div></div>

<div class="c"></div>

<div class="g6"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/bonus-ghost.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/bonus-ghost.jpg" alt="Ghosting before noise filter" /></a>

</div></div>

<div class="g6"><div class="pad">

  <a href="https://acko.net/files/teardown-teardown/bonus-ghost-final.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/bonus-ghost-final.jpg" alt="Ghosting after noise filter" /></a>

</div></div>

<div class="g8 i2"><div class="pad">
  
<p>To render the white outline, the vehicle is rendered to an offscreen buffer in solid white, and then a basic edge detection filter is applied.</p>


<h3 class="mt2">Mall Overdraw</h3>

<p>The Evertides Mall map is one of the larger levels in the game, featuring a ton of verticality, walls, and hence overdraw. It is here that the custom early-Z mechanism really pays off:</p>

</div></div>

<div class="c"></div>

<div class="g6"><div class="pad mb1">

  <a href="https://acko.net/files/teardown-teardown/28-bonus-mall1.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/28-bonus-mall1.jpg" alt="Evertides mall overdraw" /></a>

</div></div>

<div class="g6"><div class="pad mb1">

  <a href="https://acko.net/files/teardown-teardown/28-bonus-mall3.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/28-bonus-mall3.jpg" alt="Evertides mall overdraw" /></a>

</div></div>

<div class="g6"><div class="pad mb1">

  <a href="https://acko.net/files/teardown-teardown/28-bonus-mall5.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/28-bonus-mall5.jpg" alt="Evertides mall overdraw" /></a>

</div></div>

<div class="g6"><div class="pad mb1">

  <a href="https://acko.net/files/teardown-teardown/28-bonus-mall7.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/28-bonus-mall7.jpg" alt="Evertides mall overdraw" /></a>

</div></div>

<div class="c"></div>

<div class="g10 i1"><div class="pad mb1">

  <a href="https://acko.net/files/teardown-teardown/28-bonus-mall14.png" target="_blank"><img src="https://acko.net/files/teardown-teardown/28-bonus-mall14.jpg" alt="Evertides mall overdraw" /></a>

</div></div>

<div class="g8 i2"><div class="pad">

<p class="tc">That concludes this deep dive. Hope you enjoyed it as much as I did making it.</p>

<p class="tc mt2">
  <i><b>More reading/viewing:</b></i>
</p>

<ul class="indent">
  <li>Another <a href="https://juandiegomontoya.github.io/teardown_breakdown.html" target="_blank">Teardown teardown</a>.</li>
  <li><a href="https://www.youtube.com/watch?v=0VzE8ROwC58" target="_blank">Video stream</a> with an in-engine walkthrough.</li>
</ul>

<p class="mt3 mb3"><img src="https://acko.net/files/teardown-teardown/meme.jpg" style="max-width: 500px; margin: 0 auto;" /></p>

</div></div>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[This is Your Brain on CSS]]></title>
    <link href="https://acko.net/blog/this-is-your-brain-on-css/"/>
    <updated>2012-02-19T00:00:00+01:00</updated>
    <id>https://acko.net/blog/this-is-your-brain-on-css</id>
    <content type="html"><![CDATA[<div style="display: none"><img src="/files/mri/cover.jpg" alt="" /></div>

<div class="g8 i2 first"><div class="pad">

<p>First things first: the CSS 3D renderer used to power <strike>this</strike> <em>the previous</em> site is now <a href="https://github.com/unconed/CSS3D.js">available on GitHub.com</a>. However, it's still limited to only solid lines and planes. It's also limited to WebKit browsers, as Firefox's CSS 3D support just isn't quite there yet.</p>

<p>
  But CSS 3D is not a one trick pony, and as with many things, what you get out of it depends entirely on what you put in. So here's a disembodied head made out of CSS 3D. It consists of nothing more than a bunch of images stacked up against each other, and integrates perfectly with the existing 3D parallax on this site. Click and drag to rotate, or use the slider to look inside.
</p>

<link rel="stylesheet" href="/files/mri/head.css" type="text/css" media="screen" />

<div id="head-3d">
  <div class="head-viewport" style="height: 500px;">
    <div class="CSS3DCamera" data-var="transform">
      <div class="pedestal">
        
      </div>
      <div class="VolumetricView" data-var="phi slice"></div>
    </div>
  </div>
  <div class="Slider" data-var="slice"></div>
</div>

<p>
  Making the basic effect was actually quite easy. I took an MRI from the <a href="http://graphics.stanford.edu/data/voldata/">Stanford Volume Data Archive</a> and wrote a small script to turn it into a sheet of CSS sprites. There's <a href="http://acko.net/files/mri/MRbrain-color.jpg">one file for color</a>, <a href="http://acko.net/files/mri/MRbrain-alpha8.png">one for opacity</a>, totalling about 2.1 MB. Both files are composited into Canvases and placed in slices into the DOM, offset forward or backwards in 3D. Then there's just some minor logic to rotate the slices in 90 degree increments to follow the camera.
</p>

<p>
  But the slices are rendered as is, and the MRI consists of <a href="http://acko.net/files/mri/MRbrain-alpha8.png">boring grayscale data</a>. Luckily, I can precompute any amount of shaders and effects I want and just bake them into the slices. I geeked out by applying fake specular lighting, for that 'fresh meat' look, and volumetric obscurance to enhance the sense of depth on the inside. I changed the palette to gory colors based on local density, giving the impression of flesh and bone knitting itself together. Creepy, but cool.
</p>

<p>
  I wrapped it in a custom widget, using straight up CSS rather than Three.js this time. I've wanted to play with <a href="http://worrydream.com/Tangle/">Tangle.js</a>, so I used that to hook up the camera controls and slider. That's pretty much it. In an ideal world, the jarring transition when rotating would be covered up by a nice transition, but the browsers don't like it.
</p>

</div></div>

<script type="text/javascript">
// <!--
Acko.queue(function () {

    var model = {
      initialize: function () {
        // State
        this.theta = 0.0;
        this.phi = 0.5;
        this.slice = 0;
      },

      update: function () {
        this.transform = 'rotateX('+ -this.theta +'rad) rotateY('+ this.phi +'rad)';
      },
    };

    Tangle.classes.CSS3DCamera = {
      initialize: function (element, options, tangle, variables) {
        this.element = element;

        this.element.style.transformStyle = 'preserve-3d';

        var that = this;
        element.addEventListener('mousedown', function (event) {
          that.drag = true;
          that.dragLast = that.dragOrigin = { x: event.pageX, y: event.pageY };
          event.preventDefault();
        });
        document.addEventListener('mouseup', function (event) {
          that.drag = false;
        });
        document.addEventListener('mousemove', function (event) {
          if (!that.drag) return;
          var total = { x: event.pageX - that.dragOrigin.x, y: event.pageY - that.dragOrigin.y },
              delta = { x: event.pageX - that.dragLast.x, y: event.pageY - that.dragLast.y };
          that.dragLast = { x: event.pageX, y: event.pageY };
          mousemove(that.dragOrigin, total, delta);
        });

        function mousemove(origin, total, delta) {
          var phi = tangle.getValue('phi') + delta.x * .01,
              theta = Math.min(1, Math.max(-.2, tangle.getValue('theta') + delta.y * .01));

          tangle.setValue('phi', phi);
          tangle.setValue('theta', theta);
        }
      },

      update: function (element, value) {
        this.element.style.WebkitTransform = value;
        this.element.style.MozTransform = value;
        this.element.style.transform = value;
      },
    },

    Tangle.classes.Slider = {
      initialize: function (element, options, tangle, variables) {
        var that = this;

        this.tangle = tangle;
        this.element = element;

        this.bar = document.createElement('div');
        this.bar.className = 'bar';
        this.element.appendChild(this.bar);

        this.handle = document.createElement('div');
        this.handle.className = 'handle';
        this.element.appendChild(this.handle);

        this.element.addEventListener('mousedown', function (event) {
          var el = this.element, o = 0;
          do {
            o += el.offsetLeft;
            el = el.offsetParent;
          } while (el);

          this.origin = o;
          this.width = this.bar.offsetWidth;
          this.drag = true;
          return false;
        }.bind(this));

        document.addEventListener('mousemove', function (event) {
          if (!that.drag) return;
          tangle.setValue('slice', Math.max(0, Math.min(1, (event.pageX - that.origin) / that.width)));
        });
        document.addEventListener('mouseup', function () {
          that.drag = false;
        });
      },

      update: function (element, value) {
        this.handle.style.left = (100*value) + '%';
      },
    },

    Tangle.classes.VolumetricView = {
      initialize: function (element, options, tangle, variables) {
        var that = this;

        this.tangle = tangle;
        this.element = element;

        this.width = 364;
        this.height = 384;
        this.depth = 256;

        this.resX = 182;
        this.resY = 192;
        this.slices = 108;
        this.stride = 8;
        this.rows = Math.ceil(this.slices / this.stride);

        this.createSlices();

        var load = 0;
        this.image = new Image();
        this.image.onload = function () {
          if (++load == 2) that.drawSlices(); 
        };

        this.mask = new Image();
        this.mask.onload = function () {
          if (++load == 2) that.drawSlices(); 
        };

//        this.image.src = 'data/MRbrain.png';
        this.image.src = '/files/mri/MRbrain-color.jpg';
        this.mask.src = '/files/mri/MRbrain-alpha8.png';
      },

      update: function (element, value) {
        var l = Math.abs(Math.cos(value)) > Math.abs(Math.sin(value));

        if (this.l != l || this.slice != slice) {
          var slice = this.tangle.getValue('slice'), index, 
              n = (l ? Math.cos(value) : Math.sin(value)) > 0,
              sn = n ? slice : 1 - slice;

          index = +(this.slicesX.length * sn);

          var display = l ? 'block' : 'none';
          forEach(this.slicesX, function (el, i) {
            el.style.display = display;

            var opacity;
            if (i >= index) {
              opacity = n ? .95 : .001;
            }
            else {
              opacity = !n ? .95 : .001;
            }

            el.style.opacity = opacity;
          });

          index = +(this.slicesZ.length * sn);

          var display = !l ? 'block' : 'none';
          forEach(this.slicesZ, function (el, i) {
            el.style.display = display;

            var opacity;
            if (i >= index) {
              opacity = n ? .95 : .001;
            }
            else {
              opacity = !n ? .95 : .001;
            }

            el.style.opacity = opacity;
          });

          this.slice = slice;
          this.l = l;
        }
      },

      createSlices: function () {
        this.element.innerHTML = '';
        this.ctxX = [];
        this.ctxZ = [];

        // X slices
        for (var i = 0; i < this.slices; ++i) {
          var z = -((i / this.slices) - .5) * this.depth,
              t = 'translateZ(' + z + 'px) translateX(70px)';

          var canvas = document.createElement('canvas');
          canvas.className = 'x';
          canvas.width = this.resX;
          canvas.height = this.resY;
          canvas.style.width = this.width + 'px';
          canvas.style.height = this.height + 'px';
          canvas.style.WebkitTransform = t;
          canvas.style.MozTransform = t;
          canvas.style.transform = t;
          canvas.style.position = 'absolute';

          this.element.appendChild(canvas);
          this.ctxX.push(canvas.getContext('2d'));
        }

        // Z slices
        for (var i = 0; i < this.resX; ++i) {
          var z = -(this.depth - this.width) / 2,
              x = ((i / this.resX) - .5) * this.width,
              t = 'translateX(' + x + 'px) translateX(70px) rotateY(90deg) translateZ(' + z + 'px)';

          var canvas = document.createElement('canvas');
          canvas.className = 'z';
          canvas.width = this.slices;
          canvas.height = this.resY;
          canvas.style.width = this.depth + 'px';
          canvas.style.height = this.height + 'px';
          canvas.style.WebkitTransform = t;
          canvas.style.MozTransform = t;
          canvas.style.transform = t;
          canvas.style.opacity = 0;
          canvas.style.position = 'absolute';

          this.element.appendChild(canvas);
          this.ctxZ.push(canvas.getContext('2d'));
        }

        this.slicesX = this.element.querySelectorAll('canvas.x');
        this.slicesZ = this.element.querySelectorAll('canvas.z');
      },

      drawSlices: function () {

        var s = this.stride,
            sl = this.slices,
            r = this.rows,
            w = this.resX,
            h = this.resY,
            img = this.image,
            mask = this.mask,
            ctxX = this.ctxX,
            ctxZ = this.ctxZ;

        var alpha, color;

        // X slices
        forEach(this.slicesX, function (slice, i) {
          var c = ctxX[i],
              ox = (i % s) * w, oy = Math.floor(i / s) * h;

          // Draw alpha channel and get pixels
          c.drawImage(mask, ox, oy, w, h, 0, 0, w, h);
          alpha = c.getImageData(0, 0, w, h);

          // Draw color channel and get pixels
          c.drawImage(img, ox, oy, w, h, 0, 0, w, h);
          color = c.getImageData(0, 0, w, h);

          // Copy red to alpha.
          var src = alpha.data, dst = color.data;
          for (var y = 0; y < h; ++y) {
            for (var x = 0; x < w; ++x) {
              var o = (x + y * w) * 4;
              dst[o + 3] = src[o];
            }
          }

          // Draw RGBA.
          c.putImageData(color, 0, 0);
        });

        // Z slices
        forEach(this.slicesZ, function (slice, i) {
          var c = ctxZ[i];

          // Render transposed slices as vertical strips.
          for (var j = 0; j < sl; ++j) {
            var ox = (j % s) * w, oy = Math.floor(j / s) * h;

            // Draw alpha channel
            c.drawImage(mask, ox + i, oy, 1, h, j, 0, 1, h);
          }

          // Get pixels
          alpha = c.getImageData(0, 0, w, h);

          // Render transposed slices as vertical strips.
          for (var j = 0; j < sl; ++j) {
            var ox = (j % s) * w, oy = Math.floor(j / s) * h;

            // Draw color channel
            c.drawImage(img, ox + i, oy, 1, h, j, 0, 1, h);
          }
          // Get pixels
          color = c.getImageData(0, 0, w, h);

          // Copy red to alpha.
          var src = alpha.data, dst = color.data;
          for (var y = 0; y < h; ++y) {
            for (var x = 0; x < w; ++x) {
              var o = (x + y * w) * 4;
              dst[o + 3] = src[o];
            }
          }

          // Draw RGBA.
          c.putImageData(color, 0, 0);
        });
      },

    };

    var tangle = new Tangle(document.querySelector('#head-3d'), model);

}, 200);
// -->
</script>

<script>
// <!--
//
//  Tangle.js
//  Tangle 0.1.0
//
//  Created by Bret Victor on 5/2/10.
//  (c) 2011 Bret Victor.  MIT open-source license.

var Tangle = this.Tangle = function (rootElement, modelClass) {

    var tangle = this;
    tangle.element = rootElement;
    tangle.setModel = setModel;
    tangle.getValue = getValue;
    tangle.setValue = setValue;
    tangle.setValues = setValues;

    var _model;
    var _nextSetterID = 0;
    var _setterInfosByVariableName = {};   //  { varName: { setterID:7, setter:function (v) { } }, ... }
    var _varargConstructorsByArgCount = [];


    //
    // construct

    initializeElements();
    setModel(modelClass);
    return tangle;


    //
    // elements

    function initializeElements() {
        var elements = rootElement.getElementsByTagName("*");
        var interestingElements = [];
        
        // build a list of elements with class or data-var attributes
        
        for (var i = 0, length = elements.length; i < length; i++) {
            var element = elements[i];
            if (element.getAttribute("class") || element.getAttribute("data-var")) {
                interestingElements.push(element);
            }
        }

        // initialize interesting elements in this list.  (Can't traverse "elements"
        // directly, because elements is "live", and views that change the node tree
        // will change elements mid-traversal.)
        
        for (var i = 0, length = interestingElements.length; i < length; i++) {
            var element = interestingElements[i];
            
            var varNames = null;
            var varAttribute = element.getAttribute("data-var");
            if (varAttribute) { varNames = varAttribute.split(" "); }

            var views = null;
            var classAttribute = element.getAttribute("class");
            if (classAttribute) {
                var classNames = classAttribute.split(" ");
                views = getViewsForElement(element, classNames, varNames);
            }
            
            if (!varNames) { continue; }
            
            var didAddSetter = false;
            if (views) {
                for (var j = 0; j < views.length; j++) {
                    if (!views[j].update) { continue; }
                    addViewSettersForElement(element, varNames, views[j]);
                    didAddSetter = true;
                }
            }
            
            if (!didAddSetter) {
                var formatAttribute = element.getAttribute("data-format");
                var formatter = getFormatterForFormat(formatAttribute, varNames);
                addFormatSettersForElement(element, varNames, formatter);
            }
        }
    }
            
    function getViewsForElement(element, classNames, varNames) {   // initialize classes
        var views = null;
        
        for (var i = 0, length = classNames.length; i < length; i++) {
            var clas = Tangle.classes[classNames[i]];
            if (!clas) { continue; }
            
            var options = getOptionsForElement(element);
            var args = [ element, options, tangle ];
            if (varNames) { args = args.concat(varNames); }
            
            var view = constructClass(clas, args);
            
            if (!views) { views = []; }
            views.push(view);
        }
        
        return views;
    }
    
    function getOptionsForElement(element) {   // might use dataset someday
        var options = {};

        var attributes = element.attributes;
        var regexp = /^data-[\w\-]+$/;

        for (var i = 0, length = attributes.length; i < length; i++) {
            var attr = attributes[i];
            var attrName = attr.name;
            if (!attrName || !regexp.test(attrName)) { continue; }
            
            options[attrName.substr(5)] = attr.value;
        }
         
        return options;   
    }
    
    function constructClass(clas, args) {
        if (typeof clas !== "function") {  // class is prototype object
            var View = function () { };
            View.prototype = clas;
            var view = new View();
            if (view.initialize) { view.initialize.apply(view,args); }
            return view;
        }
        else {  // class is constructor function, which we need to "new" with varargs (but no built-in way to do so)
            var ctor = _varargConstructorsByArgCount[args.length];
            if (!ctor) {
                var ctorArgs = [];
                for (var i = 0; i < args.length; i++) { ctorArgs.push("args[" + i + "]"); }
                var ctorString = "(function (clas,args) { return new clas(" + ctorArgs.join(",") + "); })";
                ctor = eval(ctorString);   // nasty
                _varargConstructorsByArgCount[args.length] = ctor;   // but cached
            }
            return ctor(clas,args);
        }
    }
    

    //
    // formatters

    function getFormatterForFormat(formatAttribute, varNames) {
        if (!formatAttribute) { formatAttribute = "default"; }

        var formatter = getFormatterForCustomFormat(formatAttribute, varNames);
        if (!formatter) { formatter = getFormatterForSprintfFormat(formatAttribute, varNames); }
        if (!formatter) { log("Tangle: unknown format: " + formatAttribute); formatter = getFormatterForFormat(null,varNames); }

        return formatter;
    }
        
    function getFormatterForCustomFormat(formatAttribute, varNames) {
        var components = formatAttribute.split(" ");
        var formatName = components[0];
        if (!formatName) { return null; }
        
        var format = Tangle.formats[formatName];
        if (!format) { return null; }
        
        var formatter;
        var params = components.slice(1);
        
        if (varNames.length <= 1 && params.length === 0) {  // one variable, no params
            formatter = format;
        }
        else if (varNames.length <= 1) {  // one variable with params
            formatter = function (value) {
                var args = [ value ].concat(params);
                return format.apply(null, args);
            };
        }
        else {  // multiple variables
            formatter = function () {
                var values = getValuesForVariables(varNames);
                var args = values.concat(params);
                return format.apply(null, args);
            };
        }
        return formatter;
    }
    
    function getFormatterForSprintfFormat(formatAttribute, varNames) {
        if (!sprintf || !formatAttribute.test(/\%/)) { return null; }

        var formatter;
        if (varNames.length <= 1) {  // one variable
            formatter = function (value) {
                return sprintf(formatAttribute, value);
            };
        }
        else {
            formatter = function (value) {  // multiple variables
                var values = getValuesForVariables(varNames);
                var args = [ formatAttribute ].concat(values);
                return sprintf.apply(null, args);
            };
        }
        return formatter;
    }

    
    //
    // setters
    
    function addViewSettersForElement(element, varNames, view) {   // element has a class with an update method
        var setter;
        if (varNames.length <= 1) {
            setter = function (value) { view.update(element, value); };
        }
        else {
            setter = function () {
                var values = getValuesForVariables(varNames);
                var args = [ element ].concat(values);
                view.update.apply(view,args);
            };
        }

        addSetterForVariables(setter, varNames);
    }

    function addFormatSettersForElement(element, varNames, formatter) {  // tangle is injecting a formatted value itself
        var span = null;
        var setter = function (value) {
            if (!span) { 
                span = document.createElement("span");
                element.insertBefore(span, element.firstChild);
            }
            span.innerHTML = formatter(value);
        };

        addSetterForVariables(setter, varNames);
    }
    
    function addSetterForVariables(setter, varNames) {
        var setterInfo = { setterID:_nextSetterID, setter:setter };
        _nextSetterID++;

        for (var i = 0; i < varNames.length; i++) {
            var varName = varNames[i];
            if (!_setterInfosByVariableName[varName]) { _setterInfosByVariableName[varName] = []; }
            _setterInfosByVariableName[varName].push(setterInfo);
        }
    }

    function applySettersForVariables(varNames) {
        var appliedSetterIDs = {};  // remember setterIDs that we've applied, so we don't call setters twice
    
        for (var i = 0, ilength = varNames.length; i < ilength; i++) {
            var varName = varNames[i];
            var setterInfos = _setterInfosByVariableName[varName];
            if (!setterInfos) { continue; }
            
            var value = _model[varName];
            
            for (var j = 0, jlength = setterInfos.length; j < jlength; j++) {
                var setterInfo = setterInfos[j];
                if (setterInfo.setterID in appliedSetterIDs) { continue; }  // if we've already applied this setter, move on
                appliedSetterIDs[setterInfo.setterID] = true;
                
                setterInfo.setter(value);
            }
        }
    }
    

    //
    // variables

    function getValue(varName) {
        var value = _model[varName];
        if (value === undefined) { log("Tangle: unknown variable: " + varName);  return 0; }
        return value;
    }

    function setValue(varName, value) {
        var obj = {};
        obj[varName] = value;
        setValues(obj);
    }

    function setValues(obj) {
        var changedVarNames = [];

        for (var varName in obj) {
            var value = obj[varName];
            var oldValue = _model[varName];
            if (oldValue === undefined) { log("Tangle: setting unknown variable: " + varName);  continue; }
            if (oldValue === value) { continue; }  // don't update if new value is the same

            _model[varName] = value;
            changedVarNames.push(varName);
        }
        
        if (changedVarNames.length) {
            applySettersForVariables(changedVarNames);
            updateModel();
        }
    }
    
    function getValuesForVariables(varNames) {
        var values = [];
        for (var i = 0, length = varNames.length; i < length; i++) {
            values.push(getValue(varNames[i]));
        }
        return values;
    }

                    
    //
    // model

    function setModel(modelClass) {
        var ModelClass = function () { };
        ModelClass.prototype = modelClass;
        _model = new ModelClass;

        updateModel(true);  // initialize and update
    }
    
    function updateModel(shouldInitialize) {
        var ShadowModel = function () {};  // make a shadow object, so we can see exactly which properties changed
        ShadowModel.prototype = _model;
        var shadowModel = new ShadowModel;
        
        if (shouldInitialize) { shadowModel.initialize(); }
        shadowModel.update();
        
        var changedVarNames = [];
        for (var varName in shadowModel) {
            if (!shadowModel.hasOwnProperty(varName)) { continue; }
            if (_model[varName] === shadowModel[varName]) { continue; }
            
            _model[varName] = shadowModel[varName];
            changedVarNames.push(varName);
        }
        
        applySettersForVariables(changedVarNames);
    }


    //
    // debug

    function log (msg) {
        if (window.console) { window.console.log(msg); }
    }

};  // end of Tangle


//
// components

Tangle.classes = {};
Tangle.formats = {};

Tangle.formats["default"] = function (value) { return "" + value; };

// -->
</script>

]]></content>
  </entry>
  
</feed>
