add https://feeds.feedburner.com/JohnAustinBlog - sfeed_tests - sfeed tests and RSS and Atom files
(HTM) git clone git://git.codemadness.org/sfeed_tests
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
(DIR) commit 8b878f33850754fb3842fde08a1ebb00c6ec20d2
(DIR) parent 3b7970f5fe3ee2c8eb3accd2244f9a0b209cf43b
(HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date: Sun, 19 Jan 2025 18:31:46 +0100
add https://feeds.feedburner.com/JohnAustinBlog
Generated by Site-Server v@build.version@ (http://www.squarespace.com)
<generator>
Site-Server v@build.version@ (http://www.squarespace.com)
Diffstat:
A input/sfeed/realworld/johnaustin_s… | 7146 +++++++++++++++++++++++++++++++
1 file changed, 7146 insertions(+), 0 deletions(-)
---
(DIR) diff --git a/input/sfeed/realworld/johnaustin_squarespace.rss.xml b/input/sfeed/realworld/johnaustin_squarespace.rss.xml
@@ -0,0 +1,7145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--Generated by Site-Server v@build.version@ (http://www.squarespace.com) on Sat, 18 Jan 2025 10:00:47 GMT
+--><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://www.rssboard.org/media-rss" version="2.0"><channel><title>John Austin</title><link>https://johnaustin.io/</link><lastBuildDate>Tue, 14 Jan 2025 04:17:02 +0000</lastBuildDate><language>en-US</language><generator>Site-Server v@build.version@ (http://www.squarespace.com)</generator><description><![CDATA[]]></description><item><title>Issues with Color Spaces and Perceptual Brightness</title><category>Article</category><category>Technical</category><dc:creator>John Austin</dc:creator><pubDate>Tue, 14 Jan 2025 04:56:21 +0000</pubDate><link>https://johnaustin.io/articles/2025/issues-with-cielab-and-perceptual-brightness</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:6785e53e87fad21239890f26</guid><description><![CDATA[<p class="">Unlike RGB, the <a href="https://en.wikipedia.org/wiki/CIELAB_color_space" target="_blank">CIELab</a> color space<a href="#margin" target="_blank">And the more modern variants like CIECAM02 and Oklab.</a> is designed to be perceptually uniform. This is a broad goal, which roughly means that we would like changes in the numbers to match same changes that we observe when performing various studies of human perception. </p><p class="">It’s a bit less 'math’ and more ‘modeling’. We try to fit equations to the observed data with the lowest error possible, across many different axis. It’s helpful to imagine adding the word ‘predicted’ in front of all of the CIELAB terminology (ie. Predicted Perceptual Lightness), because it’s just trying to predict human test results! </p><p class="">This process means that there is some error. For example, ideally, a color with L=50 looks twice as bright as a color with L=25. Except, with very strongly saturated colors like red, this isn’t actually the case in any of these color spaces.</p><p class="">This is what’s called the <a href="https://en.wikipedia.org/wiki/Helmholtz%E2%80%93Kohlrausch_effect">Helmholtz-Kohlrausch effect</a>. Take a look at the below panel of colors. Most humans agree that the red is quite a bit brighter than the others in the row, and yet they all have the exact same lightness value when calculated with something like CIELAB!</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/eaf70676-eb13-4d48-b0b1-7051995f3027/Helmholtz-Kohlrausch_effect_visualized_improved.png" data-image-dimensions="440x181" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/eaf70676-eb13-4d48-b0b1-7051995f3027/Helmholtz-Kohlrausch_effect_visualized_improved.png?format=1000w" width="440" height="181" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/eaf70676-eb13-4d48-b0b1-7051995f3027/Helmholtz-Kohlrausch_effect_visualized_improved.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/eaf70676-eb13-4d48-b0b1-7051995f3027/Helmholtz-Kohlrausch_effect_visualized_improved.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/eaf70676-eb13-4d48-b0b1-7051995f3027/Helmholtz-Kohlrausch_effect_visualized_improved.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/eaf70676-eb13-4d48-b0b1-7051995f3027/Helmholtz-Kohlrausch_effect_visualized_improved.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/eaf70676-eb13-4d48-b0b1-7051995f3027/Helmholtz-Kohlrausch_effect_visualized_improved.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/eaf70676-eb13-4d48-b0b1-7051995f3027/Helmholtz-Kohlrausch_effect_visualized_improved.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/eaf70676-eb13-4d48-b0b1-7051995f3027/Helmholtz-Kohlrausch_effect_visualized_improved.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">A set of colors demonstrating the Helmholtz-Kohlrausch effect. The red appears more vivid to the human eye.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">There is some recent <a href="https://onlinelibrary.wiley.com/doi/10.1002/col.22839" target="_blank">research</a> I found that models this effect and applies an additional transform to account for it, building off of work from <a href="https://onlinelibrary.wiley.com/doi/abs/10.1002/col.5080160608" target="_blank">Fairchild MD, Pirrotta E</a> in the 90’s. The end result is labeled as the “Predicted Equivalent Achromatic Lightness”, or in other words “The lightness of the gray that most closely matches the perceived lightness”. </p><p class="">This is actually quite a useful value, because it’s precisely the value we want when desaturating an image! Most desaturation operations spit out a [0..1] value which is then (later) rendered as an RGB gray color. Using the L_EAL value instead jumps us right to the end, giving the exact perceptual gray color to place in the resulting desaturated image.</p><p class="">How much of an issue is this? Well, I noticed. I’ve been writing a tool to desaturate game screenshots, to help us evaluate relative brightnesses between art assets, and improve overall game readability. All of the red assets appeared.. strangely dark in the desaturated images, even when using CIELAB!</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c6de87d7-0d4a-4c3e-b748-632d6ed661a3/Screenshot+2025-01-13+at+8.51.36%E2%80%AFPM.png" data-image-dimensions="1624x1458" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c6de87d7-0d4a-4c3e-b748-632d6ed661a3/Screenshot+2025-01-13+at+8.51.36%E2%80%AFPM.png?format=1000w" width="1624" height="1458" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c6de87d7-0d4a-4c3e-b748-632d6ed661a3/Screenshot+2025-01-13+at+8.51.36%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c6de87d7-0d4a-4c3e-b748-632d6ed661a3/Screenshot+2025-01-13+at+8.51.36%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c6de87d7-0d4a-4c3e-b748-632d6ed661a3/Screenshot+2025-01-13+at+8.51.36%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c6de87d7-0d4a-4c3e-b748-632d6ed661a3/Screenshot+2025-01-13+at+8.51.36%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c6de87d7-0d4a-4c3e-b748-632d6ed661a3/Screenshot+2025-01-13+at+8.51.36%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c6de87d7-0d4a-4c3e-b748-632d6ed661a3/Screenshot+2025-01-13+at+8.51.36%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c6de87d7-0d4a-4c3e-b748-632d6ed661a3/Screenshot+2025-01-13+at+8.51.36%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Before desaturating.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/8c11be87-6530-4af8-94ab-e78a142a2927/Screenshot+2025-01-13+at+8.51.42%E2%80%AFPM.png" data-image-dimensions="1374x1468" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/8c11be87-6530-4af8-94ab-e78a142a2927/Screenshot+2025-01-13+at+8.51.42%E2%80%AFPM.png?format=1000w" width="1374" height="1468" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/8c11be87-6530-4af8-94ab-e78a142a2927/Screenshot+2025-01-13+at+8.51.42%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/8c11be87-6530-4af8-94ab-e78a142a2927/Screenshot+2025-01-13+at+8.51.42%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/8c11be87-6530-4af8-94ab-e78a142a2927/Screenshot+2025-01-13+at+8.51.42%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/8c11be87-6530-4af8-94ab-e78a142a2927/Screenshot+2025-01-13+at+8.51.42%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/8c11be87-6530-4af8-94ab-e78a142a2927/Screenshot+2025-01-13+at+8.51.42%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/8c11be87-6530-4af8-94ab-e78a142a2927/Screenshot+2025-01-13+at+8.51.42%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/8c11be87-6530-4af8-94ab-e78a142a2927/Screenshot+2025-01-13+at+8.51.42%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">After desaturating.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">The reds are a bit darker here, but I think we can agree that they’re not that dark! This is because CIELAB does not account for the Helmholtz-Kohlrausch effect and so it undervalues the amount that red’s saturation contributes to the final lightness. And if we had used this tool to spot check, it might have caused us to make all of our red assets too bright to balance it out. Weird.</p><p class=""><br>Unfortunately, I haven’t been able to find any perceptually uniform color spaces that seem to include these transformations in the final output space. If you’re aware of one, I would love to know.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <p class=""><em>Update: Several folks on HackerNews had some good suggestions for better desaturation models.</em></p><p class="">The first is OSA-UCS or the Darktable UCS. This was designed for photo manipulation and there is a fantastic research explanation here: <a href="https://eng.aurelienpierre.com/2022/02/color-saturation-control-for-the-21th-century/#fn:11">https://eng.aurelienpierre.com/2022/02/color-saturation-control-for-the-21th-century/#fn:11</a></p><p class="">Second is Color2Gray. This tool goes further than any others, considering <em>the contextual surrounding</em> of each pixel and solves an optimization problem, to determine a sensible gray color that retains color differentials in the original image. This is not precisely desaturation, but may do a good job if that’s your goal.</p>]]></description></item><item><title>Composability: Designing a Visual Programming Language</title><category>Technical</category><dc:creator>John Austin</dc:creator><pubDate>Sat, 20 Apr 2024 22:52:29 +0000</pubDate><link>https://johnaustin.io/articles/2024/composability-designing-a-visual-programming-language</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:66243a9b5b6abb3c12bd039d</guid><description><![CDATA[<p class=""><em>Lattice is a high-performance visual scripting system targeting Unity ECS. Read more </em><a href="https://forum.unity.com/threads/lattice-visual-scripting-for-ecs.1508402/"><em>here</em></a><em>.</em></p><p class="">I wanted to write a few posts on the design of Lattice as a language. Today, let's focus on “composability”. This is intuitively something we desire in programming languages. Some systems feel like they are effortlessly reconfigurable, recombinable, and then others.. just don’t. Some languages seem to actively reject our efforts at organization.</p><p class="">So what makes a system composable? I see two major properties:</p><ol data-rte-list="default"><li><p class=""><strong>Self-Similarity</strong> — Composable systems usually have a simple primitive that is self-similar. Think of a game grid, tetris pieces, repeating tessellations, etc. Self-similarity means most things can plug into other things. They may not do something <em>useful,</em> but they can be infinitely re-arranged.</p></li><li><p class=""><strong>Merging / Splitting</strong> — Two groups of primitives can be <strong>merged</strong> into a single primitive, and that 'merged primitive' acts just like any other primitive. Similarly, a primitive can be <strong>split</strong> into several 'pieces' and each of those pieces acts like any other primitive.</p></li></ol><p class="">Take Legos, perhaps the most composable system in existence. So composable, in fact, that talking about them is almost reductive. But they follow these rules deeply:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/3e725a3f-1c4a-4379-a506-bcaaa02a98a3/Untitled-2024-02-08-1201%2819%29.png" data-image-dimensions="1923x1244" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/3e725a3f-1c4a-4379-a506-bcaaa02a98a3/Untitled-2024-02-08-1201%2819%29.png?format=1000w" width="1923" height="1244" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/3e725a3f-1c4a-4379-a506-bcaaa02a98a3/Untitled-2024-02-08-1201%2819%29.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/3e725a3f-1c4a-4379-a506-bcaaa02a98a3/Untitled-2024-02-08-1201%2819%29.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/3e725a3f-1c4a-4379-a506-bcaaa02a98a3/Untitled-2024-02-08-1201%2819%29.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/3e725a3f-1c4a-4379-a506-bcaaa02a98a3/Untitled-2024-02-08-1201%2819%29.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/3e725a3f-1c4a-4379-a506-bcaaa02a98a3/Untitled-2024-02-08-1201%2819%29.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/3e725a3f-1c4a-4379-a506-bcaaa02a98a3/Untitled-2024-02-08-1201%2819%29.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/3e725a3f-1c4a-4379-a506-bcaaa02a98a3/Untitled-2024-02-08-1201%2819%29.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Notice how not only can Legos be split into separate pieces, but they can be split in <span>many different ways</span>. Put another way, Legos have <strong>lots of seams</strong>: places where they can be split apart into component chunks.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c91825a1-af75-422d-b906-6e545f1be831/Untitled-2024-02-08-1201%2820%29.png" data-image-dimensions="1974x1310" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c91825a1-af75-422d-b906-6e545f1be831/Untitled-2024-02-08-1201%2820%29.png?format=1000w" width="1974" height="1310" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c91825a1-af75-422d-b906-6e545f1be831/Untitled-2024-02-08-1201%2820%29.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c91825a1-af75-422d-b906-6e545f1be831/Untitled-2024-02-08-1201%2820%29.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c91825a1-af75-422d-b906-6e545f1be831/Untitled-2024-02-08-1201%2820%29.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c91825a1-af75-422d-b906-6e545f1be831/Untitled-2024-02-08-1201%2820%29.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c91825a1-af75-422d-b906-6e545f1be831/Untitled-2024-02-08-1201%2820%29.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c91825a1-af75-422d-b906-6e545f1be831/Untitled-2024-02-08-1201%2820%29.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c91825a1-af75-422d-b906-6e545f1be831/Untitled-2024-02-08-1201%2820%29.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Lego chunks can be split along many different seams.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">A system with many seams is indicative of a very composable system: new behavior is built purely through composition, not by defining new primitives.</p><p class="">I think about composability a lot in my game design work, why do some game mechanics have so much emergent behavior? We can see splitting/merging at work there too. Factorio, with its endlessly recombinable and splittable factory tiles. Or a roguelike deck-builders with their infinitely splittable/mergeable decks. Or an action game with 20 different abilities giving you an incredible number of seams to rip apart builds and reconfigure them.</p><h2>Blueprints are not composable.</h2>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<p>Let's get back to Lattice, and visual programming language design. Unreal Engine’s Blueprints are a common design for visual programming, roughly mirroring code. But despite how easy it is to sketch something out, they seem to reject organization.<footnote data-preserve-html-node="true">Common wisdom from large teams in UE is “design in blueprints, then rewrite in C++”.</footnote> It's shockingly tough to split a messy graph into reusable, smaller nodes. It's also pretty tough to merge several nodes into a single multi-purpose node. Why is that?</p>
+
+
+
+
+ <p class="">In my opinion, the problem is execution wires.</p><p class="">Execution wires don't have a trivial merge operation. Let's say I have two nodes I want to combine into a single node:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/193b73ab-d8e3-49bf-bc3a-b6f54de02276/Untitled-2024-02-08-1201%2810%29.png" data-image-dimensions="719x516" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/193b73ab-d8e3-49bf-bc3a-b6f54de02276/Untitled-2024-02-08-1201%2810%29.png?format=1000w" width="719" height="516" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/193b73ab-d8e3-49bf-bc3a-b6f54de02276/Untitled-2024-02-08-1201%2810%29.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/193b73ab-d8e3-49bf-bc3a-b6f54de02276/Untitled-2024-02-08-1201%2810%29.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/193b73ab-d8e3-49bf-bc3a-b6f54de02276/Untitled-2024-02-08-1201%2810%29.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/193b73ab-d8e3-49bf-bc3a-b6f54de02276/Untitled-2024-02-08-1201%2810%29.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/193b73ab-d8e3-49bf-bc3a-b6f54de02276/Untitled-2024-02-08-1201%2810%29.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/193b73ab-d8e3-49bf-bc3a-b6f54de02276/Untitled-2024-02-08-1201%2810%29.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/193b73ab-d8e3-49bf-bc3a-b6f54de02276/Untitled-2024-02-08-1201%2810%29.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/2c514df8-5857-4f3d-9797-7499dde8c12d/Untitled-2024-02-08-1201%288%29.png" data-image-dimensions="716x634" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/2c514df8-5857-4f3d-9797-7499dde8c12d/Untitled-2024-02-08-1201%288%29.png?format=1000w" width="716" height="634" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/2c514df8-5857-4f3d-9797-7499dde8c12d/Untitled-2024-02-08-1201%288%29.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/2c514df8-5857-4f3d-9797-7499dde8c12d/Untitled-2024-02-08-1201%288%29.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/2c514df8-5857-4f3d-9797-7499dde8c12d/Untitled-2024-02-08-1201%288%29.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/2c514df8-5857-4f3d-9797-7499dde8c12d/Untitled-2024-02-08-1201%288%29.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/2c514df8-5857-4f3d-9797-7499dde8c12d/Untitled-2024-02-08-1201%288%29.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/2c514df8-5857-4f3d-9797-7499dde8c12d/Untitled-2024-02-08-1201%288%29.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/2c514df8-5857-4f3d-9797-7499dde8c12d/Untitled-2024-02-08-1201%288%29.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+<p>What do you do about the execution wires? It's illegal to split or merge an execution wire. Doing so would require a fundamentally parallel execution environment.<footnote data-preserve-html-node="true">It’s fun to muse on a language where splitting an execution wire means “run these operations in parallel”. It’s a bit of a can of worms, though, that PL folks have been attempting for decades.</footnote> You must manually order them, instead. But which one should execute first? Even if it doesn’t actually matter, the semantics of the language require you to pick an option.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="true" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/273559e6-9355-419d-853a-17f721ad2477/Untitled-2024-02-08-1201%284%29.png" data-image-dimensions="945x626" data-image-focal-point="0.4894516363728049,0.24" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/273559e6-9355-419d-853a-17f721ad2477/Untitled-2024-02-08-1201%284%29.png?format=1000w" width="945" height="626" sizes="(max-width: 640px) 100vw, (max-width: 767px) 75vw, 75vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/273559e6-9355-419d-853a-17f721ad2477/Untitled-2024-02-08-1201%284%29.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/273559e6-9355-419d-853a-17f721ad2477/Untitled-2024-02-08-1201%284%29.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/273559e6-9355-419d-853a-17f721ad2477/Untitled-2024-02-08-1201%284%29.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/273559e6-9355-419d-853a-17f721ad2477/Untitled-2024-02-08-1201%284%29.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/273559e6-9355-419d-853a-17f721ad2477/Untitled-2024-02-08-1201%284%29.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/273559e6-9355-419d-853a-17f721ad2477/Untitled-2024-02-08-1201%284%29.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/273559e6-9355-419d-853a-17f721ad2477/Untitled-2024-02-08-1201%284%29.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Does thing 2 or thing 1 happen first? Does it matter?</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+ <p>In this situation, Blueprints just gives up, exposing two execution wire inputs — the worst of all worlds. The issue here isn't Blueprints, itself. Execution wires are just a fundamentally non-composable primitive.<footnote data-preserve-html-node="true">Foot note:<br data-preserve-html-node="true">Why does code work better?<br data-preserve-html-node="true"><br data-preserve-html-node="true">It's interesting to ask why text code doesn't seem to have this problem. After all, C++ has 'execution wires', so to speak, between each line, but doesn't have these compositional issues. However, the execution wires in text code are 'implicit' -- it's based on the linear order of the text in the file. Every time you make an edit, you're answering the ordering question.<br data-preserve-html-node="true"><br data-preserve-html-node="true">With a visual programming language lifted to a 2D plane, we can't rely on that luxury because there is no implicit linear order of nodes. Of course, we can implicitly order nodes in other ways.</footnote></p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/a9618c39-05f9-428d-9faa-ff8447a7b86a/UnrealEditor_Niv5KzZ2No.png" data-image-dimensions="419x450" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/a9618c39-05f9-428d-9faa-ff8447a7b86a/UnrealEditor_Niv5KzZ2No.png?format=1000w" width="419" height="450" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/a9618c39-05f9-428d-9faa-ff8447a7b86a/UnrealEditor_Niv5KzZ2No.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/a9618c39-05f9-428d-9faa-ff8447a7b86a/UnrealEditor_Niv5KzZ2No.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/a9618c39-05f9-428d-9faa-ff8447a7b86a/UnrealEditor_Niv5KzZ2No.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/a9618c39-05f9-428d-9faa-ff8447a7b86a/UnrealEditor_Niv5KzZ2No.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/a9618c39-05f9-428d-9faa-ff8447a7b86a/UnrealEditor_Niv5KzZ2No.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/a9618c39-05f9-428d-9faa-ff8447a7b86a/UnrealEditor_Niv5KzZ2No.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/a9618c39-05f9-428d-9faa-ff8447a7b86a/UnrealEditor_Niv5KzZ2No.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Before merging.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/35cbdede-3f9f-4142-8f09-138571d475d7/UnrealEditor_7B4GtkzKby.png" data-image-dimensions="362x273" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/35cbdede-3f9f-4142-8f09-138571d475d7/UnrealEditor_7B4GtkzKby.png?format=1000w" width="362" height="273" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/35cbdede-3f9f-4142-8f09-138571d475d7/UnrealEditor_7B4GtkzKby.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/35cbdede-3f9f-4142-8f09-138571d475d7/UnrealEditor_7B4GtkzKby.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/35cbdede-3f9f-4142-8f09-138571d475d7/UnrealEditor_7B4GtkzKby.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/35cbdede-3f9f-4142-8f09-138571d475d7/UnrealEditor_7B4GtkzKby.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/35cbdede-3f9f-4142-8f09-138571d475d7/UnrealEditor_7B4GtkzKby.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/35cbdede-3f9f-4142-8f09-138571d475d7/UnrealEditor_7B4GtkzKby.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/35cbdede-3f9f-4142-8f09-138571d475d7/UnrealEditor_7B4GtkzKby.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">After merging.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <h2>A Better Option - Value Graphs</h2><p class="">What happens if we design a language without execution wires? Actually, there's plenty of precedent for this already:</p><ul data-rte-list="default"><li><p class=""><a href="https://www.youtube.com/watch?v=cWuvb_lunJI">Houdini</a> / Blender <a href="https://www.youtube.com/watch?v=aO0eUnu0hO0">Geometry Nodes</a> (Mesh Manipulation Tools)</p></li><li><p class=""><a href="https://visualprogramming.net/">VVVV</a>, <a href="https://derivative.ca/">TouchDesigner</a>, <a href="https://cables.gl/">Cables</a> (Live Effects Tools)</p></li><li><p class=""><a href="https://cycling74.com/products/max">MaxMSP</a>, <a href="https://puredata.info/">Pure Data</a> (Audio Synthesizers)</p></li><li><p class=""><a href="https://docs.unity3d.com/Packages/com.unity.shadergraph@17.0/manual/index.html">ShaderGraph</a>, <a href="https://docs.unrealengine.com/4.27/en-US/AnimatingObjects/SkeletalMeshAnimation/AnimBlueprints/AnimGraph/">Animation Blueprints</a> (Shaders, Animations)</p></li></ul>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img class="thumb-image" elementtiming="system-gallery-block-slider" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1713651469462-F1LE8BBLGFXNIYTAHAAY/Untitled+picture.jpg" data-image-dimensions="1200x725" data-image-focal-point="0.5,0.5" alt="Blender Geometry Nodes" data-load="false" data-image-id="66243f0d6b680d3fa2c78144" data-type="image" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1713651469462-F1LE8BBLGFXNIYTAHAAY/Untitled+picture.jpg?format=1000w" /><br>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img class="thumb-image" elementtiming="system-gallery-block-slider" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1713651469512-9JUEN65HJJ2O6COX0WL1/Untitled+picture.png" data-image-dimensions="564x610" data-image-focal-point="0.5,0.5" alt="Houdini" data-load="false" data-image-id="66243f0dbf77681507fcc7d1" data-type="image" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1713651469512-9JUEN65HJJ2O6COX0WL1/Untitled+picture.png?format=1000w" /><br>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <a tabindex="0" role="button" class="previous" aria-label="Previous Slide"
+ ></a>
+ <a tabindex="0" role="button" class="next" aria-label="Next Slide"
+ ></a>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">All of these languages are value-based expression graphs under the hood. You're applying operations to input <strong>values</strong>, to produce certain output <strong>values</strong>, sometimes with some additional side effects.</p><p class="">If there's no execution wires, how do you control the ordering of execution? Usually, you don't have to. When a node precisely defines the data it requires, the order of execution is implicit. When does a node run? After all its inputs have finished.</p><p class="">Blueprints actually has this in the form of Pure nodes. No surprise, these nodes are much easier to combine and reuse throughout your graphs. <a href="#margin" target="_blank">Sadly, most BP nodes are not pure, so the functionality is limited in its usefulness.</a></p><p class="">These are fundamentally composable! It's perfectly valid to split an incoming value wire (it's just a copy of the same value on either end).</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/4c99aa82-a683-4800-830f-c558dd22a318/Untitled-2024-02-08-1201%2811%29.png" data-image-dimensions="1240x1045" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/4c99aa82-a683-4800-830f-c558dd22a318/Untitled-2024-02-08-1201%2811%29.png?format=1000w" width="1240" height="1045" sizes="(max-width: 640px) 100vw, (max-width: 767px) 83.33333333333334vw, 83.33333333333334vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/4c99aa82-a683-4800-830f-c558dd22a318/Untitled-2024-02-08-1201%2811%29.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/4c99aa82-a683-4800-830f-c558dd22a318/Untitled-2024-02-08-1201%2811%29.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/4c99aa82-a683-4800-830f-c558dd22a318/Untitled-2024-02-08-1201%2811%29.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/4c99aa82-a683-4800-830f-c558dd22a318/Untitled-2024-02-08-1201%2811%29.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/4c99aa82-a683-4800-830f-c558dd22a318/Untitled-2024-02-08-1201%2811%29.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/4c99aa82-a683-4800-830f-c558dd22a318/Untitled-2024-02-08-1201%2811%29.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/4c99aa82-a683-4800-830f-c558dd22a318/Untitled-2024-02-08-1201%2811%29.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Something else to notice: in a value graph, any <a href="https://en.wikipedia.org/wiki/Cut_(graph_theory)" target="_blank">cut</a> of the graph is a valid way of creating a sub-graph. The edges that that cross the 'cut' are precisely your inputs and outputs. Here we’ve selected a group of sub-nodes with 1 input crossing, and 1 output crossing:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/5f18c130-eb2a-4e09-804f-fd98885c8c87/Untitled-2024-02-08-1201%2815%29.png" data-image-dimensions="1171x693" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/5f18c130-eb2a-4e09-804f-fd98885c8c87/Untitled-2024-02-08-1201%2815%29.png?format=1000w" width="1171" height="693" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/5f18c130-eb2a-4e09-804f-fd98885c8c87/Untitled-2024-02-08-1201%2815%29.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/5f18c130-eb2a-4e09-804f-fd98885c8c87/Untitled-2024-02-08-1201%2815%29.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/5f18c130-eb2a-4e09-804f-fd98885c8c87/Untitled-2024-02-08-1201%2815%29.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/5f18c130-eb2a-4e09-804f-fd98885c8c87/Untitled-2024-02-08-1201%2815%29.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/5f18c130-eb2a-4e09-804f-fd98885c8c87/Untitled-2024-02-08-1201%2815%29.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/5f18c130-eb2a-4e09-804f-fd98885c8c87/Untitled-2024-02-08-1201%2815%29.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/5f18c130-eb2a-4e09-804f-fd98885c8c87/Untitled-2024-02-08-1201%2815%29.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Before</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/7031c964-c751-4381-89ee-ef90365e901d/Untitled-2024-02-08-1201%2816%29.png" data-image-dimensions="869x440" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/7031c964-c751-4381-89ee-ef90365e901d/Untitled-2024-02-08-1201%2816%29.png?format=1000w" width="869" height="440" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/7031c964-c751-4381-89ee-ef90365e901d/Untitled-2024-02-08-1201%2816%29.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/7031c964-c751-4381-89ee-ef90365e901d/Untitled-2024-02-08-1201%2816%29.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/7031c964-c751-4381-89ee-ef90365e901d/Untitled-2024-02-08-1201%2816%29.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/7031c964-c751-4381-89ee-ef90365e901d/Untitled-2024-02-08-1201%2816%29.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/7031c964-c751-4381-89ee-ef90365e901d/Untitled-2024-02-08-1201%2816%29.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/7031c964-c751-4381-89ee-ef90365e901d/Untitled-2024-02-08-1201%2816%29.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/7031c964-c751-4381-89ee-ef90365e901d/Untitled-2024-02-08-1201%2816%29.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">After</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Here’s another (more invasive) cut, with 1 input edge and 3 output edges:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6dc6b49a-40be-4c8a-902f-af08eab2e72e/Untitled-2024-02-08-1201%2814%29.png" data-image-dimensions="1060x626" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6dc6b49a-40be-4c8a-902f-af08eab2e72e/Untitled-2024-02-08-1201%2814%29.png?format=1000w" width="1060" height="626" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6dc6b49a-40be-4c8a-902f-af08eab2e72e/Untitled-2024-02-08-1201%2814%29.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6dc6b49a-40be-4c8a-902f-af08eab2e72e/Untitled-2024-02-08-1201%2814%29.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6dc6b49a-40be-4c8a-902f-af08eab2e72e/Untitled-2024-02-08-1201%2814%29.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6dc6b49a-40be-4c8a-902f-af08eab2e72e/Untitled-2024-02-08-1201%2814%29.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6dc6b49a-40be-4c8a-902f-af08eab2e72e/Untitled-2024-02-08-1201%2814%29.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6dc6b49a-40be-4c8a-902f-af08eab2e72e/Untitled-2024-02-08-1201%2814%29.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6dc6b49a-40be-4c8a-902f-af08eab2e72e/Untitled-2024-02-08-1201%2814%29.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Before</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1b470c01-e76e-4aae-8c16-162a9d7c060d/Untitled-2024-02-08-1201%2817%29.png" data-image-dimensions="1033x626" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1b470c01-e76e-4aae-8c16-162a9d7c060d/Untitled-2024-02-08-1201%2817%29.png?format=1000w" width="1033" height="626" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1b470c01-e76e-4aae-8c16-162a9d7c060d/Untitled-2024-02-08-1201%2817%29.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1b470c01-e76e-4aae-8c16-162a9d7c060d/Untitled-2024-02-08-1201%2817%29.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1b470c01-e76e-4aae-8c16-162a9d7c060d/Untitled-2024-02-08-1201%2817%29.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1b470c01-e76e-4aae-8c16-162a9d7c060d/Untitled-2024-02-08-1201%2817%29.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1b470c01-e76e-4aae-8c16-162a9d7c060d/Untitled-2024-02-08-1201%2817%29.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1b470c01-e76e-4aae-8c16-162a9d7c060d/Untitled-2024-02-08-1201%2817%29.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1b470c01-e76e-4aae-8c16-162a9d7c060d/Untitled-2024-02-08-1201%2817%29.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">After</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Put another way, value graphs have <strong>lots of seams.</strong> We can cut them up any way we like, simplify nodes, apply general transformations, and the graph stays valid. There are no ‘edge cases’. The same cannot be said for execution wires. </p><p class="">So, Lattice borrows from this lineage of programming languages. That get us an incredibly composable computation model, however we need a few super-powers to give us a more expressive language as a whole. I'll chat more about that next time.</p><p class="">Until then.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1713652321606-7J2V17Z45O6HTA1GE2N8/Untitled-2024-02-08-1201%2819%29.png?format=1500w" medium="image" isDefault="true" width="1500" height="970"><media:title type="plain">Composability: Designing a Visual Programming Language</media:title></media:content></item><item><title>Lattice now compiles to .NET IL</title><category>Technical</category><dc:creator>John Austin</dc:creator><pubDate>Sat, 30 Mar 2024 23:38:53 +0000</pubDate><link>https://johnaustin.io/articles/2024/lattice-now-compiles-to-net-il</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:6608935a8c7eed6e4e74048a</guid><description><![CDATA[<p class=""><em>Lattice is a high-performance visual scripting system targeting Unity ECS. Read more </em><a href="https://forum.unity.com/threads/lattice-visual-scripting-for-ecs.1508402/"><em>here</em></a><em>.</em></p><p class="">I’ve tried several times to write blog posts about Lattice, and each time I’ve gotten lost in the weeds. It’s hard to pick a point to start. So instead, I’ve resolved to just start writing — quantity over quality, as they say.</p><p class="">Lattice has met a major milestone this week: programs now fully compile to .NET IL! If you’ve never written a programming language before, this may not mean much to you, but there’s something beautiful about seeing a relatively complex program compile down into flat assembly instructions.</p><p class="">I’m very proud of this work. One of the goals of the Lattice project was to design a visual language that could compile down into fast linear code — effectively just the procedural code that you would write if you were coding it by hand. This is very much inspired by Rust’s tradition of “zero-cost abstractions”. With the new compilation pipeline, the node-graph representation is stripped away entirely. Outputs of nodes become local variables and bodies of nodes become plain static methods.</p><p class=""><br>I’ve talked in the past about a plan for Lattice to generate C#, but IL was a much better option for a few reasons:</p><ul data-rte-list="default"><li><p class="">Generating C# requires a Domain Reload to compile which is a non-starter if you're editing scripts while the game runs.</p></li><li><p class="">Generating C# would require shipping a C# compiler at runtime if you want to modify scripts during standalone play.</p></li><li><p class="">C# is a sloppy thing to generate. There are a lot of syntactical concerns you get bogged down in just trying to get something valid.</p></li></ul><p class="">IL, as it turns out, is trivially easy to emit in-process with Reflection.Emit, and is actually really simple to work with. I use the Sigil library which is a validating wrapper which catches a number of type errors, etc, during generation. Plus, IL can do several things that C# can't like calling private methods, etc, which just makes the whole process smoother.</p><p class="">This is possible because of the IR representation I added to the compiler a month or two back. For example, the following graph is represented under the hood as a larger graph of simple primitives:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6d6e842d-0a3f-4dac-ac54-2acaa05c2d88/Screen_Shot_2024-03-29_at_10.10.42_PM.png" data-image-dimensions="1686x1162" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6d6e842d-0a3f-4dac-ac54-2acaa05c2d88/Screen_Shot_2024-03-29_at_10.10.42_PM.png?format=1000w" width="1686" height="1162" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6d6e842d-0a3f-4dac-ac54-2acaa05c2d88/Screen_Shot_2024-03-29_at_10.10.42_PM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6d6e842d-0a3f-4dac-ac54-2acaa05c2d88/Screen_Shot_2024-03-29_at_10.10.42_PM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6d6e842d-0a3f-4dac-ac54-2acaa05c2d88/Screen_Shot_2024-03-29_at_10.10.42_PM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6d6e842d-0a3f-4dac-ac54-2acaa05c2d88/Screen_Shot_2024-03-29_at_10.10.42_PM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6d6e842d-0a3f-4dac-ac54-2acaa05c2d88/Screen_Shot_2024-03-29_at_10.10.42_PM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6d6e842d-0a3f-4dac-ac54-2acaa05c2d88/Screen_Shot_2024-03-29_at_10.10.42_PM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/6d6e842d-0a3f-4dac-ac54-2acaa05c2d88/Screen_Shot_2024-03-29_at_10.10.42_PM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">The view of the authoring experience in Lattice. This is my Integration Test graph.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/fcb4a95f-33a7-40bc-931f-2b87d01dd9e1/Screen_Shot_2024-03-29_at_10.14.22_PM.png" data-image-dimensions="1668x1378" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/fcb4a95f-33a7-40bc-931f-2b87d01dd9e1/Screen_Shot_2024-03-29_at_10.14.22_PM.png?format=1000w" width="1668" height="1378" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/fcb4a95f-33a7-40bc-931f-2b87d01dd9e1/Screen_Shot_2024-03-29_at_10.14.22_PM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/fcb4a95f-33a7-40bc-931f-2b87d01dd9e1/Screen_Shot_2024-03-29_at_10.14.22_PM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/fcb4a95f-33a7-40bc-931f-2b87d01dd9e1/Screen_Shot_2024-03-29_at_10.14.22_PM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/fcb4a95f-33a7-40bc-931f-2b87d01dd9e1/Screen_Shot_2024-03-29_at_10.14.22_PM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/fcb4a95f-33a7-40bc-931f-2b87d01dd9e1/Screen_Shot_2024-03-29_at_10.14.22_PM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/fcb4a95f-33a7-40bc-931f-2b87d01dd9e1/Screen_Shot_2024-03-29_at_10.14.22_PM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/fcb4a95f-33a7-40bc-931f-2b87d01dd9e1/Screen_Shot_2024-03-29_at_10.14.22_PM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">The IR representation of the graph. A single authored node becomes several IR nodes.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">This IR is critical because it reduces the complexity of the next step of the compilation: code generation. While there may be hundreds of nodes available for user scripts, in the IR there are only 7 distinct types of operators:</p><ul data-rte-list="default"><li><p class="">Function (a handle to a static C# method)</p></li><li><p class="">Previous (allows referencing a node value from the previous frame)</p></li><li><p class="">Entity (returns a handle to the current Entity)</p></li><li><p class="">QualifierTransform (allows referencing other entities dynamically)</p></li><li><p class="">Barrier (execution barrier, waits for all inputs to finish)</p></li><li><p class="">Collect (collects several values into an array)</p></li><li><p class="">Malformed (a stub node that returns an error. Used for syntax errors)</p></li></ul><p class="">The IL Generation step only needs to implement generation for these 7 operators, dramatically reducing the complexity. In fact, the IL Generator is only ~500 lines of code. The final code looks something like:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/16b176c7-94e4-428e-89fa-357198b11c9c/Screen_Shot_2024-03-29_at_10.10.00_PM.png" data-image-dimensions="1870x1594" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/16b176c7-94e4-428e-89fa-357198b11c9c/Screen_Shot_2024-03-29_at_10.10.00_PM.png?format=1000w" width="1870" height="1594" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/16b176c7-94e4-428e-89fa-357198b11c9c/Screen_Shot_2024-03-29_at_10.10.00_PM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/16b176c7-94e4-428e-89fa-357198b11c9c/Screen_Shot_2024-03-29_at_10.10.00_PM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/16b176c7-94e4-428e-89fa-357198b11c9c/Screen_Shot_2024-03-29_at_10.10.00_PM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/16b176c7-94e4-428e-89fa-357198b11c9c/Screen_Shot_2024-03-29_at_10.10.00_PM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/16b176c7-94e4-428e-89fa-357198b11c9c/Screen_Shot_2024-03-29_at_10.10.00_PM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/16b176c7-94e4-428e-89fa-357198b11c9c/Screen_Shot_2024-03-29_at_10.10.00_PM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/16b176c7-94e4-428e-89fa-357198b11c9c/Screen_Shot_2024-03-29_at_10.10.00_PM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">With the new backend, Lattice is now quite fast — as fast as C#! I still need to do comparative profiling, but I’m fairly confident Lattice is by far the fastest visual scripting system in Unity by a long shot. Bolt, NodeCanvas, and Playmaker all interpret their node graphs. Lattice emits a single static method that executes all lattice scripts in the game in a single pass. The .NET and Mono JITs eat pure static methods for breakfast. </p><p class="">This work also enables some interesting next steps for the compiler:</p><ul data-rte-list="default"><li><p class="">Automatic parallelization & jobification (Unity Job system)</p></li><li><p class="">Burst compilation of Lattice Graphs</p></li></ul><p class="">However, Lattice is now plenty fast for my needs. So for the time being, I’m pivoting back to working on gameplay workflows and UX. Stay tuned for more updates.</p>]]></description></item><item><title>Why I Like Rust: Documentation</title><dc:creator>John Austin</dc:creator><pubDate>Mon, 19 Feb 2024 20:57:08 +0000</pubDate><link>https://johnaustin.io/articles/2024/qnrfslcq0zakur955ntxabmi8cps8o</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:65d3b91e624f260b58fd08ca</guid><description><![CDATA[<p class="">The other day, I was helping a friend learn Python. They were writing a simple HTTP request to an API, and at one point they asked me "what does <code>requests.get(...).json()</code> return? A string or a parsed JSON object?”</p><p class="">I thought this would be a good "teach a man to fish" sort of lesson: look it up the docs. <a href="https://requests.readthedocs.io/en/latest/">requests</a> is a massively popular library that forms the bedrock of many Python scripts. Here's what the documentation has to say:</p><blockquote><p class="">json(**kwargs)<br>Returns the json-encoded content of a response, if any. <br>Parameters:**kwargs – Optional arguments that json.loads takes.</p></blockquote><p class="">Brutal. It barely answers the question. The description says it "returns a json-encoded value". It wouldn't make sense to call a python object "encoded", so is it a JSON encoded string.</p><p class="">Well, no. Looking at the parameter, <code>json.loads()</code> is called internally. That would only make sense if <code>json()</code> returned a deserialized value. This is, in fact, the correct answer, conveyed through pure luck in an optional parameter. </p><p class="">And what happens if the body is not JSON? Does it return None? Does it throw? What kind of exception should I catch?</p><p class="">In truth, the Pythonic answer to this question is "try it for yourself". Boot up a python interactive console, pip install requests, dig up a fake API to hit, and see what it returns. Hope and pray that you don't hit some snag along the way.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <p class="">Let's see how Rust handles this. </p><p class=""><a href="https://docs.rs/reqwest/latest/reqwest/blocking/struct.Response.html#method.json">https://docs.rs/reqwest/latest/reqwest/blocking/struct.Response.html#method.json</a></p><p class="">We've got:</p><ul data-rte-list="default"><li><p class="">A clear answer: it returns the deserialized object. <a href="#margin" target="_blank">Specifically from the content body. </a></p></li><li><p class="">A short example. In case you need a little context. </p></li><li><p class="">Gotchas: you'll need to enable the JSON feature.<a href="#margin" target="_blank">Rust libraries generally only make you pay for what you use. If you don't need JSON support, it won't even download the dependency.</a></p></li><li><p class="">Failure cases: If it's not JSON, the Result will return a specific error.</p></li></ul><p class="">Further, the broader docs experience is phenomenal:</p><ul data-rte-list="default"><li><p class="">A single global website (<a href="https://docs.rs" target="_blank">docs.rs</a>) where all Rust docs are hosted. </p></li><li><p class="">Real, symbol-based doc search.</p></li><li><p class="">Source files. I can browse the exact version of the code that I'm exploring.</p></li><li><p class="">Inter-library links for symbols! Even across crates, with versioning.</p></li></ul><p class="">This isn't an exception, it's the rule. Here are a random sampling of libraries I recently pulled for a project: [<a href="https://docs.rs/hyper/latest/hyper/" target="">hyper</a>, <a href="https://docs.rs/petgraph/latest/petgraph/">petgraph</a>, <a href="https://docs.rs/base64/0.21.7/base64/">base64</a>]. And just <a href="https://doc.rust-lang.org/std/collections/struct.HashMap.html" target="_blank">look</a> at the <a href="https://doc.rust-lang.org/std/sync/struct.Mutex.html" target="_blank">standard</a> <a href="https://doc.rust-lang.org/std/primitive.char.html" target="_blank">library</a>! Beautiful. Rust has somehow managed to cultivate an ecosystem where this quality of documentation is the standard. </p><p class="">I can't begin to tell you how pleasant this makes developing in Rust. When people ask me why I enjoy the language, sometimes it's hard to convey. It's the little things. Discovering new libraries is <strong>fun</strong>. It's a new box of toys you can pick through, not a slog of trying to dig up arcane, undocumented knowledge. </p><p class="">People assume the "Rewrite it in Rust" is popular because of memory safety, but I have this sneaking feeling that an even larger part is because developing in Rust is just so dang lovely. Of course I want all my libraries to be this level of quality. Who wouldn't?</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <p class="">Nothing Rust is doing here is particularly novel, nor technically complex. Heavily inspired by Go<a href="#margin" target="_blank">See gofmt and godoc.</a>, Rust has a focus on strong, unified tooling. Any Rust library can trivially generate docs with a few config settings. Slap <code>#![warn(missing_docs)]</code> and clippy will automatically warn on public symbols missing docs. </p><p class="">But there's also something bigger at work here. Rust sets the bar high. When I'm surrounded by incredible documentation, it feels shoddy to release a library with anything less. It's not a 'gun to the head' kind of threat. It's more like walking into a nice home and feeling like you want to take off your shoes. Everyone here takes so much pride in their work, that it feels good to maintain that. Quality breeds more quality.</p>]]></description></item><item><title>A Gameplay Programming Puzzle for ECS</title><dc:creator>John Austin</dc:creator><pubDate>Sat, 23 Sep 2023 01:16:32 +0000</pubDate><link>https://johnaustin.io/articles/2023/a-gameplay-programming-brain-teaser-for-ecs</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:650e3733ce36643e83e70df0</guid><description><![CDATA[<p class="">Entity Components Systems are hot stuff right now in the world of game engine design. Most of Unity’s <a href="https://unity.com/ecs">new engine workflows</a> is based on ECS, and there are other junior up-starts like the open-source <a href="https://bevyengine.org">Bevy Engine</a>. They’re extremely fast, and well suited to today’s machines with many cores. But they can be a real pain sometimes when implementing gameplay code.</p><p class="">Every now and then, I stumble on a gameplay pattern that’s particularly challenging to implement within an ECS architecture. When that happens, I like to try to simplify the problem – distill it down – and save a draft for posterity. These simplified puzzles are fun because they’re tractable to think about, but they still derive from real-world gameplay needs!</p><p class="">It’s great practice for breaking the bonds of OOP and thinking of the world in a data driven way, and they also provide a sort of ‘design benchmark’ for any promising ECS coming onto the scene. If you can’t implement these patterns with your ECS, it’s pretty likely you’ll run into problems as your games get bigger and more complex!</p><p class="">Let’s dig into one I hit recently, which I’m calling the <strong>Pet Adoption</strong>:</p><h2><strong>Pet Adoption</strong></h2><p class="">Here are the rules of the (extremely simple) game. </p><ul data-rte-list="default"><li><p class="">There are many Humans and many Pets.</p></li><li><p class="">Each human adopts the first pet that likes them.</p></li><li><p class="">Pets may arbitrarily decide if they will let a given human adopt them.</p></li><li><p class="">Each pet can only be adopted once, and each human only adopts one pet.</p></li></ul><p class="">Gameplay goes as follows:</p><ul data-rte-list="default"><li><p class="">Each human goes from pet to pet, asking them if they’d like to be adopted.</p></li><li><p class="">They leave with the first pet that says yes.</p></li><li><p class="">We run our adoption game every frame.</p></li></ul><p class="">—</p><p class="">There’s no concept of ranking here: pets will take the first human that suits them! Similarly, humans will take any pet that allows it!</p><p class="">The thing that makes this tricky is that you may have 50 different types of pets, all with different logic for determining which humans they like. For example, when asked to be adopted, they might:</p><ul data-rte-list="default"><li><p class="">Read from the human state (that person is too ugly!)</p></li><li><p class="">Read from their own state (I’m scared and don’t like anyone!)</p></li><li><p class="">Read from global state (It’s cold today, so I’ll take anyone!)</p></li></ul><p class="">They could even <em>modify</em> their own state based on the previous humans. Maybe the first few humans are so unpleasant that a pet decides to say no to everyone.</p><p class="">This problem is fundamentally one of <strong>indirection</strong>, usually solved by polymorphism. With OOP, this is trivial: add a function to the Pet base class <code>bool CanBeAdopted(Human adopter)</code> and call it as humans are inspecting different pets.<a href="#margin">It’s no wonder UI libraries tend to heavily make use of OOP.</a> ECS generally bans this entirely. Components should be plain, blittable data. Systems should contain your logic.</p><p class="">Instead, other types of decoupling are suggested, such as event systems. However, those don’t work here. Event systems are one-directional: you fire off an event and expect another system to handle it. This gameplay requires bi-directional coupling: a human needs to know a pet’s response first, so it can move on to the next pet in line.<a href="#margin">Tag Components are another way of handling indirection. For example, adding a “Collision” component to any objects that collide with something. However, that’s also one-directional and fails for the same reasons.</a></p><p class="">The human can’t wait for this information – it needs to know now! This is the distinction between <strong>events</strong> which send information, and <strong>callbacks</strong> which send information and retrieve results.</p><p class="">So it’s tricky. ECS asks you to queue up big chunky systems and pipeline everything, but this logic needs to reflect on objects in the moment, while a human is traversing the list of pets. </p><p class="">So, how would you architect this?</p><p data-rte-preserve-empty="true" class=""></p><p class="">(Discuss on <a href="https://www.reddit.com/r/programming/comments/16prxvm/a_gameplay_programming_puzzle_for_ecs/">/r/programming</a>)</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <p class=""><strong>Real World Context</strong></p><p class="">If you’re curious how this maps to the real world, here are two examples.</p><p class=""><strong>Object Interaction:</strong> Characters (Players or NPCs) can interact with objects in the game, but the object chooses whether it will allow a specific character to interact with it, depending on arbitrary factors (time of day, weather, character stats, etc). Certain chests may only open for players, and not NPCs. Or only if the player has <500 gold in their inventory.</p><p class="">Essentially, each object has a ‘shot’ at the interaction event, but can choose to reject it, and the character will move the next valid thing in front of it. This continuation is important, otherwise when an object rejects the interaction event, it could swallow the interaction for a perfectly valid object sitting on top of it.</p><p class="">Characters are humans, Pets are objects, and adoption is whether the interactable allows a certain character to interact with it.</p><p class=""><strong>User Interface:</strong> UI systems tend to have big trees of elements. We want to be able to propagate events down the tree, allow elements to handle them in arbitrary ways, and also return whether an element has “consumed” an event. Event handlers are bidirectional: they affect the propagation of the event itself.</p><p class="">The UI systems are humans, layout elements are pets, and the consumption return value of the event handler is a pet deciding whether to be adopted.</p><p class="">This pattern tends to occur in any system that has single-ownership. Ie. Where several different entities in the game need to agree on who is assigned which other objects. </p><p class=""><br><br></p>]]></description></item><item><title>How to Actually Move Gamedev Off Twitter</title><dc:creator>John Austin</dc:creator><pubDate>Sat, 08 Apr 2023 08:18:42 +0000</pubDate><link>https://johnaustin.io/articles/2023/how-we-can-actually-move-gamedev-off-twitter</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:64291e2da71828296f7fff5d</guid><description><![CDATA[<figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/24890d3e-9dab-4841-9557-4c91cf79a26a/chrome_hkic8TWCwB.png" data-image-dimensions="1666x935" data-image-focal-point="0.4790181650362688,0.23201933022374" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/24890d3e-9dab-4841-9557-4c91cf79a26a/chrome_hkic8TWCwB.png?format=1000w" width="1666" height="935" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/24890d3e-9dab-4841-9557-4c91cf79a26a/chrome_hkic8TWCwB.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/24890d3e-9dab-4841-9557-4c91cf79a26a/chrome_hkic8TWCwB.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/24890d3e-9dab-4841-9557-4c91cf79a26a/chrome_hkic8TWCwB.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/24890d3e-9dab-4841-9557-4c91cf79a26a/chrome_hkic8TWCwB.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/24890d3e-9dab-4841-9557-4c91cf79a26a/chrome_hkic8TWCwB.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/24890d3e-9dab-4841-9557-4c91cf79a26a/chrome_hkic8TWCwB.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/24890d3e-9dab-4841-9557-4c91cf79a26a/chrome_hkic8TWCwB.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Twitter is going downhill fast. First they banned Mastodon promotion (and then reversed it), and now it’s Substack in it’s entirety. We all want to move off, and yet.. here we are. I’ve been doing some thinking and I see two fairly obvious problems that have been keeping everyone on Twitter: </p><ol data-rte-list="default"><li><p class="">The <strong>Chicken/Egg</strong>: Nobody wants to leave Twitter for a place that has no community, but there can’t be another gamedev community unless we all leave Twitter! It might work if we all leave at once, but that’s proven difficult. We have to break this cycle.</p></li><li><p class="">The <strong>Algorithm: </strong>I think the algorithmic feed is more important than we give it credit. Seeing what your direct followers post is nice, but it doesn’t <strong>really</strong> feel like being a part of the broader community. For better or worse, people are on Twitter to hear about current events, news, and other.. well.. gossip. Any alternative needs to provide that.</p></li></ol><p class="">But we are game designers! If nothing else we’re great at working around gnarly systemic problems! </p><p class="">So! I want to present two concrete actions you can personally take to begin to fix these problems.</p><h2>Action 1. Mirror your Twitter posts to Mastodon with <a href="https://moa.party.">moa.party.</a></h2><p class="">We can’t leave Twitter because all the good content is still on Twitter. It’s classic FOMO. Theoretically, we could <strong>all</strong> leave Twitter at the same time, but I think we should accept the fact that’s not going to happen. It’s kind of like the Prisoner’s Dilemma — nobody wants to make the first move and be left out.<a href="#margin">Okay… technically it’s a <a href=https://en.wikipedia.org/wiki/Stag_hunt>Stag Hunt</a>. Which is the <em>opposite</em> of the Prisoner’s Dilemma. Sorry Mr. Nash, I’ll turn in my game designer card now.</a></p><p class="">But, if everyone mirrors their tweets, it could be possible to move between the two freely without missing out. All the discourse, all the hot takes will be on Mastodon, too, just with a different UI!</p><p class="">This won’t fix the problem overnight, but it does remove Twitter’s main draw. With enough people mirroring, it wouldn’t matter much which platform you’re on. This means that <strong>people can leave Twitter at their own pace</strong>, and we’ll never need to have a single large exodus. </p><p class="">This breaks the chicken/egg problem, and even you stay on Twitter.. <strong>worst case you’re getting more engagement for your Tweets? </strong>There’s kind of no argument against mirroring.<a href="#margin">Instead… eventually we’ll have two chickens… which are clones?</a></p><p class="">Luckily<strong> it’s trivial to setup using </strong><a href="https://moa.party"><strong>moa.party</strong></a><strong>.</strong> It takes literal seconds! Then you can leave it running and forget about it. </p><p class="">Every single person who mirrors their tweets is taking a concrete step to erode the wall Twitter has around it. Destroy FOMO! Mirror your tweets!</p><h2>Action 2. Install <a href="https://elk.zone">Elk.zone</a>, a Mastodon interface that’s actually good.</h2><p class=""><a href="https://elk.zone">Elk.zone</a> is by far the best UI for using Mastodon. It works on all platforms, and they basically copied the Twitter UI, so you don’t have to learn anything new. I know: the normal Mastodon UI is nice in it’s own way, but in the interest of getting more people onto the platform, I think it’s better to focus on something expected. We’re trying to make it easy to switch! We can dazzle folks with features later.<a href="#margin">You can access it with the #Explore tab in the menu.</a></p><p class=""><strong>Importantly, it also has an algorithmic feed!</strong> As much as a raw timeline is nice, Twitter gamedev thrives because you can keep a pulse on what’s happening in the broader community. In my opinion, without some sort of algorithmic ranking, we’ll never be able to recreate the value of Twitter.</p><p class="">Elk’s algorithmic feed isn’t perfect, but it’s the first time I’ve felt connected with the community when using Mastodon. And because it’s open-source, we can improve it ourselves.</p><p class="">Elk is a web-app, but it <a href="https://www.wikihow.com/Install-Web-Apps-on-iPhone-or-iPad">can be installed</a> into Android and iPhones like any other normal app. It’s well built and feels like a native app. Consider installing it to your phone’s home screen, and try to build it into your Twitter habit!</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c117f42d-d85c-44f8-a891-0c8aebc9210c/chrome_8ew13OczUv.png" data-image-dimensions="1244x845" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c117f42d-d85c-44f8-a891-0c8aebc9210c/chrome_8ew13OczUv.png?format=1000w" width="1244" height="845" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c117f42d-d85c-44f8-a891-0c8aebc9210c/chrome_8ew13OczUv.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c117f42d-d85c-44f8-a891-0c8aebc9210c/chrome_8ew13OczUv.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c117f42d-d85c-44f8-a891-0c8aebc9210c/chrome_8ew13OczUv.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c117f42d-d85c-44f8-a891-0c8aebc9210c/chrome_8ew13OczUv.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c117f42d-d85c-44f8-a891-0c8aebc9210c/chrome_8ew13OczUv.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c117f42d-d85c-44f8-a891-0c8aebc9210c/chrome_8ew13OczUv.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/c117f42d-d85c-44f8-a891-0c8aebc9210c/chrome_8ew13OczUv.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">An example of Elk’s algorithmic “#Explore” tab. I don’t follow any of these people!</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <h2>Will This Work?</h2><p class="">Neither of these suggestions is a silver bullet, but I think there’s good reasons to believe that we can actually make a difference. I’m sure there are other small things we can do, too. So let’s put our brains together and figure this out!</p><p class="">It might take longer than we hope, but: baby steps.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <p class=""><span>Footnote A: Why Mastodon?</span></p><p class="">I should probably write a longer article on why I think Mastodon is the best option. For one, Mastodon is where most people have moved to — it seems to have won the battle. But, to be pragmatic, we just need to pick something, and Mastodon is good!</p><p class="">It’s literally Twitter, except it’s open-source, developed by a non-profit, and it operates like email<a href="#margin">No single company controls the application. It’s shared communication, like email. Importantly, Mastodon has the <strong>least</strong> likely chance of becoming another Twitter in 10 years. It’s late-stage capitalism resistant.</a>. Most people are on <a href="https://mastodon.gamedev.place">gamedev.place</a> or <a href="https://peoplemaking.games">peoplemaking.games</a>, and both are owned by upstanding community members. It doesn’t matter which one you choose: they all go to the same place. It’s like choosing a Gmail or Yahoo email address.</p><p class=""><span>Footnote B: The “Fediverse”</span></p><p class="">Since I have your attention: can we stop calling it the “Fediverse”? Nobody knows what that is, and it just gives off the vibes of “Metaverse” or “Federated Blockchain”. Just call it Mastodon! It’s a cute elephant website! People understand that kind of thing.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1680943000856-MKGG643PLN03A9AE4RJF/chrome_hkic8TWCwB.png?format=1500w" medium="image" isDefault="true" width="1500" height="842"><media:title type="plain">How to Actually Move Gamedev Off Twitter</media:title></media:content></item><item><title>Writing a Fast C# Code-Search Tool in Rust</title><dc:creator>John Austin</dc:creator><pubDate>Sat, 26 Nov 2022 23:58:54 +0000</pubDate><link>https://johnaustin.io/articles/2022/blazing-fast-structural-search-for-c-sharp-in-rust</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:6381a6f3287ff4075fdb8dbf</guid><description><![CDATA[<p class="">When I began learning Rust, I had several expectations. I figured that I would be a little less productive <em>writing</em> code, in exchange for memory safety and C-like performance. </p><p class="">What I did <strong>not</strong> expect, was that I would actually be <strong>more</strong> productive writing code. So much has that been the case, that for all of the one-off scripts I used to write in Python, I now write in Rust. And it’s not out of casual fancy, but because it <em>legitimately</em> keeps turning out to be the best tool for the job. </p><p class="">I know. These are wild times.<a href="#margin">I’ll have to write up another post at some point exploring the reasons for this. For this one we’re just going to look at one case. </a></p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <p class="">As a recent example of this development, I was working on a Unity C# codebase where I had a few questions about the code. Namely, I wanted to find “all classes with a private field, that extend from interface X”. Normally, I would reach for Resharper’s <a href="https://www.jetbrains.com/help/resharper/Navigation_and_Search__Structural_Search_and_Replace.html">Structural Search</a>, but I just moved over to Jetbrains Rider, and sadly… structural search is in the <a href="https://youtrack.jetbrains.com/issue/RIDER-11489">backlog</a>. </p><p class="">I’d heard good things about <a href="https://tree-sitter.github.io/tree-sitter/">Tree-sitter</a>, though, and it was my day off, so I figured I’d take a crack at writing a CLI tool. Several hours and 130~ lines of code later, I had a tiny little script that could query 6000 files in under a second!<a href="#margin">As it happens, I wrote a similar tool in C# using Microsoft’s Roslyn library, and a similar query took nearly 30 seconds. Rust is fast! <br/> <br/>To be fair, the Roslyn version was not multi-threaded. Although, the ease of multi-threading in Rust is the primary reason the Rust version is threaded and the C# one is not.</a></p><p class="">So let’s see how that came together. Read on, or <a href="https://github.com/Kleptine/csharp-semantic-search">skip straight to the code</a>. </p><h2>Part 0: Setup</h2><p class="">Let’s start with the hard bit. Tree-sitter is a general AST parsing library that supports a huge number of languages. It works by generating a C library for each language it parses. For simplicity, we can pull the pre-generated <a href="https://github.com/tree-sitter/tree-sitter-c-sharp">C# parser</a> in as a submodule to our project and add it to the build. In order to build this C library as part of our Rust executable, we’ll need to add a few lines to a <code>build.rs</code> file: <a href="#margin"><code>build.rs</code> is a file that executes as a part of the Cargo build system. You can use it to compile C code, or define other more complicated modifications to the way your library is built. </a></p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-rust"><code>use std::path::PathBuf;
+
+fn main() {
+ let dir: PathBuf = ["tree-sitter-c-sharp", "src"].iter().collect();
+
+ cc::Build::new()
+ .include(&dir)
+ .file(dir.join("parser.c"))
+ .file(dir.join("scanner.c"))
+ .compile("tree-sitter-c-sharp");
+}</code></pre>
+
+
+ <p class="">This grabs the headers and source files from <code>tree-sitter-c-sharp</code> and compiles them as a pre-build step. </p><h2>Part 1: Tree-sitter and Rust Bindings</h2><p class="">Tree-sitter provides Rust bindings, which we can use to load this parser library in as an FFI. Because C is unsafe by Rust standards, we’ll need to mark it as unsafe.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-rust"><code>extern "C" {
+ fn tree_sitter_c_sharp() -> Language;
+}
+
+fn main() {
+ // Invoke the top-level C function from the Tree-sitter C# library.
+ // Unsafe, because the C FFI is unsafe.
+ let language = unsafe { tree_sitter_c_sharp() };
+
+ // Create a tree-sitter parser.
+ let mut parser = tree_sitter::Parser::new();
+ parser.set_language(language).unwrap();
+
+ // Parse the file.
+ let source_code = fs::read_to_string("my_csharp_file.cs")?;
+ let tree = parser.parse(&source_code, None).unwrap();
+
+ // ...
+}
+</code>
+</pre>
+
+
+ <p class="">Hey! We parsed a C# file! Easy.</p><p class="">Now we need some way to define our query. Tree-sitter actually provides a <a href="https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries">query language</a> built-in! It’s based on <a href="https://en.wikipedia.org/wiki/S-expression">S-Expressions</a>, and looks a bit like this: <code>(binary_expression (number_literal) (number_literal))</code>. That query would find all pieces of code that are a binary operator over two numbers. ie. <code>2 + 2</code>, <code>7 << 2</code>, etc.</p><p class="">My original question: “all of the classes with a private field, that extend from interface IComponentData” would be encoded as:</p><p class=""><code>(class_declaration bases: (base_list (identifier) @parent) body: (declaration_list (field_declaration (modifier) @modifier) @field (#eq? @modifier ""private"")) (#eq? @parent ""IComponentData""))</code></p><p class="">The <code>@name</code> bits tell Tree-sitter to <strong>capture</strong> the given AST node into a named variable. These can be sent through additional filtering, as in <code>(#eq? @modifier “private”)</code> , or they can simply be read out in our Rust script, the same way you’d read captures out of a regular expression.</p><p class="">For ergonomics, we’ll accept a query as an argument on the command line. We’ll then build a tree-sitter query object, and match it against the AST we just parsed from the file:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-rust"><code>#[derive(Parser, Debug)]
+#[command(author, version, about)]
+struct Args {
+ path: PathBuf, // Directory to search.
+ pattern: String, // Query string (s-exp)
+}
+
+fn parse_file(/*..*/) {
+ // ..
+
+ let args = Args::parse();
+ let full_pattern = format!("{} @full_pattern_cli_capture", args.pattern); // Add an extra root pattern to force capturing the root pattern for display.
+
+ // The final query built from the user's string.
+ let query =
+ Query::new(language, &full_pattern).expect("Error building query from given string.");
+
+ // Iterate the nodes in the AST that match the given query:
+ let source_bytes = &*source_code.as_bytes();
+ let root_node = tree.root_node();
+ let mut cursor = QueryCursor::new();
+ for m in cursor.matches(query, root_node, source_bytes) {
+ // Captures is a hashmap that stores the value of each capture in the query string.
+ let captures: HashMap<_, _> = m
+ .captures
+ .iter()
+ .map(|c: &QueryCapture| (query.capture_names()[c.index as usize].clone(), c))
+ .collect();
+
+ // Here are our matches!
+ // Do anything we want here. Print the results, filter them more, etc.
+ }
+}
+</code></pre>
+
+
+ <p class="">That’s all we need for code search. The variable <code>captures</code> now contains the captured nodes of the AST tree, specified in the query above. We can use this to print out results, or do whatever we like. </p><p class="">I added one extra trick here. By default tree-sitter doesn’t provide access to the root node of a query, so I’ve automatically appended a capture variable called <code>@full_pattern_cli_capture</code> to the user’s pattern to capture the root node. <a href="#margin">The reason Tree-sitter doesn’t expose access to the root node is because some patterns may have <a href=”https://github.com/tree-sitter/tree-sitter/discussions/898”>multiple</a> root nodes! Luckily, this is a quick script, so we don’t have to worry about those patterns.</a></p><h2>Part 2: Directory Traversal</h2><p class="">We’ll use <a href="https://crates.io/crates/walkdir">walkdir</a> library for traversal. It’s not in the standard library, but it might as well be, because it’s reliable and quite performant. </p><p class="">Notice how Rust explicitly marks potential edge-cases with <code>unwrap()</code> and <code>expect()</code> . I’ve chosen just to bubble them up for this script, but it’s nice to know which errors <em>could</em> occur and where. <a href="#margin">In this case, walkdir could encounter an I/O error (disk inaccessible, can’t be read, etc), encounter an infinite loop of symbolic links, or the filename could be invalid utf-8 (and would need more complex handling for file type). <br/>This is laid out nicely in the <a href=https://doc.servo.org/walkdir/struct.Error.html>walkdir doc page</a>. </a></p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-rust"><code>
+ // Scan the entire directory *first*, so that we can more easily split the work among worker threads later.
+ let mut paths = Vec::new();
+ for entry in WalkDir::new(args.path).into_iter() {
+ let path = entry.unwrap().into_path();
+ let path_str = path.to_str().expect("filename was not valid utf-8");
+ if path_str.ends_with(".cs") {
+ paths.push(path);
+ }
+ }
+
+ println!("Searching {} files.", paths.len());</code></pre>
+
+
+ <p class="">We feed these paths into the above parsing code, printing any files that have parse errors. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-rust"><code> for path in paths {
+ if let Err(e) = parse_file(&path, &query, &output_text) {
+ println!("Skipping [{}] [{}]", path.display(), e);
+ }
+ }
+</code></pre>
+
+
+ <p class="">With these two pieces, we have a fully functional query tool! Let’s try it out. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-powershell"><code>
+PS C:\projects\tree-search> cargo run --release -- D:\Projects\EcsEngine "(class_declaration name: (identifier) @classname bases: (base_list (identifier) @parent) body: (declaration_list (field_declaration (modifier) @modifier) @field (#eq? @modifier ""private"")) (#eq? @parent ""IComponentData""))"
+ Compiling tree-search v0.1.0 (C:\projects\tree-search)
+ Finished release [optimized] target(s) in 1.16s
+ Running `target\release\tree-search.exe D:\Projects\EcsEngine "(class_declaration name: (identifier) @classname bases: (base_list (identifier) @parent) body: (declaration_list (field_declaration (modifier) @modifier) @field (#eq? @modifier private)) (#eq? @parent IComponentData))"`
+Searching 6441 files.
+
+ManagedComponent [D:\Projects\EcsEngine\Assets\SnapshottingSystem.cs:at byte 5935]
+TestEmbedInterface [D:\Projects\EcsEngine\Library\PackageCache\com.unity.entities@1.0.0-exp.8\Unity.Entities.Tests\TypeManagerTests.cs:at byte 46500]
+TestTestEmbedBaseClass [D:\Projects\EcsEngine\Library\PackageCache\com.unity.entities@1.0.0-exp.8\Unity.Entities.Tests\TypeManagerTests.cs:at byte 46621]
+==> Skipping [D:\Projects\EcsEngine\Library\PackageCache\com.unity.platforms@1.0.0-exp.6\Tests\Editor\Unity.Build.Tests\ResultBaseTests.cs] [stream did not contain valid UTF-8]
+PostLoadCommandBuffer [D:\Projects\EcsEngine\Library\PackageCache\com.unity.entities@1.0.0-exp.8\Unity.Entities\Types\SceneTagComponent.cs:at byte 6757]
+
+Found 4 total results.
+PS C:\projects\tree-search>
+</code></pre>
+
+
+ <p class="">Looks like there are four results, and one of my files contains non-UTF8 text! I’ll have to look into that. </p><p class="">The nice thing about this tool over IDE code search is that it’s stateless. Code doesn’t need to be indexed first by an IDE before searching. This is particularly useful in Unity, because not all code in a Unity project is exposed into the .NET solution.</p><h2>Bonus: Parallelization and Performance</h2><p class="">Single-threaded isn’t fun, so let’s parallelize this! The <a href="https://github.com/rayon-rs/rayon">rayon</a> library provides a <code>par_iter()</code> iterator extension that automatically splits the input iterator between threads:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-diff-rust diff-highlight"><code>
+// Divide the files to be searched into equal portions and send to worker threads, via rayon's par_iter.
+- for path in paths {
++ paths.par_iter().for_each(|path| {
+</code></pre>
+
+
+ <p class="">That’s basically it. Rust guarantees we have no data races or unsafe sharing. I’ve also sped things up a bit more by having our threads <a href="https://github.com/Kleptine/csharp-semantic-search/blob/master/src/main.rs#L159">output text to a shared buffer of strings</a>, with a simple Mutex, rather having them all trying to lock the standard output stream with <code>println!</code> .</p><p class="">Look at this utilization! This is a tool I’d be happy to use.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/be6a97e9-e6e8-4ab6-943b-64e8a93f652d/FiJkXzlXEAEAE4D.png" data-image-dimensions="1514x673" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/be6a97e9-e6e8-4ab6-943b-64e8a93f652d/FiJkXzlXEAEAE4D.png?format=1000w" width="1514" height="673" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/be6a97e9-e6e8-4ab6-943b-64e8a93f652d/FiJkXzlXEAEAE4D.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/be6a97e9-e6e8-4ab6-943b-64e8a93f652d/FiJkXzlXEAEAE4D.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/be6a97e9-e6e8-4ab6-943b-64e8a93f652d/FiJkXzlXEAEAE4D.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/be6a97e9-e6e8-4ab6-943b-64e8a93f652d/FiJkXzlXEAEAE4D.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/be6a97e9-e6e8-4ab6-943b-64e8a93f652d/FiJkXzlXEAEAE4D.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/be6a97e9-e6e8-4ab6-943b-64e8a93f652d/FiJkXzlXEAEAE4D.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/be6a97e9-e6e8-4ab6-943b-64e8a93f652d/FiJkXzlXEAEAE4D.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Profile courtesy of Superluminal.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <h2>Closing Thoughts</h2><p class="">Sometimes it’s hard to exactly convey why I find Rust so appealing. There’s a lot of zealotry around the community, which is.. in good faith, but can leave a bad taste in the mouth of those who are just exploring. We don’t need to rush to rewrite everything in Rust. It took Rust 10 years to get here, we’ve got time.</p><p class="">That said, I really like the language. It’s as if someone set out to design a programming language, and just picked all the right answers. Great ecosystem, flawless cross platform<a href="#margin">Finally, a language where Windows is not an after-thought. I’m looking at you Python and Go…</a>, built-in build tools, no “magic”, static binaries, performance-focused, built-in concurrency checks. Maybe these “correct” choices are just laser-targeted at my soul, but in my experience, once you leap over the initial hurdles, it all just works™️, without much fanfare. </p><p class="">It’s magic that I spent a few hours playing with a new library and came out with a tool that is blazingly fast <em>and</em> not going to fall over in a stiff breeze like so many bash scripts.</p><p class="">And that leads to my primary feeling: <span>Rust is just fun</span>. Programming is finally fun again. <br></p>]]></description></item><item><title>Fast Post-Processing on the Oculus Quest and Unity</title><dc:creator>John Austin</dc:creator><pubDate>Sun, 18 Sep 2022 00:32:46 +0000</pubDate><link>https://johnaustin.io/articles/2022/fast-post-processing-on-the-oculus-quest</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:6326489e4be25f1d0d1d198e</guid><description><![CDATA[<p class="">One of the earliest effects I implemented for The Last Clockwinder was HDR Tonemapping and Color Grading. We had decided from an early stage that the game was going to be lit entirely with static lightmapping<a href="#margin">The entire environment is lit with a single, massive bounced area light!</a>, and post processing goes hand-in-hand with that approach. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img class="thumb-image" elementtiming="system-gallery-block-slideshow" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1663546063780-OP1PP8Y7VZDXL6KN1EAU/The_Last_Clockwinder_Umgebung-1200x675.jpg" data-image-dimensions="1200x675" data-image-focal-point="0.4965986394557823,0.38365963855421686" alt=" The Last Clockwinder uses ACES tonemapping and a pre-renderered Color Lookup Table (LUT). the brightest colors flatten towards desaturated white, a feature of using a filmic tonemapper. " data-load="false" data-image-id="6327b2cfca5fb034dd2682e0" data-type="image" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1663546063780-OP1PP8Y7VZDXL6KN1EAU/The_Last_Clockwinder_Umgebung-1200x675.jpg?format=1000w" /><br>
+
+
+
+
+
+
+
+
+ <p class="">The Last Clockwinder uses ACES tonemapping and a pre-renderered Color Lookup Table (LUT). the brightest colors flatten towards desaturated white, a feature of using a filmic tonemapper.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img class="thumb-image" elementtiming="system-gallery-block-slideshow" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1663546055041-YQEI1A2SP64GTNFBWACY/Unity_koBvB96Bih.png" data-image-dimensions="1306x798" data-image-focal-point="0.5,0.5" alt=" The same shot, without HDR tonemapping or color grading. " data-load="false" data-image-id="6327b2c5787a9c6850704518" data-type="image" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1663546055041-YQEI1A2SP64GTNFBWACY/Unity_koBvB96Bih.png?format=1000w" /><br>
+
+
+
+
+
+
+
+
+ <p class="">The same shot, without HDR tonemapping or color grading.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">If you’re a VR developer you might find that surprising! </p><p class="">Generally, post processing isn’t <a href="https://developer.oculus.com/blog/pc-rendering-techniques-to-avoid-when-developing-for-mobile-vr/">viable</a> on platforms like the Oculus Quest. Rendering a second pass requires you to first resolve your intermediate framebuffer to main memory and then read it back again. This can eat up 20-30% of your frame budget just waiting on memory!</p><p class="">Additionally, at least in Unity, MSAA and Foveated Rendering are only supported on the backbuffer, and not on intermediate buffers. Obviously, these are non-starters for efficient rendering. </p><h2>A simple trick: move Post Post-Processing to the Forward Pass </h2><p class="">Luckily, we can apply a trick here. Any post-processing that is independent per pixel<a href="#margin">Ex: Color Grading, Tonemapping, Vignette, Film Grain. But <em>not </em>Blur, Bloom.</a> can be moved to run directly in the forward geometry pass, rendered right at the end of the pixel shader. The pixel shaders calculate with 16bit HDR values, and are tone-mapped down to 8bit RGB just before outputting the final pixel color.</p><p class="">In our case, I modified the Unity URP forward renderer and Shader Graph packages to enable this tweak with a checkbox in the render pipelines settings asset. If you’re interested in replicating this you can take a look at the PR <a href="https://github.com/AStrangerGravity/Graphics/pull/3">here</a> in our public branch of the Unity Graphics repo. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-hlsl"><code>// Used in Standard (Physically Based) shader
+half4 LitPassFragment(Varyings input) : SV_Target
+{
+ UNITY_SETUP_INSTANCE_ID(input);
+ UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
+
+ half4 color = UniversalFragmentPBR(inputData, surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.occlusion, surfaceData.emission, surfaceData.alpha);
+ // ... snipped for space
+ color.rgb = MixFog(color.rgb, inputData.fogCoord);
+
+ // (ASG) Add tonemapping and color grading in forward pass.
+ // This uses the same color grading function as the post processing shader.
+#ifdef _COLOR_TRANSFORM_IN_FORWARD
+ color.rgb = ApplyColorGrading(color.rgb, _Lut_Params.w, TEXTURE2D_ARGS(_InternalLut, sampler_LinearClamp), _Lut_Params.xyz, TEXTURE2D_ARGS(_UserLut, sampler_LinearClamp), _UserLut_Params.xyz, _UserLut_Params.w);
+#endif
+
+ // Return linear color. Conversion to sRGB happens automatically through the target texture format.
+ // (ASG) Note: sRGB conversion *must* be done in hardware, so that filtering / msaa
+ // averaging is done properly in linear space, rather than in sRGB space.
+ return color;
+}</code></pre>
+
+
+ <p class="">I’ve heard of a few other developers applying this technique, so I suspect that most of the AAA-looking games on the Quest store are doing something similar. I’d be curious to hear of any others doing something similar!</p><h2>Downsides</h2><p class="">Of course, there are some downsides, which are good reasons not to have this on by default for all Unity projects.</p><p class=""><strong>Incorrect Transparent Blending</strong></p><p class="">In a standard two-pass HDR pipeline, blending transparent objects with the pixels behind it occurs on the HDR frame buffer. This means it happens in a linear RGB color space.<a href="#margin">If you’re not familiar with linear/non-linear RGB, you might check out my Strangeloop talk <a href=”https://www.youtube.com/watch?v=AS1OHMW873s”>here</a> on color science.</a> Moving tone-mapping to the forward pass means that blending happens <strong>after</strong> tone-mapping. </p><p class="">But tone-mapping is a non-linear function. And so applying a linear blend operation after tone-mapping is not equivalent to applying the blend before tone-mapping. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/62c92622-3358-4c9c-9dc0-3523f202e8ab/image11.png" data-image-dimensions="602x431" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/62c92622-3358-4c9c-9dc0-3523f202e8ab/image11.png?format=1000w" width="602" height="431" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/62c92622-3358-4c9c-9dc0-3523f202e8ab/image11.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/62c92622-3358-4c9c-9dc0-3523f202e8ab/image11.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/62c92622-3358-4c9c-9dc0-3523f202e8ab/image11.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/62c92622-3358-4c9c-9dc0-3523f202e8ab/image11.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/62c92622-3358-4c9c-9dc0-3523f202e8ab/image11.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/62c92622-3358-4c9c-9dc0-3523f202e8ab/image11.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/62c92622-3358-4c9c-9dc0-3523f202e8ab/image11.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">The ACES filmic tone-mapping curve. It’s a non-linear transformation! (image: Chris Brejon) </p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Pragmatically, this means that your transparent objects will look a little bit different than they would in a traditional HDR pipeline, especially if you’re additively layering effects such that the final brightness starts to drift into either shoulder of the ACES S-Curve.<a href="#margin">Theoretically there should also be some incorrect blending around the anti-aliased edges of geometry. However, I’ve never noticed this in practice. </a> </p><p class="">In our case, this effect was entirely unnoticeable. We don’t use many transparent materials to begin with, and none of them were very bright or dark. And for the vast majority of projects, you could adjust for these affects in your materials. </p><p class=""><strong>More Expensive Pixel Shaders</strong></p><p class="">We’ve added a handful of math operations (Tonemapping) and a texture LUT read (Color Grading), so naturally our pixel shaders are a bit more expensive. And it’s more expensive to do this work in the pixel shader than a separate pass:</p><ul data-rte-list="default"><li><p class="">If you’re using MSAA 4x, any pixels hit by MSAA (pixels on the edges of triangles) will be executed 4 times for every final pixel. A standard post-process pass only executes once per pixel. </p></li><li><p class="">The GPU renders in quads, and some of these pixels are discarded. So you will be running post processing for these discarded pixels as well. This is called <a href="https://www.youtube.com/watch?t=404s&v=UZH4vZ0NDAw">Quad Overdraw</a>.</p></li><li><p class="">Inefficient draw ordering. If your objects aren’t perfectly sorted, pixel shaders may execute for occluded pixels, before being rendered over by later geometry. This is highly hardware specific. For example, the Qualcomm Snapdragon XR2 in the Quest 2 implements a <a href="https://developer.qualcomm.com/sites/default/files/docs/adreno-gpu/snapdragon-game-toolkit/gdg/gpu/overview.html#lrz">Low Resolution Z-pass</a> (aka. “Order independent depth testing”). However, it’s disabled for alpha cutout shaders, and only applies conservatively because of the low resolution. </p></li></ul><p class="">That said, the cost of these additional operations is still quite low, especially if the LUT fits in cache.</p><h2>Conclusion</h2><p class="">The nice thing about our implementation is that we can continue to use all of the standard Post Processing Volume components and configuration on the Unity side. No changes needed to happen except in the renderer. </p><p class="">Looking forward to the future, <a href="https://developer.samsung.com/galaxy-gamedev/resources/articles/renderpasses.html">Vulkan Subpasses</a> should make this kind of work much easier. But until then, rolling it into the forward pass worked well for us, and I’d recommend it for anyone on a tile-based GPU platform.</p><p class=""> </p>]]></description></item><item><title>Hot Reloading Rust: Windows and Linux</title><category>Article</category><dc:creator>John Austin</dc:creator><pubDate>Sun, 06 Feb 2022 23:03:52 +0000</pubDate><link>https://johnaustin.io/articles/2022/hot-reloading-rust</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:61e2d7212036f06114d81c2e</guid><description><![CDATA[<p class="">Recently, I’ve been implementing hot reloading for a new game engine I’m writing in Rust. If you’re interested in the subject, you may have seen the Faster Than Lime <a href="https://fasterthanli.me/articles/so-you-want-to-live-reload-rust">article</a>, which is one of the most detailed sources out there. Although Amos encountered major issues implementing hot reloading on Linux, I was surprised to discover that hot reloading works just fine on Windows! </p><p class="">What makes these platforms different? Read on!</p><h2>Hot Reloading and Thread Local Storage</h2><p class="">Check out the Faster Than Lime article for the gory details, but to summarize: Hot reloading interacts poorly with values in Thread Local Storage, when these values have destructors<a href="#margin">Any object that allocates (Vec, Box, String, Rc) must run a destructor to free the underlying memory.</a>. To see why, let’s consider this situation:</p><ol data-rte-list="default"><li><p class="">The main thread loads a dynamic library.</p></li><li><p class="">Thread 2 calls a function in the library, which creates a TLS value <code>x</code>, associated with Thread 2.</p></li><li><p class="">The main thread unloads the dynamic library.</p></li><li><p class="">Thread 2 exits.</p></li></ol><p class="">When should the destructor for <code>x</code> be run? </p><p class="">Normally, TLS destructors run when a thread exits (step 4), but the dynamic library has already unloaded! The code for the destructor may be long gone! Clearly it cannot run at step 4.</p><p class="">Step 3 is a possibility, but it’s unsafe to access a TLS value from a different thread that the one it was created on. At step 3, we’re running on the main thread. Thread 2 may be busy, or even using that TLS value — we can’t just interrupt it to tell it to run a destructor. Clearly, we cannot destruct the value at step 3!</p><p class="">We shouldn’t leak the value — it could be holding a socket or other RAII resource.</p><h2>What Linux Does</h2><p class="">The second half of the Faster Than Lime article explores this problem. What Linux does, in this case, is to skip unloading the dynamic library. Even if you request unloading the library at step 3, it will hang on to the library until all threads with TLS values from the dynamic library have exited.<a href="#margin">By waiting for the thread to exit, the TLS destructors can be run normally, because the dynamic library is still loaded.</a></p><p class="">This means any thread that enters the dynamic library becomes “poisoned”. The library now can’t be unloaded until all of these threads exit. Worse, if the main thread ever calls into the library, the library can <strong>never </strong>be unloaded. Once Rust registers a TLS destruction callback via <code>__cxa_thread_atexit</code>, we’re stuck.</p><p class="">Obviously, this is not great for our hot-reloading plans. Two options cross my mind:</p><ul data-rte-list="default"><li><p class="">Create a special thread used exclusively for each plugin library.<a href="#margin">You can kill it before unloading.</a></p></li><li><p class="">Push all TLS usage to an <em>additional</em>, unloadable, dynamic library. Ban TLS in all plugins libraries. <a href="#margin">TLS values are associated with the library that creates them. In Amos’s case, the println! macro was allocating TLS in the std. Dynamically linking the standard library to each plugin would allow the core plugin to reload, because the TLS usage is in a separate library.</a></p></li></ul><p class="">Frankly, neither of those solutions are great. If you’re aware of a great solution on Linux, I’d love to know. But as I’m primarily developing for Windows, I won’t dig deeper here.</p><h2>What Windows Does</h2><p class="">Unlike Linux, the Windows TLS API does not take a destruction callback. Instead, the <code>DllMain</code> function of the dynamic library is called whenever threads are created and destroyed. It’s up to the author of the dynamic library to <a href="https://docs.microsoft.com/en-us/windows/win32/dlls/using-thread-local-storage-in-a-dynamic-link-library">manually manage</a> the creation and destruction of TLS values. So in this case, it’s up to <strong>Rust</strong> how it wants to implement TLS Destructors.</p><p class="">What does Rust do? </p><p class="">Let’s write a short program to test out the behavior of TLS destructors on Windows, simulating the scenario at the start of this article. For our TLS value, we’re using a custom type that prints whenever it is constructed or drops.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-bash"><code>PS C:\Projects\hotreload> .\harness.exe
+Loading library on main thread. Thread=19944.
+Calling plugin function on thread 2. Thread=28996
+(TLS) Initialized X on thread 28996
+Unloading plugin on main thread. Thread=19944
+Finished thread 2. Thread=28996
+</code></pre>
+
+
+ <p class="">The behavior we see is:</p><ol data-rte-list="default"><li><p class="">The plugin is loaded.</p></li><li><p class="">The TLS value is lazily<a href="#margin">Note the fact that TLS values are lazy! If you don’t use them, they never get created.</a> initialized on first use.</p></li><li><p class="">The plugin is unloaded.</p></li><li><p class="">The TLS value is <strong>not </strong>dropped. </p></li></ol><p class="">Odd.. the TLS value is never dropped — it never printed ‘Destroyed’. But the plugin library <strong>was</strong> successfully unloaded. We can confirm that with WinDBG’s module explorer. What’s happening here? </p><p class="">Let’s dig a bit deeper. The beauty of an open source standard library is that we can just browse the code! Destructors for TLS values are handled in the standard library by <code><a href="https://github.com/rust-lang/rust/blob/17dfae79bbc3dabe1427073086acf7f7bd45148c/library/std/src/sys/windows/thread_local_key.rs#L196">std::sys::windows::on_tls_callback</a></code>. </p><p class="">The documentation for this file is absolutely fantastic, so I highly recommend giving it a read. <code>on_tls_callback</code> is called by the OS when a thread is destroyed (<code><a href="https://docs.microsoft.com/en-us/windows/win32/dlls/dllmain#parameters">DLL_THREAD_DETACH</a></code>), or the process exits / library is unloaded (<code><a href="https://docs.microsoft.com/en-us/windows/win32/dlls/dllmain#parameters">PROCESS_DETACH</a></code>).</p><p class="">We can drop down a break point into this function to see what happens. Note that in Rust, the standard library is compiled statically into both the main executable and the dynamic library, so there are actually <strong>two</strong> versions of <code>on_tls_callback</code> in our program, but the one in the dynamic library is the one that will be called when the library unloads.</p><p class="">In WinDBG we can use the command: <code>bu plugin!std::sys::windows::thread_local_key::on_tls_callback</code>.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/baf7f613-ea77-4e09-ae25-d2992e80d958/DbgX.Shell_2022-01-29_15-54-00.png" data-image-dimensions="1188x686" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/baf7f613-ea77-4e09-ae25-d2992e80d958/DbgX.Shell_2022-01-29_15-54-00.png?format=1000w" width="1188" height="686" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/baf7f613-ea77-4e09-ae25-d2992e80d958/DbgX.Shell_2022-01-29_15-54-00.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/baf7f613-ea77-4e09-ae25-d2992e80d958/DbgX.Shell_2022-01-29_15-54-00.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/baf7f613-ea77-4e09-ae25-d2992e80d958/DbgX.Shell_2022-01-29_15-54-00.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/baf7f613-ea77-4e09-ae25-d2992e80d958/DbgX.Shell_2022-01-29_15-54-00.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/baf7f613-ea77-4e09-ae25-d2992e80d958/DbgX.Shell_2022-01-29_15-54-00.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/baf7f613-ea77-4e09-ae25-d2992e80d958/DbgX.Shell_2022-01-29_15-54-00.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/baf7f613-ea77-4e09-ae25-d2992e80d958/DbgX.Shell_2022-01-29_15-54-00.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Stepping through we see the following behavior:</p><p class=""><code>dwReason</code> (aka register <code>edx</code>) contains the value of 0 (<code><a href="https://github.com/rust-lang/rust/blob/17dfae79bbc3dabe1427073086acf7f7bd45148c/library/std/src/sys/windows/c.rs#L184">DLL_PROCESS_DETACH</a></code>) when the library unloads. This is what we expect based on Microsoft documentation, for an unloading dynamic library.</p><p class="">Both <code>run_dtors</code> and <code>run_keyless_dtors</code> are called, but when stepping through, both of them have an empty list of destructors! So we’re not running any destructors at all.</p><p class="">This makes some sense though. The library is being unloaded on the main thread, but in our use case, the main thread never calls into the library! The TLS values in our plugin are never initialized on the main thread, so there’s nothing to destroy.</p><p class="">So what happens if we unload the plugin instead on thread 2? </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-bash"><code>PS C:\Projects\hotreload> .\harness.exe
+Loading library on main thread. Thread=6456.
+Calling plugin function on thread 2. Thread=14024
+(TLS) Initialized X on thread 14024
+Unloading plugin on thread 2. Thread=14024
+(TLS) Dropping X on thread 14024
+Finished thread 2. Thread=14024
+</code></pre>
+
+
+ <p class="">Oho! So now we’re properly dropping our TLS value. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <p class="">This leads to our final conclusion:</p><p class=""><strong>Rust will run the TLS destructors for the thread that unloads the library. All other TLS values are leaked. </strong><a href="#margin">Note: Rust only runs the TLS destructors associated with the unloading library. The TLS values associated with the primary executable will be left alone — as they should, because the rest of the program is still running!</a> </p><p class="">The leaks are unfortunate, but to be fair, that’s probably the best we can do in this situation. Windows is unloading the library whether we like it or not, and the TLS destructors on the current thread are the only ones that are safe to access. </p><p class="">A silver lining of this approach is that if the library is only used from a single thread, hot-reloading works perfectly: it can be loaded and unloaded promptly, and without TLS leaks. <a href="#margin">And unlike Linux, that thread can continue to live on past the usage of the dynamic library.</a></p><h2>Hot-reloading in Rust</h2><p class="">So which strategy is better, Windows or Linux? Honestly, it feels like a bit of a wash. </p><p class="">Linux is arguably the safest. It guarantees that no TLS values are leaked. However, it does that... by leaking the entire dynamic library itself. I’m not sure that’s an obvious win, especially if you’re running on the main thread, and it’s certainly a problem for implementing hot-reloading on Linux.</p><p class="">Rust’s Windows implementation is more flexible, but this leads to situations where TLS values are leaked, which is dangerous for RAII resources. But.. that flexibility enables us to make our own choice. If we know the TLS values are safe to leak for our plugins, hot-reloading works just fine. And so far I haven’t <em>actually found </em>one that has caused issues. I’d be curious from others if they have examples of TLS values with RAII semantics. </p><p class="">At the end of the day, hot-reloading in Rust is usable on Windows by default, so it’s hard not to at least give it a point there. At least for me as a game developer, hot-reloaded modules are a critical engine feature.</p><p class="">Perhaps the real criminal here is thread-local storage itself. The very nature of tying value lifetime to thread lifetime conflicts with the ability to unload dynamic libraries. But as far as performance goes, it’s hard to imagine life without them. Perhaps one day Rust could expose a different primitive that provides the benefits of both!</p><h3>Further Reading:</h3><ul data-rte-list="default"><li><p class=""><a href="https://github.com/rust-lang/rust/issues/28129">The Rust Team Grapples with TLS Destructors - Github Issues</a> </p></li><li><p class=""><a href="https://docs.microsoft.com/en-us/windows/win32/procthread/thread-local-storage">Thread Local Storage - MDSN</a></p></li></ul>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />]]></description></item><item><title>Scene View Debug Modes in the Unity URP</title><category>Article</category><dc:creator>John Austin</dc:creator><pubDate>Wed, 10 Mar 2021 22:41:29 +0000</pubDate><link>https://johnaustin.io/articles/2021/scene-view-debug-modes-in-the-unity-urp</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:6049490ae2d41705723143ad</guid><description><![CDATA[<p class="">In the Unity Universal Render Pipeline, the scene view debug shader list is fairly empty. There aren’t any of the standard view modes you would expect for debugging PBR materials (Normal, Albedo, etc). </p><p class="">So I added them, here you go.</p><p data-rte-preserve-empty="true" class=""></p><p class=""><a href="https://johnaustin.io/s/SceneDebugViews.zip">Download Here</a></p><blockquote><p class=""><strong>Installation</strong>: Unzip the folder into the path <code>Assets/Editor/SceneDebugViews/</code> or modify <code>SceneDebugViewsAsset.cs</code> to point to the correct installation location.</p></blockquote><p data-rte-preserve-empty="true" class=""></p><p class="">This package gives you a few modes:</p><ul data-rte-list="default"><li><p class="">World Normals (Per-Vertex)</p></li><li><p class="">World Normals (Per-Pixel)</p></li><li><p class="">World Normals (Per-Pixel, Absolute)</p></li><li><p class="">Object Normals (Per-Vertex)</p></li><li><p class="">Object Normals (Per-Vertex, Absolute)</p></li><li><p class="">Albedo</p></li><li><p class="">Occlusion</p></li><li><p class="">Smoothness</p><p data-rte-preserve-empty="true" class=""></p></li></ul><p class="">Normals can be shown in standard or absolute. Absolute shows the raw normal color, but clips negative normals as black. Standard shows the normals, remapped from [-1 .. 1] to [0 .. 1].</p><p class="">These only support the URP, but it would not be hard to modify the shaders to support the HDRP or Legacy pipeline.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <a role="presentation" class="
+ image-slide-anchor
+
+ content-fill
+ "
+ >
+
+ <img class="thumb-image" elementtiming="system-gallery-block-grid" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1615416323579-STAKM9Q2XNGT2YBVYB4P/Unity_Y4gZBjP8dh.png" data-image-dimensions="872x645" data-image-focal-point="0.5,0.5" alt="Unity_Y4gZBjP8dh.png" data-load="false" data-image-id="60494c0230dfbe479515235d" data-type="image" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1615416323579-STAKM9Q2XNGT2YBVYB4P/Unity_Y4gZBjP8dh.png?format=1000w" /><br>
+ </a>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <a role="presentation" class="
+ image-slide-anchor
+
+ content-fill
+ "
+ >
+
+ <img class="thumb-image" elementtiming="system-gallery-block-grid" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1615416323338-X4KSPSW9G2MGFBP3I3HW/Unity_OhzFvYutzh.png" data-image-dimensions="872x645" data-image-focal-point="0.5,0.5" alt="Unity_OhzFvYutzh.png" data-load="false" data-image-id="60494c0230e2f3294865fbcf" data-type="image" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1615416323338-X4KSPSW9G2MGFBP3I3HW/Unity_OhzFvYutzh.png?format=1000w" /><br>
+ </a>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <a role="presentation" class="
+ image-slide-anchor
+
+ content-fill
+ "
+ >
+
+ <img class="thumb-image" elementtiming="system-gallery-block-grid" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1615416325198-O2NS0KDCYI6PBIV85QAU/Unity_orHisuWDCl.png" data-image-dimensions="872x645" data-image-focal-point="0.5,0.5" alt="Unity_orHisuWDCl.png" data-load="false" data-image-id="60494c0351e5ef74e67ef762" data-type="image" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1615416325198-O2NS0KDCYI6PBIV85QAU/Unity_orHisuWDCl.png?format=1000w" /><br>
+ </a>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <a role="presentation" class="
+ image-slide-anchor
+
+ content-fill
+ "
+ >
+
+ <img class="thumb-image" elementtiming="system-gallery-block-grid" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1615416325285-8FICKIBIK9F3CYRHVPO9/Unity_Xdjqo1kaHx.png" data-image-dimensions="872x645" data-image-focal-point="0.5,0.5" alt="Unity_Xdjqo1kaHx.png" data-load="false" data-image-id="60494c042e18cc7ab988e55f" data-type="image" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1615416325285-8FICKIBIK9F3CYRHVPO9/Unity_Xdjqo1kaHx.png?format=1000w" /><br>
+ </a>]]></description></item><item><title>Fast Domain Reloads in Unity</title><category>Article</category><dc:creator>John Austin</dc:creator><pubDate>Tue, 27 Oct 2020 15:16:28 +0000</pubDate><link>https://johnaustin.io/articles/2020/domain-reloads-in-unity</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:5f7dcb12ecabfd5e06f2e217</guid><description><![CDATA[Whenever you make a change in a Unity project, the Editor will freeze while
+it swaps in your new code. This process is called a Domain Reload. While it
+only takes a few seconds in an small project, domain reloads are the bane
+of all large Unity projects. As a project grows, reloads can take 10
+seconds, 20 seconds, and sometimes minutes.
+
+But this doesn’t have to be the case. As an engineer, you have more control
+over your domain reload times than you might think. Most projects should be
+able to achieve a 5 second reload time, almost regardless of overall size.
+
+So let’s dig into this a bit, deeper than we usually go. What are the main
+components of the Domain Reload, and how can we achieve those golden 5
+second iteration times?]]></description><content:encoded><![CDATA[<p class="">Whenever you make a change in a Unity project, the Editor will freeze while it swaps in your new code. This process is called a <a href="https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.unload?view=netcore-3.1">Domain Reload</a>. While it only takes a few seconds in an small project, domain reloads are the bane of all large Unity projects. As a project grows, reloads can take 10 seconds, 20 seconds, and sometimes minutes. </p><p class="">But this doesn’t have to be the case. As an engineer, you have more control over your domain reload times than you might think. <em>Most </em>projects should be able to achieve a <strong>5 second</strong> reload time, almost regardless of overall size.</p><p class="">So let’s dig into this a bit, deeper than we usually go. What are the main components of the Domain Reload, and how can we achieve those golden 5 second iteration times? </p><h1>The Anatomy of a Domain Reload</h1><p class="">There are a few main phases to code compilation:</p><ol data-rte-list="default"><li><p class="">Find and import C# file changes.</p></li><li><p class="">Compile C# code.</p></li><li><p class="">Swap the AppDomain.</p></li><li><p class="">Run InitializeOnLoad callbacks.</p></li></ol><p class="">A long domain reload is almost always a symptom of a problem in one of these phases. Let’s look at each of these individually. </p><h3>1. Importing C# File Changes</h3><p class="">As the number of files in a project grows, Unity spends more and more time scanning those files for changes. The time spent in this step is grows with respect to the number of files in the project. </p><p class="">Luckily, Unity has been working on this problem. In the latest Unity 2020.1 there is a new option: Preferences > General > <a href="https://unity.com/releases/2020-1/editor-team-workflows#asset-import-pipeline-v2">Directory Monitoring</a>. Checking this box will cause Unity to watch for new changes using the underlying OS APIs, rather than scanning the entire Assets folder for updated files. </p><p class="">With this feature, asset scanning times should drop to nearly <strong>zero.</strong></p><h3>2. C# Compilation</h3><p class="">Once Unity knows which files have changed, it must compile the assembly for the changed files, as well as recompile any code that <em>depends </em>on those assemblies. This can take a long time if your change causes a large percentage of the code to be recompiled. </p><p class="">This is where the value of <code>.asmdef</code> files really comes into play. You should architect your project such that <strong>most</strong> code changes happen in Assemblies with few dependencies. This can be tricky, but there is some low hanging fruit: </p><ul data-rte-list="default"><li><p class="">Create a <code>Core</code> assembly, and place gameplay code in a few different leaf-node assemblies, each depending on <code>Core</code>.</p></li><li><p class="">Make sure installed assets and plugins are in a separate folder (ie. the Plugins folder).</p></li></ul><p class="">Doing this correctly, most code changes should only touch a few assemblies, and the code compilation should take no more than <strong>1-2 seconds</strong>. </p><p class="">There are a few tools to help debug this step. For instance, <a href="https://openupm.com/packages/com.needle.compilation-visualizer/">Compilation Time Visualizer</a>.</p><h3>3. Swap the AppDomain</h3><p class="">This is the big one, and the most opaque. Unity doesn’t do a great job of documenting <em>what actually happens</em> during a Domain Reload. However, they have released a fantastic profiling tool, the <a href="https://forum.unity.com/threads/introducing-the-editor-iteration-profiler.908390/">Editor Iteration Profiler</a>. Using this we can dig in a bit, to see what is actually taking so long.</p><p class="">As it turns out, the <code>AppDomain.Unload</code> and <code>AppDomain.Load</code> calls are not the problem. Those are fairly fast — on the order of hundreds of milliseconds. The majority of the time in this phase is actually spent saving and restoring managed objects. </p><p class="">What does that mean? All C# objects (classes, structs, etc) are tied to an AppDomain that created them. When Unity reloads the AppDomain, those old objects are permanently destroyed. So to maintain the state of the Editor process, Unity finds all <code>UnityEngine.Object</code> or <code>[Serializable]</code> objects that are currently alive, serializes them, and restores them after the AppDomain has been swapped.</p><p class="">This serialization can take <strong>ages</strong>. And worse, <span>it is linear with the number of objects currently alive in the Editor process</span>. This explains why Domain Reload times tend to grow as the Editor stays open: as more objects are created, Unity must save/load more state.</p><p class="">As a project grows, we need to keep a close watch on the total memory footprint of objects in the Editor. Luckily, the Unity tools team is at it again. There is the <a href="https://docs.unity3d.com/Packages/com.unity.memoryprofiler@0.2/manual/index.html">Memory Profiler</a> package, which lets us do just that. Here’s what a capture of a recent project looks like:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1602084302570-SYAMGMMOIWPSBOFXSQD9/Unity_oZ8zpbpHvr.png" data-image-dimensions="2500x1523" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1602084302570-SYAMGMMOIWPSBOFXSQD9/Unity_oZ8zpbpHvr.png?format=1000w" width="2500" height="1523" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1602084302570-SYAMGMMOIWPSBOFXSQD9/Unity_oZ8zpbpHvr.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1602084302570-SYAMGMMOIWPSBOFXSQD9/Unity_oZ8zpbpHvr.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1602084302570-SYAMGMMOIWPSBOFXSQD9/Unity_oZ8zpbpHvr.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1602084302570-SYAMGMMOIWPSBOFXSQD9/Unity_oZ8zpbpHvr.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1602084302570-SYAMGMMOIWPSBOFXSQD9/Unity_oZ8zpbpHvr.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1602084302570-SYAMGMMOIWPSBOFXSQD9/Unity_oZ8zpbpHvr.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1602084302570-SYAMGMMOIWPSBOFXSQD9/Unity_oZ8zpbpHvr.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">This is a <a href="https://en.wikipedia.org/wiki/Treemapping">treemap</a> visualization of all managed C# objects in the current AppDomain. Notice the 96mb byte[] array. In this case, serializing this one byte array was adding 1-2 seconds of time to the domain reload! This array was being generated by a custom importer for a large asset file. </p><p class="">Memory Profiler also lets you <em>diff</em> between two snapshots. It’s a great idea to take regular snapshots, to keep track of how the memory footprint of your project grows over time, or in different scenes.</p><p class="">There is low hanging fruit here:</p><ul data-rte-list="default"><li><p class="">Avoid large, serializable data in Unity Objects. </p></li><li><p class="">Assets objects are loaded lazily, as they are needed (ex: clicking an asset in the project window). A few careful calls to <code><a href="https://docs.unity3d.com/ScriptReference/Resources.UnloadUnusedAssets.html">AssetDatabase.UnloadUnusedAssets</a></code> can reduce the total memory footprint dramatically.</p></li><li><p class="">Avoid heavy calls in <a href="OnAfterDeserialize">OnAfterDeserialize</a>/<a href="https://docs.unity3d.com/ScriptReference/ISerializationCallbackReceiver.OnBeforeSerialize.html">OnBeforeSerialize</a>, as well as in constructors and functions like <code>Awake</code> (if an object is ExecuteInEditMode). These methods are called during serialization, and run during Domain Reload for all Objects.</p></li><li><p class="">Try to limit the overall size of scenes. All objects and scripts a scene depends on will contribute to your memory footprint. Splitting up scenes can reduce this.</p></li><li><p class="">Keep windows closed, unless you need them! There’s no need to have objects from those windows floating around, taking up serialization time.</p></li></ul><p class="">You don’t need to worry about asset size. Native assets like textures, models, audio, or any other data that is stored in the native layer (ie. the C++ core of Unity) do not get serialized during this phase. What matters is the size of the C# classes.</p><p class="">At the end of the day, this step is likely going to take a second or two. But at least it can be lowered with a bit of careful work.</p><h3>4. Run InitializeOnLoad Callbacks</h3><p class="">After a domain reload, Unity calls all functions tagged with <code>[InitializeOnLoad]</code>. This is the phase that most people have already optimized, but can still take a bit of time. The best you can do here is either optimize these functions, or remove them. A few notable examples:</p><ul data-rte-list="default"><li><p class="">The new Unity Input System takes ~800ms to <a href="https://forum.unity.com/threads/inputsystem-adding-1-second-to-domain-reload.984281/">reload</a> and recreate input devices.</p></li><li><p class="">Empty folder scanners (like <a href="https://assetstore.unity.com/packages/tools/utilities/maintainer-32199">Maintainer</a>) can take ~350ms to scan the Assets directory.</p></li><li><p class="">Packages for editors like Rider and Visual Studio often take a bit of time to regenerate solution files. </p></li></ul><p class="">There’s already a good amount of information on this phase, so I won’t cover it more.</p><h3>Bonus: Miscellaneous Operations</h3><p class="">There’s a few other tasks that Unity does when reloading the domain. Most are small, but they can add up:</p><ul data-rte-list="default"><li><p class="">Unity immediately repaints the Editor UI after reload. Normally this is pretty fast (~200ms), but if you have complicated editor windows, this could be a bottleneck.</p></li><li><p class="">Unity calls the constructors for EditorWindow windows. Keep heavy code out of here.</p></li><li><p class="">Unity calls OnPostProcessAllAssets (for the changed C# files). </p></li><li><p class="">Unity regenerates the <a href="https://docs.unity3d.com/ScriptReference/TypeCache.html">TypeCache</a>. This takes ~300ms, depending on number of types in your assemblies. (Using this class saves time in the long run, however)</p></li></ul><h2>Conclusion</h2><p class="">Long iteration times are brutal, and tricky to diagnose. But, don’t despair! <em>Profile it</em>! In a recent large project, we were able to reduce iteration times from ~12 seconds, all the way down to 5 seconds. You don’t <em>have</em> to live with it! </p><p class="">Happy to answer questions: reach out on <a href="https://twitter.com/kleptine">Twitter</a>, or post a comment below.</p>]]></content:encoded></item><item><title>Running Unity 2020.1 in Docker</title><category>Article</category><dc:creator>John Austin</dc:creator><pubDate>Mon, 24 Aug 2020 19:58:43 +0000</pubDate><link>https://johnaustin.io/articles/2020/running-unity-20201-in-docker</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:5f440bdc3759d66b0fd8538a</guid><description><![CDATA[<p class="">With every new update, Unity changes their command line interface a bit, and this is still the case with 2020.1. I spent a few hours getting Unity up and running again within Linux Docker, and wanted to document it for those that follow.</p><p class="">Running Unity within Docker is current unsupported, but seems to work if you know the correct incantation of commands. You can read our general approach in <a href="https://johnaustin.io/articles/2020/automated-unity-builds-at-a-stranger-gravity">this</a> previous article. If you already had Unity 2019.3 running properly in Docker, here are the changes we made to our Docker setup for 2020.1:</p><ol data-rte-list="default"><li><p class="">Activation now requires a special flag to get the .alf. I followed <a href="https://gitlab.com/gableroux/unity3d-gitlab-ci-example#b-locally">these</a> steps for activation, generally, but here’s the modified command I ran to get the .alf XML string:</p><p class=""><code>/opt/Unity/Editor/Unity -batchmode -nographics -manualActivation -logFile /dev/stdout -username myemail@gmail.com -password mypassword</code></p><p class=""><br></p></li><li><p class="">When activating, you must <strong>not</strong> run Unity with xvfb. It will cause a segfault. Use <code>-nographics</code> instead:</p><p class=""><code>/opt/Unity/Editor/Unity -batchmode -nographics</code></p><p class=""><br></p></li><li><p class="">Once you’ve uploaded the .alf file to Unity’s website, and downloaded the .ulf license file, you can specify the license for subsequent Unity runs using the new flag: <code>-manualLicenseFile</code></p><p class="">However, we don’t use this flag, instead we manually inject the license file at the correct path:</p><p class=""><strong>Root User: </strong><code>/root/.local/share/unity3d/Unity/Unity_lic.ulf</code></p><p class=""><strong>Normal User: </strong><code>/home/username/.local/share/unity3d/Unity/Unity_lic.ulf</code></p></li></ol><p data-rte-preserve-empty="true" class=""></p><p class="">If you’re interested in the docker files we use to build our Unity images, here they are. They will, however, need a bit of modification for your own build processes.</p><ul data-rte-list="default"><li><p class=""><a href="https://johnaustin.io/s/unity_clean_unactivated.Dockerfile">unity_clean_unactivated.Dockerfile</a> — builds an Ubuntu image with Unity installed, but unactivated</p></li><li><p class=""><a href="https://johnaustin.io/s/unity_clean.Dockerfile">unity_clean.Dockerfile</a> — consumes the previously built image and activates Unity with a .alf file.</p><p data-rte-preserve-empty="true" class=""></p></li></ul><p class="">Reach out on <a href="https://twitter.com/kleptine/">Twitter</a>.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<a href="https://johnaustin.io/?format=rss" title="Blog RSS" class="social-rss">Blog RSS</a>]]></description></item><item><title>Angular Velocity Integration in PhysX</title><category>Article</category><dc:creator>John Austin</dc:creator><pubDate>Sat, 09 May 2020 02:23:15 +0000</pubDate><link>https://johnaustin.io/articles/2020/when-an-angular-velocity-isnt-an-angular-velocity</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:5eb606ced4d3cc5898b643af</guid><description><![CDATA[I came across some odd behavior when debugging some of the core physics
+code in the Unity engine.]]></description><content:encoded><![CDATA[<p class="">I came across some odd behavior when debugging some of the core physics code in the Unity engine. If we start with a still rigid body, say: </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-c#"><code>body.rotation: Quaternion(0.671550453, 0.243898988, 0.07047556, 0.696108162)
+body.angularVelocity: Vector3(0, 0, 0) </code></pre>
+
+
+ <p class="">and then directly set the body’s angular velocity to a new value:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-csharp"><code>body.angularVelocity = new Vector3(-1.21769, -0.8254209, 0.107366793);</code></pre>
+
+
+ <p class="">The final rotation of the body after one physics step is always <strong>slightly less</strong> than it should be. If you run the math, the correct rotation after a time step of <code>1/72</code> seconds is:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-csharp"><code>body.rotation: Quaternion(0.6714375, 0.243848488, 0.07051581, 0.696230769) // Mathematically correct, using axis-angle
+body.rotation: Quaternion(0.665043, 0.24099271, 0.07277777, 0.7030958) // What Unity returns </code></pre>
+
+
+ <p class="">In this case, this is only difference of <code>1.151</code> degrees, but when the angular velocity is high, say <code>(36, 10, 11)</code>, the final Unity value can be off by 30 degrees! That’s a very noticeable amount!</p><p class=""><br></p><p class="">But.. why? </p><p class=""><br></p><p class="">I lucked out on this one. I’ve been delving into the dark secrets of quaternions for a totally separate project, and a couple months back I stumbled on an approximation for applying angular velocities to quaternions, mentioned in <a href="https://gamedev.stackexchange.com/questions/108920/applying-angular-velocity-to-quaternion">this StackOverflow post</a>:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">As it turns out, this is <strong>precisely</strong> the approximation used by PhysX for applying a rigid body’s angular velocity. If we run the numbers, we get the results we expect:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre class="language-csharp"><code>body.rotation = Quaternion(0.665043354, 0.240992859, 0.0727777, 0.7030955) // Approximated formula
+body.rotation = Quaternion(0.665043, 0.24099271, 0.07277777, 0.7030958) // What Unity returns
+</code></pre>
+
+
+
+ <p class="">Now <em>that’s </em>clean. We’re within a factor of floating point rounding! Much better. </p><p data-rte-preserve-empty="true" class=""></p><p class="">Does this matter? It does in our case. For our object grabbing code, we need to calculate the precise angular velocity to apply to a body, such that it will rotate to the orientation of the hand in the next time step. This inaccuracy made objects feel just slightly bouncy, rather than snapped to the hand (particularly noticeable under strong rotations of the wrist). </p><p data-rte-preserve-empty="true" class=""></p><p class="">I love little micro-discoveries like this. It’s one step closer to truly understanding how your game engine is put together.</p><p class=""><br>-JA<br></p><p class=""><br><br></p>]]></content:encoded></item><item><title>Fast Subsurface Scattering for the Unity URP</title><category>Technical</category><category>Article</category><dc:creator>John Austin</dc:creator><pubDate>Sat, 02 May 2020 21:34:51 +0000</pubDate><link>https://johnaustin.io/articles/2020/fast-subsurface-scattering-for-the-unity-urp</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:5eada7b5d255432802916352</guid><description><![CDATA[<p class="">We’ve been working on a project targeting the Oculus Quest, which comes with some tricky constraints. The device is essentially a beefy Android phone, but must render a 2880 × 1600 display at 72fps. Compared to a standard 1080p/30fps console game, that’s a factor of 5.3x more pixels that must be shaded per second, and on mobile-grade hardware.</p><p class="">Because of this, everything we do graphically must be extraordinarily lightweight. Our project’s target style has a softness to it, and so I’ve been looking at fast ways to add Subsurface Scattering to our materials. SS interacts with the scene lighting, so it must be written as a modification to the render pipeline itself. Luckily Unity’s URP is open source, so we can modify it right in the project!</p><p class="">If you’re just here for the code, take a look at the following branch <a href="https://github.com/AStrangerGravity/Graphics/pull/1">here</a>, which has the changes to the URP mentioned in this article.</p><h2>Subsurface Scattering in a Forward Renderer</h2><p class="">Most SS <a href="http://advances.realtimerendering.com/s2018/Efficient%20screen%20space%20subsurface%20scattering%20Siggraph%202018.pdf">implementations</a> make use of deferred rendering, adding a depth-aware blur directly onto the light buffer. We can’t afford <em>any</em> separate post-processing passes on the Oculus Quest, much less a full deferred pipeline. Instead, we can look towards how older games simulated subsurface scattering. I went with the approach mentioned in <a href="http://www.cim.mcgill.ca/~derek/files/jgt_wrap.pdf">this</a> 2011 paper: a modified wrapped shading model. By default, wrapped lighting adds energy, so I additionally included a normalization term, which is included at the bottom of the paper.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449666440-2OB3C5D298T0EV56TDM1/image-asset.png" data-image-dimensions="1080x1080" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449666440-2OB3C5D298T0EV56TDM1/image-asset.png?format=1000w" width="1080" height="1080" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449666440-2OB3C5D298T0EV56TDM1/image-asset.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449666440-2OB3C5D298T0EV56TDM1/image-asset.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449666440-2OB3C5D298T0EV56TDM1/image-asset.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449666440-2OB3C5D298T0EV56TDM1/image-asset.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449666440-2OB3C5D298T0EV56TDM1/image-asset.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449666440-2OB3C5D298T0EV56TDM1/image-asset.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449666440-2OB3C5D298T0EV56TDM1/image-asset.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Ground-truth Blender render</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449673655-H4KORZ6BDH3OOQ8B6IGW/image-asset.png" data-image-dimensions="1080x1080" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449673655-H4KORZ6BDH3OOQ8B6IGW/image-asset.png?format=1000w" width="1080" height="1080" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449673655-H4KORZ6BDH3OOQ8B6IGW/image-asset.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449673655-H4KORZ6BDH3OOQ8B6IGW/image-asset.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449673655-H4KORZ6BDH3OOQ8B6IGW/image-asset.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449673655-H4KORZ6BDH3OOQ8B6IGW/image-asset.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449673655-H4KORZ6BDH3OOQ8B6IGW/image-asset.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449673655-H4KORZ6BDH3OOQ8B6IGW/image-asset.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588449673655-H4KORZ6BDH3OOQ8B6IGW/image-asset.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">My Unity URP with wrapped lighting</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Wrapped lighting is a non-physical approach, and so there are a few different algorithms, each with slightly different results. The paper includes some comparisons:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588450499414-CRLKXI2SL54NN4CIO50R/image-asset.png" data-image-dimensions="1423x462" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588450499414-CRLKXI2SL54NN4CIO50R/image-asset.png?format=1000w" width="1423" height="462" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588450499414-CRLKXI2SL54NN4CIO50R/image-asset.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588450499414-CRLKXI2SL54NN4CIO50R/image-asset.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588450499414-CRLKXI2SL54NN4CIO50R/image-asset.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588450499414-CRLKXI2SL54NN4CIO50R/image-asset.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588450499414-CRLKXI2SL54NN4CIO50R/image-asset.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588450499414-CRLKXI2SL54NN4CIO50R/image-asset.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588450499414-CRLKXI2SL54NN4CIO50R/image-asset.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">I preferred the look of Valve’s approach and it also matches more closely to a normal diffuse lighting in non-wrapped areas. It uses the following formula, theta being the angle of the surface towards the light:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<span class="latex">$$f(\theta)=0.25(\cos (\theta)+1)^{2}$$</span>
+
+
+ <p class="">The paper helpfully provides a generalized version to control the distance of the wrapping. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<span class="latex">$$f(\theta, a)=\left\{\begin{array}{ll}
+((\cos \theta+a) /(1+a))^{1+a} & , \text { if } \theta \leq \theta_{m} \\
+0 & , \text { otherwise }
+\end{array}\right.$$
+</script>
+
+
+ <p class="">We can implement this in the URP with a simple lighting function. I’ve added a ‘subsurface color’ parameter to the Lit shader. Additionally, I’ve left in <code>wrapped_valve</code> and <code>wrapped_simple</code> as two alternative wrapping functions you can play with.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre><code class="language-hlsl">// Calculates the subsurface light radiating out from the current fragment. This is a simple approximation using wrapped lighting.
+// Note: This does not use distance attenuation, as it is intented to be used with a sun light.
+// Note: This does not subtract out cast shadows (light.shadowAttenuation), as it is intended to be used on non-shadowed objects. (for now)
+half3 LightingSubsurface(Light light, half3 normalWS, half3 subsurfaceColor, half subsurfaceRadius) {
+ // Calculate normalized wrapped lighting. This spreads the light without adding energy.
+ // This is a normal lambertian lighting calculation (using N dot L), but warping NdotL
+ // to wrap the light further around an object.
+ //
+ // A normalization term is applied to make sure we do not add energy.
+ // http://www.cim.mcgill.ca/~derek/files/jgt_wrap.pdf
+
+ half NdotL = dot(normalWS, light.direction);
+ half alpha = subsurfaceRadius;
+ half theta_m = acos(-alpha); // boundary of the lighting function
+
+ half theta = max(0, NdotL + alpha) - alpha;
+ half normalization_jgt = (2 + alpha) / (2 * (1 + alpha));
+ half wrapped_jgt = (pow(((theta + alpha) / (1 + alpha)), 1 + alpha)) * normalization_jgt;
+
+ half wrapped_valve = 0.25 * (NdotL + 1) * (NdotL + 1);
+ half wrapped_simple = (NdotL + alpha) / (1 + alpha);
+
+ half3 subsurface_radiance = light.color * subsurfaceColor * wrapped_jgt;
+
+ return subsurface_radiance;
+}</code></pre>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588452658248-EDB3F9M2L1O46TQIF0WB/image-asset.png" data-image-dimensions="546x415" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588452658248-EDB3F9M2L1O46TQIF0WB/image-asset.png?format=1000w" width="546" height="415" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588452658248-EDB3F9M2L1O46TQIF0WB/image-asset.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588452658248-EDB3F9M2L1O46TQIF0WB/image-asset.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588452658248-EDB3F9M2L1O46TQIF0WB/image-asset.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588452658248-EDB3F9M2L1O46TQIF0WB/image-asset.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588452658248-EDB3F9M2L1O46TQIF0WB/image-asset.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588452658248-EDB3F9M2L1O46TQIF0WB/image-asset.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588452658248-EDB3F9M2L1O46TQIF0WB/image-asset.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">JGT wrapped (and colored) lighting</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">With just this wrapped lighting, we do get a softer look, but it’s quite a far cry from the target. As it happens, this model <em>is</em> correct, but only if all of the light scatters into the object. However, the key subsurface scattering look (as seen in the above Blender render) comes from the <em>mixing</em> of scattered light and normal PBR diffuse lighting. Most objects will only scatter some portion of the light through the object, with the rest bouncing off as specular or diffuse.</p><p class="">We can simulate this by blending the subsurface lighting with the normal URP lighting routine. Blending rather than adding makes sure we don’t add energy:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre><code class="language-hlsl">half3 mainLightContribution = LightingPhysicallyBased(brdfData, mainLight, inputData.normalWS, inputData.viewDirectionWS);
+half3 subsurfaceContribution = LightingSubsurface(mainLight, inputData.normalWS, _SubsurfaceColor, _SubsurfaceRadius);
+
+// '_SubsurfaceScattering' controls the portion of the direct light that scatters within the object.
+// When 1, all light is scattered within the object, so the full contribution of color comes from the subsurface.
+// When .5, some light is scattered within, picking up the subsurface color, and is added to the normal reflectance of the surface.
+color += mainLightContribution * (1-_SubsurfaceScattering);
+color += subsurfaceContribution * (_SubsurfaceScattering);</code></pre>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453209430-JBZJYPKUISDSVUB9SVLW/image-asset.png" data-image-dimensions="1080x1080" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453209430-JBZJYPKUISDSVUB9SVLW/image-asset.png?format=1000w" width="1080" height="1080" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453209430-JBZJYPKUISDSVUB9SVLW/image-asset.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453209430-JBZJYPKUISDSVUB9SVLW/image-asset.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453209430-JBZJYPKUISDSVUB9SVLW/image-asset.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453209430-JBZJYPKUISDSVUB9SVLW/image-asset.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453209430-JBZJYPKUISDSVUB9SVLW/image-asset.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453209430-JBZJYPKUISDSVUB9SVLW/image-asset.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453209430-JBZJYPKUISDSVUB9SVLW/image-asset.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453241779-PSOM87GI4AIVP7EQMFUX/image-asset.png" data-image-dimensions="1080x1080" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453241779-PSOM87GI4AIVP7EQMFUX/image-asset.png?format=1000w" width="1080" height="1080" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453241779-PSOM87GI4AIVP7EQMFUX/image-asset.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453241779-PSOM87GI4AIVP7EQMFUX/image-asset.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453241779-PSOM87GI4AIVP7EQMFUX/image-asset.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453241779-PSOM87GI4AIVP7EQMFUX/image-asset.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453241779-PSOM87GI4AIVP7EQMFUX/image-asset.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453241779-PSOM87GI4AIVP7EQMFUX/image-asset.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1588453241779-PSOM87GI4AIVP7EQMFUX/image-asset.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">This is the final look, on a ball and suzanne. Pretty close to the blender render! As it turns out, wrapped lighting is quite a good approximation for subsurface scattering on a sphere. On a more complicated mesh, not so much, but I think it still looks pretty good. </p><p class="">If you’re interested in taking a look at the code, I’ve hosted our branch with subsurface scattering on <a href="https://github.com/AStrangerGravity/Graphics/pull/1">Github</a>. Note that for our project, we only need to support a single sun light, and so I’ve only implemented this effect for the directional light. However, it should be fairly easy to add similar support for point lights.</p><p class="">One final note. This is just one part of a full subsurface scattering model, simulating the scattering of light along the surface of an object (bleeding). However, many objects exhibit <em>translucency</em>, where you can see the light <em>through</em> an object. A great technique for this from the Frostbite Engine has already been described by <a href="https://www.alanzucconi.com/2017/08/30/fast-subsurface-scattering-1/">Alan Zucconi</a>.</p><p class="">- JA</p><p data-rte-preserve-empty="true" class=""></p><p class=""><br><br><br></p><p class=""><br><br><br></p>]]></description></item><item><title>Fast, Automated Builds For Unity</title><category>Technical</category><category>Article</category><dc:creator>John Austin</dc:creator><pubDate>Sat, 25 Apr 2020 21:08:42 +0000</pubDate><link>https://johnaustin.io/articles/2020/automated-unity-builds-at-a-stranger-gravity</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:5e922cf92121991619dbee76</guid><description><![CDATA[<p class="">We’ve recently been putting work into quick automated builds for our Unity projects at A Stranger Gravity. Our builds, which run Unity tests and build a Windows standalone, are now down to 2 minutes, using a combination of Docker, Google Cloud, and Azure Pipelines. </p><p class="">This kind of work involves stumbling through arcane documentation and using services that <strong>really</strong> don’t want you to have 20 Gb of data flying around for every build. I thought it would be helpful to consolidate information into something like an automated build bible. Hopefully someone in the future will find this helpful as they tackle this process themselves.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1587848296384-3FLTA4EOR0TYJT5VYE0I/chrome_ChBOCqSbsE.png" data-image-dimensions="1472x842" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1587848296384-3FLTA4EOR0TYJT5VYE0I/chrome_ChBOCqSbsE.png?format=1000w" width="1472" height="842" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1587848296384-3FLTA4EOR0TYJT5VYE0I/chrome_ChBOCqSbsE.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1587848296384-3FLTA4EOR0TYJT5VYE0I/chrome_ChBOCqSbsE.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1587848296384-3FLTA4EOR0TYJT5VYE0I/chrome_ChBOCqSbsE.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1587848296384-3FLTA4EOR0TYJT5VYE0I/chrome_ChBOCqSbsE.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1587848296384-3FLTA4EOR0TYJT5VYE0I/chrome_ChBOCqSbsE.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1587848296384-3FLTA4EOR0TYJT5VYE0I/chrome_ChBOCqSbsE.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1587848296384-3FLTA4EOR0TYJT5VYE0I/chrome_ChBOCqSbsE.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <h3><br>Table of Contents:</h3><ol data-rte-list="default"><li><p class="">High Level Workflow</p></li><li><p class="">Why..</p><ol data-rte-list="default"><li><p class="">Why Docker?</p></li><li><p class="">Why Azure and GCP?</p></li></ol></li><li><p class="">Quirks</p><ol data-rte-list="default"><li><p class="">Unity Quirks</p></li><li><p class="">Docker Quirks</p></li><li><p class="">Azure Quirks</p></li></ol></li></ol>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <h1>The Workflow</h1><p class="">At a high level, our workflow is similar to traditional software development. We build within <a href="https://www.docker.com/resources/what-container">Docker</a> images on <a href="https://cloud.google.com/compute">Google Cloud</a> instances, with a agents managed by <a href="https://azure.microsoft.com/en-us/services/devops/pipelines/">Azure Pipelines</a>.</p><ol data-rte-list="default"><li><p class="">Builds trigger with Azure Pipelines from Git commits made to <code>master</code> and by Github PR checks.</p></li><li><p class="">A Linux-box on Google Cloud Platform performs the build, running the Azure agent.</p></li><li><p class="">Builds execute within a custom Docker image, with the Library folder pre-populated.</p></li></ol><p class="">Our “Quick Check” build runs in 5 minutes, executing tests and making a Windows executable.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <h2>Why Docker?</h2><p class="">We use docker because it makes it easy, and <em>very quick</em> to boot up a clean environment for our builds. In the past I’ve maintained build servers that maintain state between builds, and inevitably these servers drift into a bad state. Docker gives us the peace of mind that our builds are clean. If your change works on the build server, then it’s a local issue, end of story.</p><p class="">Docker makes it incredibly easy to debug issues. You can simply boot up and exact replica of the build machine on your local computer to debug. You can very quickly boot up a different cloud instance if you need extra build capacity, or you want to try out a more powerful machine.</p><p class="">Lastly, Docker is extremely fast. It uses clever caching to “boot” a new machine environment in just seconds, even if the docker image is many Gb of data. This means we can load a clean machine for every build, essentially for free. </p><p class="">If you’re not familiar with docker, I’d suggest reading a bit about it. It’s not a common tool in the game development sphere, but is incredibly useful.</p><h2>Why Azure and Google Cloud?</h2><p class="">A CI like Azure means that all of our build artifacts and logs are easily accessible on the web. You don’t have to figure out how to run a Jenkins instance securely on the public web — it just works. Azure is not particularly special, but we chose it primarily because of the strong .NET support (Unity) and that it had a reliable backend from a large company. Azure additionally had a plentiful free tier for getting started.</p><p class="">As for the build agents themselves, you can get away with using the hosted agents, but you’ll need self-hosting for any meaningful performance. Azure agents, like other CI services, are limited to roughly 10gb of disk space — nowhere near enough for a Unity project. The machines they run on are also quite weak.</p><p class="">We use a GCP instance setup with a <a href="https://cloud.google.com/local-ssd">Local SSD</a> to run our builds. It’s a bit pricey. We had a bunch of GCP credits sitting around from an old venture, and it was a good way to make use of them. An easy secondary option is just to set up a build machine at home. Any machine can be a build agent. A cloud instance is nice, however, because:</p><ol data-rte-list="default"><li><p class="">Server maintenance is real. Things break, machines don’t connect, etc. We’re a remote company and having a reliable instance in the cloud is a big plus.</p></li><li><p class="">Easier upgrades. We can swap disks / images / wipe the whole machine in minutes. This keeps us closer to a clean slate for our builds.</p></li><li><p class="">Power costs money! A personal server isn’t free — power costs can be quite high if you’re not careful. </p></li></ol><p class="">We’ve tried different instance types. Unity importing is largely dependent on disk speed and CPU performance, so the more cores you can afford, the better. As a note, the “High CPU” instances on GCP are not actually faster for single-threaded performance. They do offer better multi-threaded performance, which is only useful for parts of the Unity import process and shader compilation. We’ve found that a high-end personal machine will often beat out some of the beefiest cloud instances. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <p class="">With these systems setup, we execute a fairly simple pipeline, invoking the Unity editor from the command line. There are a lot of gotchas, however with this process:</p><h1>Unity Build Quirks:</h1><p class=""><strong>Mono doesn’t support 64bit Android.<br></strong>Mono doesn’t support building a standalone player on 64 bit ARM. This is just a limitation of Mono, which we use to avoid IL2CPP build times (for quick debug builds). </p><p class=""><strong>Cloud Instances don’t have displays.<br></strong>Unity expects a display by default. You must pass <code>-nographics</code> to Unity to avoid hitting issues on headless servers. Note that in the past, this flag <em>did not </em>work on Linux before 2019.3, and we had to use <code>xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' $UNITY_EXE</code> as a workaround, which runs the editor with a fake display. As of 4/2020, using <code>xvfb</code> is still necessary when running our Unity tests. </p><p class=""><strong>Getting Unity to log output to stdout on Linux.<br></strong>Supposedly, you can pass the argument <code>-logFile</code> with no logfile specified and Unity will output the logfile to stdout. This hasn’t ever worked for us, and we’re not sure why. It may have been deprecated as of 2019.3. There are two solutions. If you’re running the build as root user, you can simply pass <code>-logFile /dev/stdout</code>. AZP agents are not root users, so instead we setup a unix pipe, and run Unity in the background with <code>&</code>: </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<pre data-prompt="$" class="language-bash command-line"><code>mkfifo unity_output
+/path/to/Unity -logFile unity_output (.. other arguments) &
+cat unity_output # blocks until the pipe is closed by Unity!
+</code></pre>
+
+
+
+ <p class="">Unity must be able to open the logfile as write/read. On our system, /dev/stdout was only permissioned for write.</p><p class=""><strong>Disable the assembly updater.<br></strong>By default Unity runs the assembly updater, which tries to update out-of-date code and assemblies. This can block a build if it hits something. For a CI service, it’s better to just fail the build and alert the user. Pass the argument <code>-disable-assembly-updater</code> to unity.</p><p class=""><strong>Burst does not cross compile.<br></strong>We run all of our builds in Linux, but the new Burst compiler <a href="https://forum.unity.com/threads/burst-does-not-support-cross-compilation-between-windows-mac-or-linux.686143/">doesn’t cross compile</a>. Supposedly you can disable burst with the <code>--burst-disable-compilation</code> Unity flag. This has never worked for us. Instead we run an extra script in our C# build script to manually change the burst settings json file, just before building. </p><p class=""><strong>Unity hangs on ‘Cleanup Mono’ after build (Unsolved)<br></strong>Sometimes, Unity will hang at the end of a standalone build. The last entry of the log file is “cleanup mono”. This is after the build has succeeded. For some reason, Unity fails to exit gracefully and just hangs. We’ve been unable to find a graceful way to handle this case. For our standalone builds we simply kill the process after a successful build:</p><p class=""><code>Process.GetCurrentProcess().Kill();</code></p><p class="">It’s messy, but it gets the job done. Because we’re running within Docker, we don’t have to worry about corrupting the state of the editor. </p><h2>Docker Quirks</h2><p class="">Unity really wasn’t built to run on Docker. There’s no official support. That said, in a Linux docker image, Unity executes fine, once you work around some strange quirks. That said, for Windows release builds you <em>will</em> need a non-docker, Windows instance. Here’s two big issues we’ve faced:</p><p class=""><strong>Unity doesn’t run in a Windows Docker container! <br></strong>Inside a Windows Docker container, Unity.exe simply <a href="https://forum.unity.com/threads/unity-in-a-windows-container.645238/">fails to start</a>, with no output. This is regardless of the the arguments you pass it, activation, etc. It just doesn’t seem like this is supported, although some people claim to have gotten it working. Windows Docker containers are also, unfortunately, a bit of a second-class citizen for Docker in general. Docker is heavily Linux focused.</p><p class=""><strong>Windows Release builds can’t run on Linux.<br></strong>This is because the new <a href="https://forum.unity.com/threads/burst-does-not-support-cross-compilation-between-windows-mac-or-linux.686143/">Burst compiler can’t cross compile</a>. This means a windows release build <em>must</em> be made on a Windows machine. If Unity functioned within a Windows Docker image, we could run that machine through docker. Unfortunately the combination of these two issues means that we’re stuck with:</p><ol data-rte-list="default"><li><p class="">Most development and release builds run on Linux in a Docker Image</p></li><li><p class="">The Windows release builds are built locally.</p></li></ol><p class="">In the future we may use a Windows box on GCP to build these release builds, but as all developers have a Windows machine, there’s not a big need.</p><h2>Azure CI Quirks</h2><p class=""><strong>Build Agents don’t run as root.<br></strong>Most CI services run their build agents as root, except Azure. Worse, the default Ubuntu docker image no longer includes <code>sudo</code> as an installed tool. We had to install sudo with a <code>RUN</code> command in our custom Docker image. </p><p class=""><strong>The Cache task is slow.<br></strong>We initially tried caching LFS and the Library folder using Azure Pipelines caching. Unfortunately when these folders are many Gb of data, uploading and downloading those caches became the dominant factor in our builds. Instead we embed a stale version the LFS and Library folders directly into our docker image, and then only download / import the changes for every build.</p><p class=""><strong>Set </strong><code>workspace: clean</code><strong> for self-hosted agents<br></strong>Azure doesn’t set this flag by default on a self-hosted agent. Make sure to set it or your builds won’t be cleaned between runs!</p><p class=""><br></p><p class=""><em>Reach out to me with questions on </em><a href="https://twitter.com/kleptine"><em>Twitter</em></a><em>.</em></p>]]></description></item><item><title>Unity Construction Scripts</title><category>Technical</category><category>Article</category><dc:creator>John Austin</dc:creator><pubDate>Mon, 16 Mar 2020 02:46:43 +0000</pubDate><link>https://johnaustin.io/articles/2020/unity-construction-scripts</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:5e6edcd78afded74f55dd52c</guid><description><![CDATA[<p class="">Unreal Engine has a convenient feature known as “Construction Scripts”: simple scripts that run when an Actor is modified, letting you quickly procedurally generate an object and see the results immediately in the editor.</p><p class="">Unity has a similar feature, <code>ExecuteInEditMode</code>, but it comes with a number of downsides:</p><ol data-rte-list="default"><li><p class="">Changes made to objects in the scene are saved to the scene file, generating nasty merge conflicts when two people modify the same scene, even if they don’t touch the generated objects.</p></li><li><p class=""><code>ExecuteInEditMode</code> can be dangerous — it’s easy to accidentally delete / modify the rest of the scene unintentionally.</p></li><li><p class="">For one-off objects, <code>ExecuteInEditMode</code> requires a ton of boilerplate, with careful coding.</p></li></ol><p data-rte-preserve-empty="true" class=""></p><p class=""><a href="https://gist.github.com/Kleptine/ac4b7db7714003f7968f4c532f0dc82d">Unity Construction Scripts</a> is a simple system which fixes these issues, and makes it quick to write small one-off procedural objects. On top of this, a ConstructionScript is entirely Editor-only. All changes are baked into the scene file just before building the standalone player.</p><p class="">Documentation is provided in the code for now, but feel free to reach out on <a href="http://twitter.com/kleptine">Twitter</a> if you have questions. </p><p class=""> </p>]]></description></item><item><title>Tragedy in the Rise of Giants</title><category>Essay</category><dc:creator>John Austin</dc:creator><pubDate>Wed, 04 Dec 2019 19:31:48 +0000</pubDate><link>https://johnaustin.io/articles/2019/tragedy-in-the-rise-and-fall-of-giants</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:5de7df871ccae03c969cabdf</guid><description><![CDATA[<figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489791109-NBBZC2BLSDTPTCQGOLKD/unnamed.jpg" data-image-dimensions="660x257" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489791109-NBBZC2BLSDTPTCQGOLKD/unnamed.jpg?format=1000w" width="660" height="257" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489791109-NBBZC2BLSDTPTCQGOLKD/unnamed.jpg?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489791109-NBBZC2BLSDTPTCQGOLKD/unnamed.jpg?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489791109-NBBZC2BLSDTPTCQGOLKD/unnamed.jpg?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489791109-NBBZC2BLSDTPTCQGOLKD/unnamed.jpg?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489791109-NBBZC2BLSDTPTCQGOLKD/unnamed.jpg?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489791109-NBBZC2BLSDTPTCQGOLKD/unnamed.jpg?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489791109-NBBZC2BLSDTPTCQGOLKD/unnamed.jpg?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">I first worked at Google back in 2012. Thinking back to the way the company was run then, it seems like an entirely different era. It was a place of radical transparency, where any employee could stand up at a weekly TGIF meeting, and have an earnest conversation directly with the founders. It <em>was </em>just a company, but it really felt like a company with heart. This year has been a bombshell for Google and now with Larry and Sergey finally <a href="https://www.npr.org/2019/12/03/784570156/google-founders-brin-page-step-down-pichai-takes-over-as-alphabet-ceo">leaving</a>, it inspires the question: what happened? In 2008, both had a <a href="https://www.webcitation.org/6BYbNOfxM?url=http%3A%2F%2Fmoney.cnn.com%2F2008%2F01%2F18%2Fnews%2Fcompanies%2Fgoogle.fortune%2Findex.htm">plan to work at Google until 2024</a>. What takes a company with everything going for it, and drags it down into a place with employee protests, cut benefits, and secretive government deals?</p><p class="">The most common answer I hear is “greed”, but I think the real answer is a bit more subtle. Normal people don’t wake up in the morning and decide they’d like to take a controversial government contract. All of Google’s businesses are already growing at a breakneck pace. My opinion? Google’s decline is <em>systematic</em>, not intentional. So what happened? The stock market and rapid expansion. Let me explain:</p><p data-rte-preserve-empty="true" class=""></p><p class="">Here’s a fundamental secret to how the stock market works: Stock prices aren’t driven by the <em>current </em>value of the company, they are driven by the company’s <em>future</em> value. </p><p class="">Let’s say Google is poised to grow 25% every year, for the next 10 years. Investors are smart: a long term growth like that is a great stock to purchase! Naturally, as investors buy in, this raises the price of the <em>current</em> shares. That future growth is ‘priced in’ to the stock’s current price. But here’s the odd thing: if Google simply makes good on their promise to grow, the stock price would stay flat! Investors already know that Google stock is good for a 25% return, so the demand for the stock stays flat, and so does the price.</p><p class="">This quirk means that if a company would like their stock price to continue to rise, not only do they have to grow,<em> they have to grow at an increasing rate. </em>At their earnings report it isn’t enough to be successful, they have to continuously be more successful than anyone thought they could be. </p><p class="">Let that point sink in for a moment. It’s a fundamental driving force of the stock market. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489904514-LB8HFIJ3L5ATI8RXTMUC/chrome_QHspam5IvS.png" data-image-dimensions="613x221" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489904514-LB8HFIJ3L5ATI8RXTMUC/chrome_QHspam5IvS.png?format=1000w" width="613" height="221" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489904514-LB8HFIJ3L5ATI8RXTMUC/chrome_QHspam5IvS.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489904514-LB8HFIJ3L5ATI8RXTMUC/chrome_QHspam5IvS.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489904514-LB8HFIJ3L5ATI8RXTMUC/chrome_QHspam5IvS.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489904514-LB8HFIJ3L5ATI8RXTMUC/chrome_QHspam5IvS.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489904514-LB8HFIJ3L5ATI8RXTMUC/chrome_QHspam5IvS.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489904514-LB8HFIJ3L5ATI8RXTMUC/chrome_QHspam5IvS.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489904514-LB8HFIJ3L5ATI8RXTMUC/chrome_QHspam5IvS.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">So how does this affect Google? If they want their stock price to increase, they must expand <em>exponentially</em>. In 2018, Alphabet added nearly 20,000 employees to the company. In 2011, the entirety of Google <em>was </em>20,000 employees [<a href="http://techland.time.com/2011/01/21/the-eric-schmidt-era-google-2001-vs-google-2011/">1</a>]. Every year, the company adds another Google. </p><p class="">With this kind of insane expansion, it’s no wonder the company is having culture problems. You can’t maintain a culture in that kind of environment. People need time to adapt and assimilate and learn how a company works. Google will be lucky if they can even get these people hired. At 20,000 employees you are running a company — you can be a bit personal with HR and management. At 100,000, you’re running an empire. Your HR department needs to be the size of a small army. Policy becomes a necessity over human interaction — humans are fallible, and at these numbers the chances of failure are too high. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489453364-5T72PTY8KB14YIA3HTCG/1_y18S9SWzF4sDdG1gOIHCrA.png" data-image-dimensions="976x486" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489453364-5T72PTY8KB14YIA3HTCG/1_y18S9SWzF4sDdG1gOIHCrA.png?format=1000w" width="976" height="486" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489453364-5T72PTY8KB14YIA3HTCG/1_y18S9SWzF4sDdG1gOIHCrA.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489453364-5T72PTY8KB14YIA3HTCG/1_y18S9SWzF4sDdG1gOIHCrA.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489453364-5T72PTY8KB14YIA3HTCG/1_y18S9SWzF4sDdG1gOIHCrA.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489453364-5T72PTY8KB14YIA3HTCG/1_y18S9SWzF4sDdG1gOIHCrA.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489453364-5T72PTY8KB14YIA3HTCG/1_y18S9SWzF4sDdG1gOIHCrA.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489453364-5T72PTY8KB14YIA3HTCG/1_y18S9SWzF4sDdG1gOIHCrA.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1575489453364-5T72PTY8KB14YIA3HTCG/1_y18S9SWzF4sDdG1gOIHCrA.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Employee growth by years after incorporation. [<a href="https://medium.com/gabor/timeline-employee-count-growth-for-microsoft-yahoo-google-and-facebook-9ede22a37824">source</a>]</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">This expansion affects their deal-making as well. The ads product might be growing at 10%, but if they don’t push it to 12%, or (god forbid) the grown rate drops to 9%, they’ll be in hot water. It’s in these situations that lucrative government contracts start to become pretty tempting. Sure, those drones might cause harm, but maybe if <em>we’re</em> doing it, we can do it ethically. Better us than someone else, right? “We’ll just take one to ensure the stability of our stock price”.</p><p class="">—</p><p class="">At this point, you might say: “well, fuck the stock price”. Here lies the tragedy. When you are the size of Google, your stock price starts to have major affects on the world. The total value of Google’s stock is <a href="https://www.macrotrends.net/stocks/charts/GOOGL/alphabet/market-cap">$900 billion</a>. You stop expanding, your stock price drops 15%, and suddenly you’ve erased $135 billion from the US economy. That money doesn’t just come from the pockets of the wealthy. It comes from the retirement funds of blue collar workers, investment funds of governments, and municipalities. You directly hurt all of the employees and small investors with money tied up in Google stock. It’s not a dip that will recover, like so many stock market crashes. It only recovers if you grow. </p><p class="">That doesn’t mean Google is making ethical choices — they’re not — but I don’t envy being in the shoes of those decisions. Exponential growth is a runaway train. Someone has to figure out how to stop it. </p><p class=""><br></p><p class=""><br></p><p class=""><br></p>]]></description></item><item><title>Texture Atlasing using Houdini</title><category>Article</category><dc:creator>John Austin</dc:creator><pubDate>Tue, 22 Oct 2019 13:42:35 +0000</pubDate><link>https://johnaustin.io/articles/2019/building-a-texture-atlas-tool-in-houdini</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:5daf01acf04e4a32186df563</guid><description><![CDATA[<p class="">Houdini really is the swiss-army knife of 3D asset pipelines. Recently a friend asked around on twitter: </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <blockquote data-dnt="true" data-theme="light" data-link-color="#E81C4F" class="twitter-tweet"><p lang="en" dir="ltr">I'd think in the year of ahh-lawwd two thousand and nineteen there would be a straightforward solution out there.... hey, take these half-dozen meshes and their associated maps and poop out UV-corrected meshes and a set of mongo-atlassed maps.</p>— Eric A Anderson (@edoublea) <a href="https://twitter.com/edoublea/status/1186489202818441217?ref_src=twsrc%5Etfw">October 22, 2019</a></blockquote>
+
+
+ <p class="">Houdini to the rescue! Let’s start with a few simple shapes. We’ll unwrap them individually in the simplest way possible, and then apply a simple texture to each:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571750913185-7DAI2B76AVBD0DU2QOFP/image-asset.jpeg" data-image-dimensions="1665x1271" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571750913185-7DAI2B76AVBD0DU2QOFP/image-asset.jpeg?format=1000w" width="1665" height="1271" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571750913185-7DAI2B76AVBD0DU2QOFP/image-asset.jpeg?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571750913185-7DAI2B76AVBD0DU2QOFP/image-asset.jpeg?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571750913185-7DAI2B76AVBD0DU2QOFP/image-asset.jpeg?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571750913185-7DAI2B76AVBD0DU2QOFP/image-asset.jpeg?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571750913185-7DAI2B76AVBD0DU2QOFP/image-asset.jpeg?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571750913185-7DAI2B76AVBD0DU2QOFP/image-asset.jpeg?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571750913185-7DAI2B76AVBD0DU2QOFP/image-asset.jpeg?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751083541-MNSUUFVOE44WF6L6K2TJ/houdini_2019-10-22_06-31-13.png" data-image-dimensions="2037x768" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751083541-MNSUUFVOE44WF6L6K2TJ/houdini_2019-10-22_06-31-13.png?format=1000w" width="2037" height="768" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751083541-MNSUUFVOE44WF6L6K2TJ/houdini_2019-10-22_06-31-13.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751083541-MNSUUFVOE44WF6L6K2TJ/houdini_2019-10-22_06-31-13.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751083541-MNSUUFVOE44WF6L6K2TJ/houdini_2019-10-22_06-31-13.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751083541-MNSUUFVOE44WF6L6K2TJ/houdini_2019-10-22_06-31-13.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751083541-MNSUUFVOE44WF6L6K2TJ/houdini_2019-10-22_06-31-13.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751083541-MNSUUFVOE44WF6L6K2TJ/houdini_2019-10-22_06-31-13.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751083541-MNSUUFVOE44WF6L6K2TJ/houdini_2019-10-22_06-31-13.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Three object with 3 separate texture maps.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Now, we can have Houdini merge the individual UV sets into a single UV space with the UV-Layout node:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751154140-ANU1PIDN0LCWYGBXWDBL/houdini_2019-10-22_06-32-27.png" data-image-dimensions="2035x770" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751154140-ANU1PIDN0LCWYGBXWDBL/houdini_2019-10-22_06-32-27.png?format=1000w" width="2035" height="770" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751154140-ANU1PIDN0LCWYGBXWDBL/houdini_2019-10-22_06-32-27.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751154140-ANU1PIDN0LCWYGBXWDBL/houdini_2019-10-22_06-32-27.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751154140-ANU1PIDN0LCWYGBXWDBL/houdini_2019-10-22_06-32-27.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751154140-ANU1PIDN0LCWYGBXWDBL/houdini_2019-10-22_06-32-27.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751154140-ANU1PIDN0LCWYGBXWDBL/houdini_2019-10-22_06-32-27.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751154140-ANU1PIDN0LCWYGBXWDBL/houdini_2019-10-22_06-32-27.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751154140-ANU1PIDN0LCWYGBXWDBL/houdini_2019-10-22_06-32-27.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Now that the UVs are setup, we just need to transfer the original textures from the individual objects into a single packed texture in the new UV space. Luckily, there’s a simple node to do that in the Houdini Game Development Toolkit: GameDev Maps Baker. The Maps Baker takes a ‘High Resolution’ geometry and a ‘Low Resolution’ geometry, and transfers the textures from one to the other. In our case, the high resolution and low resolution geometries are exactly the same, except for the uv layout.</p><p class="">Note that the use of the Quick Material nodes for initial texturing of each object is crucial, as this allows us to specify the ‘diffuse’ channel in the baker and have it track down the correct texture information.</p><p class="">The other important setting is to choose “Nearest Surface” on the <em>Tracing Mode</em> option in the baker. Because our meshes are exactly the same, this transfers the texture perfectly, rather than casting rays to find the closest points.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751439934-EESJ5NMPIYBFS6XI72ZX/image-asset.png" data-image-dimensions="2038x771" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751439934-EESJ5NMPIYBFS6XI72ZX/image-asset.png?format=1000w" width="2038" height="771" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751439934-EESJ5NMPIYBFS6XI72ZX/image-asset.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751439934-EESJ5NMPIYBFS6XI72ZX/image-asset.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751439934-EESJ5NMPIYBFS6XI72ZX/image-asset.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751439934-EESJ5NMPIYBFS6XI72ZX/image-asset.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751439934-EESJ5NMPIYBFS6XI72ZX/image-asset.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751439934-EESJ5NMPIYBFS6XI72ZX/image-asset.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751439934-EESJ5NMPIYBFS6XI72ZX/image-asset.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">The same three objects using a single texture map.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">After pressing ‘bake’ in the node, we get the following texture output:</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751543996-HZFZB513T9NKTSRR2IWV/image-asset.png" data-image-dimensions="2048x2048" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751543996-HZFZB513T9NKTSRR2IWV/image-asset.png?format=1000w" width="2048" height="2048" sizes="(max-width: 640px) 100vw, (max-width: 767px) 50vw, 50vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751543996-HZFZB513T9NKTSRR2IWV/image-asset.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751543996-HZFZB513T9NKTSRR2IWV/image-asset.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751543996-HZFZB513T9NKTSRR2IWV/image-asset.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751543996-HZFZB513T9NKTSRR2IWV/image-asset.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751543996-HZFZB513T9NKTSRR2IWV/image-asset.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751543996-HZFZB513T9NKTSRR2IWV/image-asset.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751543996-HZFZB513T9NKTSRR2IWV/image-asset.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">The new 2048x2048 texture atlas matching the unified UV sets.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Easy peasy. The baker has options for a variety of different PBR maps beyond diffuse, and, incredibly, is built entirely with Houdini COP nodes, meaning you can dive into it and tweak it to your liking. Paul Ambrosiussen gives a great talk <a href="https://www.youtube.com/watch?v=B8HqBuH9xew">here</a> on the Maps Baker.</p><p class="">Hit me up on <a href="http://twitter.com/kleptine">Twitter </a>with questions.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1571751727067-V635183FEO5XOSW5K7NU/different_geos_uv_packing_diffuse.png?format=1500w" medium="image" isDefault="true" width="1500" height="1500"><media:title type="plain">Texture Atlasing using Houdini</media:title></media:content></item><item><title>Fix your (Unity) Timestep!</title><category>Article</category><dc:creator>John Austin</dc:creator><pubDate>Sun, 12 May 2019 20:10:12 +0000</pubDate><link>https://johnaustin.io/articles/2019/fix-your-unity-timestep</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:5cd87daa7398a700018cd3dc</guid><description><![CDATA[<p class="">The preeminent article on game timesteps is <a href="https://web.archive.org/web/20210224113810/https://gafferongames.com/post/fix_your_timestep/">Gaffer On Games: “Fix Your Timestep!”</a>.<a href="#margin">Cached version, as the article occasionally goes down.</a> The article gives a great amount of context for implementing a game engine time step in a couple of different ways. Unfortunately, while this is useful, it is aimed at those writing their own engine, not using an existing one. </p><p class="">On top of that, there is surprisingly little information regarding the precise behavior of the Unity time step. The best articles are in the <a href="https://docs.unity3d.com/Manual/TimeFrameManagement.html">manuals</a>, but they stop at a simple description and don’t go into any subtle detail. </p><p class="">Thus, this article attempts to collate the varying information on the Unity time step in one place and add objective testing to illuminate its behavior in the corner cases. So without further ado!</p><h2>The Unity Timestep</h2><p class="">In this article, we will need to talk about different types of time:</p><p class=""><strong>wall time: </strong>The amount of time passing in the real world on your wall clock — Time.realtimeSinceStartup</p><p class=""><strong>game time: </strong>The amount of time that is perceived to pass within the game — Time.unscaledTime</p><p class="">Under heavy load (see below), <strong>game-time</strong> may slow down, and may not match one-to-one with <strong>wall-time</strong>. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<hr />
+
+
+ <p class="">Unity uses a two-part timestep, as described in the Gaffer article as “Free the physics”. In Unity, this means that there are two main update passes: FixedUpdate and Update. </p><p class="">The <strong>FixedUpdate </strong>pass steps forward in increments of 0.02 <em>game-time</em> seconds<a href="#margin">The default is 0.02 seconds. You can change the fixed timestep amount by setting Time.fixedDeltaTime — even at runtime!</a>, regardless of rendering or <strong>any</strong> other performance factors. The key here is that FixedUpdate is reliable. If 0.02 seconds of game time has passed, it is guaranteed that the FixedUpdate has run during that period. Note that this does not guarantee FixedUpdate will be run every 0.02 <em>wall-clock</em> seconds! This is only the case when game-time and wall-time are in sync.</p><p class="">The <strong>Update</strong> pass steps forward in leaps and bounds. It generally correct to think of this pass as the “render update”, as it runs exactly once per frame render. It steps forward in a dynamic number of <em>game-time </em>seconds. Unity attempts to run this Update pass at the given Application.targetFrameRate. This is generally 60 FPS, but is specialized depending on the platform and V-Sync rate.</p><p class="">Depending on the circumstances, multiple FixedUpdates may run between two Updates (the game is render-blocked), or multiple Updates may run between two FixedUpdates (rendering is faster than the FixedUpdate step). </p><p class="">By default on desktop, Unity runs the FixedUpdate at 50 FPS and the Update at 60 FPS (the VSync rate). As for the reasoning behind this choice, it seems that even the folks at Unity have <a href="https://twitter.com/aras_p/status/1072227369405046792">long forgotten</a>. But consider this. These numbers also happen to be values Gaffer uses for the example at the end of the article! Coincidence? We can only imagine.</p><h2>Measuring Update/FixedUpdate</h2><p class="">Lets test some of the above assumptions. We’ll use the fantastic <a href="https://assetstore.unity.com/packages/tools/utilities/squiggle-21970">Squiggle</a> asset to log graphs of each of our tests.</p><p class="">To start, let’s run Unity under a simple scene and the default settings. The following graph shows FixedUpdate in blue, then Update in red. At the bottom is one second of wall-clock time.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557710867640-FBO6FKWEV9WVIBVD76PU/2019-05-12_18-27-06.png" data-image-dimensions="564x231" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557710867640-FBO6FKWEV9WVIBVD76PU/2019-05-12_18-27-06.png?format=1000w" width="564" height="231" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557710867640-FBO6FKWEV9WVIBVD76PU/2019-05-12_18-27-06.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557710867640-FBO6FKWEV9WVIBVD76PU/2019-05-12_18-27-06.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557710867640-FBO6FKWEV9WVIBVD76PU/2019-05-12_18-27-06.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557710867640-FBO6FKWEV9WVIBVD76PU/2019-05-12_18-27-06.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557710867640-FBO6FKWEV9WVIBVD76PU/2019-05-12_18-27-06.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557710867640-FBO6FKWEV9WVIBVD76PU/2019-05-12_18-27-06.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557710867640-FBO6FKWEV9WVIBVD76PU/2019-05-12_18-27-06.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Blue: FixedUpdate, Red: Update, Grey: 1 Second Wall-Clock</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Over the course of a single wall-time second, 52 FixedUpdates and 62 Updates passed. If game-time and wall-time were <em>perfectly</em> matched, we would expect 50 FixedUpdates and 60 Updates. However, the real world is a messy place, and instead Unity game-time is always catching up and overshooting the wall clock. We over-counted this second, but we’ll under count in some later second to balance it out. In practice, this minor fluctuation in game-time vs. wall-time is imperceptible. </p><p class="">Notice something interesting about the FixedUpdate graph. They seem to be grouped in smaller batches of updates. Within each batch, 20ms of <em>game-time</em> passes for each, and 17ms <em>wall-time</em> passes. Then, there is a gap where 20ms of <em>game-time </em>passes and 35ms of <em>wall-time </em>passes, before another batch gets run. Unity seems to be trying to run the slower FixedUpdate in lockstep with the faster Update until it must pause to maintain 50FPS. </p><p class="">On the other hand, Update is perfectly smooth. We can thank V-Sync for this. My display is set to 60Hz, and as such, Unity performs an Update whenever the display is ready for the next frame. </p><p class="">So what does this mean for stutter? In this setup, now and then <strong>two</strong> frames will be rendered for a single FixedUpdate pass. If the render frames are being sent at 60Hz, you’d see a single frame stutter every 5-8 frames. If you’re doing processing in FixedUpdate, without interpolating,<a href="#margin">Interpolation can fix this stutter to some degree, but has its own set of visual artifacts.</a> you’ll see this fairly consistent stutter on playback. This is not great, and is a big reason developers in Unity feel pressured to do their processing in Update. </p><p class="">You can see this clearly in the following graph. Here, each FixedUpdate is assigned a color. The Update is colored with the FixedUpdate that preceded it.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763576179-A92HVNC3H703C1CMGI3D/2019-05-13_09-04-49.png" data-image-dimensions="695x233" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763576179-A92HVNC3H703C1CMGI3D/2019-05-13_09-04-49.png?format=1000w" width="695" height="233" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763576179-A92HVNC3H703C1CMGI3D/2019-05-13_09-04-49.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763576179-A92HVNC3H703C1CMGI3D/2019-05-13_09-04-49.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763576179-A92HVNC3H703C1CMGI3D/2019-05-13_09-04-49.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763576179-A92HVNC3H703C1CMGI3D/2019-05-13_09-04-49.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763576179-A92HVNC3H703C1CMGI3D/2019-05-13_09-04-49.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763576179-A92HVNC3H703C1CMGI3D/2019-05-13_09-04-49.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763576179-A92HVNC3H703C1CMGI3D/2019-05-13_09-04-49.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Look closely and you’ll notice duplicates in the Update track. Every 5-8 frames, two Updates render for a single FixedUpdate. </p><p class="">Luckily, though, Unity exposes the FixedUpdate time-step to developers. What if we try running the FixedUpdate at 60FPS instead of 50FPS? </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763039989-JUJNBM96FYB20S0OXO63/2019-05-13_08-56-39.png" data-image-dimensions="663x234" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763039989-JUJNBM96FYB20S0OXO63/2019-05-13_08-56-39.png?format=1000w" width="663" height="234" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763039989-JUJNBM96FYB20S0OXO63/2019-05-13_08-56-39.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763039989-JUJNBM96FYB20S0OXO63/2019-05-13_08-56-39.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763039989-JUJNBM96FYB20S0OXO63/2019-05-13_08-56-39.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763039989-JUJNBM96FYB20S0OXO63/2019-05-13_08-56-39.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763039989-JUJNBM96FYB20S0OXO63/2019-05-13_08-56-39.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763039989-JUJNBM96FYB20S0OXO63/2019-05-13_08-56-39.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557763039989-JUJNBM96FYB20S0OXO63/2019-05-13_08-56-39.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">FixedUpdate timestep at 60FPS, Update timestep at 60FPS</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Much better!</p><p class="">Fundamentally, there’s no way to run a 60 FPS Update and a 50 FPS FixedUpdate without incurring <strong>some</strong> artifacts. This is a sampling problem, and you have to pick your poison. The best option is to do your best to keep your FixedUpdate running close to your render timestep. Failing that, interpolation may be worth investigating.</p><p class="">If you take one thing away from this article, I would consider setting your Time.fixedDeltaTime to 1/60f. </p><h2>A Heavy Load in FixedUpdate</h2><p class="">Up to this point we’ve been dealing with Unity at its most performant. However, this is rarely the case in production. We’ve said that the FixedUpdate is <strong>guaranteed </strong>to run every 0.02 game-time seconds. What happens when the FixedUpdate pass takes longer than 0.02 wall-time seconds to run? In this case, by the time a single FixedUpdate runs, even more wall-time seconds have passed and it will need to run again! It will never catch up. Instead, Unity chooses to just slow down the overall <em>game-time</em> itself.</p><p class="">The following setup spends 33ms in every FixedUpdate, causing the game to take only 30 FixedUpdates per second. Thus, game-time progresses forward 0.6 seconds for every 1 wall-time second<a href="#margin"> (30 FPS / 50 FPS) = 0.6</a>.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557846510375-VCPJUB4SRX2TCZ22A3XM/Unity_2019-05-14_08-08-15.png" data-image-dimensions="687x233" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557846510375-VCPJUB4SRX2TCZ22A3XM/Unity_2019-05-14_08-08-15.png?format=1000w" width="687" height="233" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557846510375-VCPJUB4SRX2TCZ22A3XM/Unity_2019-05-14_08-08-15.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557846510375-VCPJUB4SRX2TCZ22A3XM/Unity_2019-05-14_08-08-15.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557846510375-VCPJUB4SRX2TCZ22A3XM/Unity_2019-05-14_08-08-15.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557846510375-VCPJUB4SRX2TCZ22A3XM/Unity_2019-05-14_08-08-15.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557846510375-VCPJUB4SRX2TCZ22A3XM/Unity_2019-05-14_08-08-15.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557846510375-VCPJUB4SRX2TCZ22A3XM/Unity_2019-05-14_08-08-15.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557846510375-VCPJUB4SRX2TCZ22A3XM/Unity_2019-05-14_08-08-15.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Notice here that Update is also lowered in frequency, even though the Update pass (and rendering) has no load! Even though the FixedUpdate is updating at 30 FPS, the game is only being rendered at a measly 6 FPS. You might expect that rendering would run once every FixedUpdate, but that’s not what happens. The reason behind this is due to Unity’s ‘catch-up’ mechanism. </p><p class="">When the Update perceives that game-time is running behind wall-time, it will run the FixedUpdate as fast as possible until it catches back up. This is sensible when your game is largely render-blocked (as FixedUpdates will be relatively quick). However, in this case, because the FixedUpdate is too slow to run at frame-rate, Unity would end up running FixedUpdates forever, doomed to never catch up.</p><p class="">Instead, Unity provides a ‘bail out’ setting: Time.maximumDeltaTime. After a certain amount of time spent of “catching up” on FixedUpdates, it will bail out to finish the frame before running any more. By default this value is set to 0.1 seconds (wall-time). This explains the 5 FixedUpdates between each render. If we like, we can lower this value down, such that it always bails out after one FixedUpdate. Here is the same scene with a setting of 0.02.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557848938866-61PCHX2VMFAQHMWEQOZ3/Unity_2019-05-14_08-48-52.png" data-image-dimensions="637x232" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557848938866-61PCHX2VMFAQHMWEQOZ3/Unity_2019-05-14_08-48-52.png?format=1000w" width="637" height="232" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557848938866-61PCHX2VMFAQHMWEQOZ3/Unity_2019-05-14_08-48-52.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557848938866-61PCHX2VMFAQHMWEQOZ3/Unity_2019-05-14_08-48-52.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557848938866-61PCHX2VMFAQHMWEQOZ3/Unity_2019-05-14_08-48-52.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557848938866-61PCHX2VMFAQHMWEQOZ3/Unity_2019-05-14_08-48-52.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557848938866-61PCHX2VMFAQHMWEQOZ3/Unity_2019-05-14_08-48-52.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557848938866-61PCHX2VMFAQHMWEQOZ3/Unity_2019-05-14_08-48-52.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557848938866-61PCHX2VMFAQHMWEQOZ3/Unity_2019-05-14_08-48-52.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class=""><br>Much smoother. Now there’s one Update for every FixedUpdate (no sampling issues). Both run at 30 FPS. Unfortunately, tuning this value is tricky and depends on the game. Set it too high and you can see lower frame-rates than necessary. If you set it too low, the game could get bottlenecked rendering all of those extra frames (ie. Unity isn’t able to skip rendering to help catch up). </p><p class="">One more thing. In this last case, because the game is running slower than real-time, Unity is passing a Time.deltaTime of 0.02 game-time seconds to the Update function. This is a different value than the actual wall-clock frame time! Each Update pass actually spans about 0.033 seconds, but because the game as a whole has been slowed down, the deltaTime is scaled down as well. Try your best not to think of Update’s deltaTime as “the time between frames”. In many situations that’s not correct — more correctly it is the <em>game-time</em> between frames.</p><p class="">Slowing down is probably the best thing that Unity can do in this situation. Without dynamically changing the FixedUpdate timestep, there’s just no way to make the game do 0.033 seconds of work in 0.02 seconds. The lesson here is to be careful about the amount of work placed in the FixedUpdate (ie. Physics, Game State logic). A physics heavy scene will slow down the game, regardless of the ability of Unity to render at a dynamic frame rate.</p><h2>A Heavy Load in Update</h2><p class="">What if, instead, we are render-blocked or blocked on heavy animation / skinning on the CPU? In this case, the Update function will be slow. Here, I have set the Update function to run at 10 FPS with a 100ms second load. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557841828984-U1H69PMABRCBS92NV16J/Unity_2019-05-14_06-49-06.png" data-image-dimensions="540x232" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557841828984-U1H69PMABRCBS92NV16J/Unity_2019-05-14_06-49-06.png?format=1000w" width="540" height="232" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557841828984-U1H69PMABRCBS92NV16J/Unity_2019-05-14_06-49-06.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557841828984-U1H69PMABRCBS92NV16J/Unity_2019-05-14_06-49-06.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557841828984-U1H69PMABRCBS92NV16J/Unity_2019-05-14_06-49-06.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557841828984-U1H69PMABRCBS92NV16J/Unity_2019-05-14_06-49-06.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557841828984-U1H69PMABRCBS92NV16J/Unity_2019-05-14_06-49-06.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557841828984-U1H69PMABRCBS92NV16J/Unity_2019-05-14_06-49-06.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557841828984-U1H69PMABRCBS92NV16J/Unity_2019-05-14_06-49-06.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">This result is a bit odd. We can count 10 Update passes for every wall-time second — this checks out. However, at first glance it seems like the FixedUpdate is only being run 10 times a second as well. If this were true, then the game physics would be proceeding at 1/5th the speed! If we zoom in, the reality becomes more clear. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557842161763-KOG3HTDRE0HOYCYCUULK/Unity_2019-05-14_06-55-49.png" data-image-dimensions="649x149" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557842161763-KOG3HTDRE0HOYCYCUULK/Unity_2019-05-14_06-55-49.png?format=1000w" width="649" height="149" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557842161763-KOG3HTDRE0HOYCYCUULK/Unity_2019-05-14_06-55-49.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557842161763-KOG3HTDRE0HOYCYCUULK/Unity_2019-05-14_06-55-49.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557842161763-KOG3HTDRE0HOYCYCUULK/Unity_2019-05-14_06-55-49.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557842161763-KOG3HTDRE0HOYCYCUULK/Unity_2019-05-14_06-55-49.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557842161763-KOG3HTDRE0HOYCYCUULK/Unity_2019-05-14_06-55-49.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557842161763-KOG3HTDRE0HOYCYCUULK/Unity_2019-05-14_06-55-49.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1557842161763-KOG3HTDRE0HOYCYCUULK/Unity_2019-05-14_06-55-49.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Because our FixedUpdate is so fast, after every 100ms frame, the next 5 FixedUpdates execute immediately, before starting on another heavy Update pass. In this situation, the game-time <em>doesn’t</em> slow down, relative to wall-clock time. Instead, game time is shrunken and expanded! </p><p class="">Imagine if you read the wall-time value of Time.realtimeSinceStartup in the FixedUpdate — it would be extremely incorrect. This is a strong reason to keep wall-clock time out of your game calculations. Unity plays around with the way game-time flows depending on the load. You want your game to simulate nicely even when game-time is being stretched.</p><h2>Conclusion</h2><p class="">Let’s take stock:</p><ul data-rte-list="default"><li><p class="">Game-time can drastically differ from wall-clock time, so avoid using the latter. </p></li><li><p class="">Make use of Time.fixedDeltaTime and Time.maximumDeltaTime to tune the specific performance characteristics of your game. These can all be changed at runtime, too!</p></li><li><p class="">Stuttering can occur even with an empty load on the engine. </p></li><li><p class="">Be careful of sampling issues. Debug your frame timesteps to see what is actually happening.</p></li></ul><p class="">Further, I showed the behavior of the timestep in isolated cases, but in reality all of these situations overlap. A single long Update could trigger a series of longer FixedUpdate steps, causing the Update to slow down, etc. It’s one big feedback loop. Hopefully, though, this gives you some intuition behind it.</p><p class="">Thanks for reading! Drop me a line on <a href="http://twitter.com/Kleptine">Twitter</a> if you have questions or if you have information to add. I’d love to make this article a one-stop-shop for timestep information. Additionally, if you think I’ve gotten something wrong, please let me know! </p><h2>FAQ</h2><p class="">Finally, let’s answer a few frequent questions. Perhaps you’ve come here from Google just for these (I suggest reading the above article for context).</p><h3><a href="#anchor">#</a> Help! My game is stuttering!</h3><p class="">The first thing to do is figure out <strong>why</strong> your game is stuttering. Stutter can be a result of mismatched timesteps (see first section) or could be from a variety of different performance issues (your update or fixed update are too long and Unity stutters to compensate in other ways). Drop a few console lines, or graph out the data on when your frames are rendered, and then see what looks wrong compared to the graphs above. </p><h3><a href="#anchor">#</a> Is FixedUpdate always called every 0.02 seconds?</h3><p class="">No, FixedUpdate will run once for every 0.02 <em>game-time</em> seconds, but precisely when in real time this happens is unspecified and can vary <em>drastically. </em>See above: “A Heavy Load in FixedUpdate”.</p><h3><a href="#anchor">#</a> How does the Unity timestep respond to low framerates?</h3><p class="">Depending on the source of the slowness, it will do different things. For slow FixedUpdates, it slows down the overall game-time (the game will run for 0.5 seconds in the span of one real time second). In some cases, the rendering rate can be drastically cut, even if the problem is your FixedUpdate speed. </p><p class="">For slow Updates, FixedUpdates get crunched together during the “catch up” phase between the longer Updates. Otherwise, generally the the Update dynamic timestep just gets longer.</p><h3><a href="#anchor">#</a> What is the value of Time.deltaTime in the Update function?</h3><p class="">You can generally think of it as the difference in <em>game-time </em>between frames being rendered to the screen. When at performance, this is the same as the wall time between frames. However, if the game slows down because of FixedUpdate lag, Time.deltaTime will shrink, even if the render frame rate remains the same! Weird!</p><h3><a href="#anchor">#</a> Should I put my code in FixedUpdate or Update?</h3><p class="">This is a subtle question, with no correct answer, unfortunately. Code in FixedUpdate means your game is deterministic, but FixedUpdate can’t adapt to varying framerates. Under load, your game will <em>slow down. </em>You must be much more careful that FixedUpdate can run at display frame rate. </p><p class="">If you favor determinism and systematic stability, go for FixedUpdate. If you don’t want to worry about it too terribly much, go with the default Unity approach of putting physics-related code in FixedUpdate and the rest in Update.</p><p class="">I wrote up my opinions in detail in a Reddit comment with <a href="https://www.reddit.com/r/gamedev/comments/bu0l6d/fix_your_unity_timestep_everything_you_need_to/ep5jrq1/">two different takes on the matter.</a></p><h3><a href="#anchor">#</a> Does physics have to run in FixedUpdate?</h3><p class="">Unity provides the ability to <a href="https://docs.unity3d.com/ScriptReference/Physics-autoSimulation.html">remove the physics update</a> from FixedUpdate, letting you<a href="https://docs.unity3d.com/ScriptReference/Physics.Simulate.html"> manually step the physics engine</a> on your own time. This is great, but probably should be used with caution. It is a non-standard approach, and may play oddly with plugins and other engine features.</p><p class="">On top of this, Unity now supports <a href="https://blogs.unity3d.com/2018/11/12/physics-changes-in-unity-2018-3-beta/">the ability</a> to have separate physics worlds. This lets you simulate an entirely separate physics system outside of the game loop. </p><p class="">If you’re reading this from the future, you may also be interested in Unity’s new <a href="https://blogs.unity3d.com/2019/03/19/announcing-unity-and-havok-physics-for-dots/">Havok and DOTS Physics</a> systems.</p><h3><a href="#anchor">#</a> Where can I learn more about time steps?</h3><p class="">This article is built upon the shoulders of others. Chief among them is the <a href="http://vodacek.zvb.cz/archiv/681.html">Gaffer on Games: Fix Your Timestep</a> article. See Google for a cached link if it goes down. Additional sources, in no particular order:</p><ul data-rte-list="default"><li><p class="">The Unity manual for <a href="https://docs.unity3d.com/Manual/TimeFrameManagement.html">Time Management</a> and <a href="https://docs.unity3d.com/Manual/ExecutionOrder.html">Game Loop Lifecycle</a>.</p></li><li><p class="">The <a href="https://gameprogrammingpatterns.com/game-loop.html">Game Loop</a> entry in Game Programming Patterns — a nice continuation after reading Gaffer</p></li><li><p class=""><a href="http://lspiroengine.com/?p=378">Fixed Time Stepping</a> for the L Spiro engine</p></li><li><p class=""><a href="https://www.gamedev.net/blogs/entry/2265460-fixing-your-timestep-and-evaluating-godot/">A great article on Gamedev.net </a>by lawnjelly</p></li><li><p class="">Scott Sewell’s article on <a href="http://www.kinematicsoup.com/news/2016/8/9/rrypp5tkubynjwxhxjzd42s3o034o8">Achieving Smooth Motion in Unity</a></p></li></ul><p class="">Finally, you can reach out to me on <a href="http://twitter.com/kleptine">Twitter</a> if you have any questions.</p>]]></description></item><item><title>Florence as Interactive Metaphor</title><category>Essay</category><dc:creator>John Austin</dc:creator><pubDate>Sat, 20 Apr 2019 18:01:36 +0000</pubDate><link>https://johnaustin.io/articles/2019/florence-as-interactive-metaphor</link><guid isPermaLink="false">5b1340bcee17595af30b6b97:5b7cbb5f40ec9a4b7342c8a9:5cbb5461038c49000190d28f</guid><description><![CDATA[<figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782791738-AQSFSKH44KRU84GFTV0N/Q4e_nu3RXlHrDOWWOZHz7RRERRnUX4FbNwgRjBpzOmg.jpg" data-image-dimensions="1200x628" data-image-focal-point="0.4938181464174455,0.0" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782791738-AQSFSKH44KRU84GFTV0N/Q4e_nu3RXlHrDOWWOZHz7RRERRnUX4FbNwgRjBpzOmg.jpg?format=1000w" width="1200" height="628" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782791738-AQSFSKH44KRU84GFTV0N/Q4e_nu3RXlHrDOWWOZHz7RRERRnUX4FbNwgRjBpzOmg.jpg?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782791738-AQSFSKH44KRU84GFTV0N/Q4e_nu3RXlHrDOWWOZHz7RRERRnUX4FbNwgRjBpzOmg.jpg?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782791738-AQSFSKH44KRU84GFTV0N/Q4e_nu3RXlHrDOWWOZHz7RRERRnUX4FbNwgRjBpzOmg.jpg?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782791738-AQSFSKH44KRU84GFTV0N/Q4e_nu3RXlHrDOWWOZHz7RRERRnUX4FbNwgRjBpzOmg.jpg?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782791738-AQSFSKH44KRU84GFTV0N/Q4e_nu3RXlHrDOWWOZHz7RRERRnUX4FbNwgRjBpzOmg.jpg?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782791738-AQSFSKH44KRU84GFTV0N/Q4e_nu3RXlHrDOWWOZHz7RRERRnUX4FbNwgRjBpzOmg.jpg?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782791738-AQSFSKH44KRU84GFTV0N/Q4e_nu3RXlHrDOWWOZHz7RRERRnUX4FbNwgRjBpzOmg.jpg?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">There is a category of games that I’ve seen growing over the past couple of years. These games occupy multiple genres: they are primarily narrative-driven, but borrow mechanics and structures from many ‘loaner’ genres. These loaner genres can be anything, but often fall in the category of puzzle, action, or rhythm. Notably, however, they rarely achieve any of the goals of their loaner genres. Florence is not a puzzle game: every task can be solved by ‘fiddling’. Neither is it an action game: all timing or response based challenges can be completely ignored and taken at your own pace.</p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782641643-80X2VK9W0115CQURNZC8/chrome_2019-04-20_10-49-51.jpg" data-image-dimensions="898x1597" data-image-focal-point="0.5131316489361702,0.8630952380952381" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782641643-80X2VK9W0115CQURNZC8/chrome_2019-04-20_10-49-51.jpg?format=1000w" width="898" height="1597" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782641643-80X2VK9W0115CQURNZC8/chrome_2019-04-20_10-49-51.jpg?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782641643-80X2VK9W0115CQURNZC8/chrome_2019-04-20_10-49-51.jpg?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782641643-80X2VK9W0115CQURNZC8/chrome_2019-04-20_10-49-51.jpg?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782641643-80X2VK9W0115CQURNZC8/chrome_2019-04-20_10-49-51.jpg?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782641643-80X2VK9W0115CQURNZC8/chrome_2019-04-20_10-49-51.jpg?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782641643-80X2VK9W0115CQURNZC8/chrome_2019-04-20_10-49-51.jpg?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782641643-80X2VK9W0115CQURNZC8/chrome_2019-04-20_10-49-51.jpg?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">Florence solves the puzzle of her job.</p>
+ </figcaption>
+
+
+ </figure>
+
+
+
+
+
+
+
+
+
+
+
+
+ <p class="">Instead, these games ‘loan’ mechanics from others. They borrow the bare-bones mechanisms and in doing so borrow a <strong>feeling</strong>. Take, for example, the spreadsheet ‘puzzle’ in Florence that happens early on in the game. Florence is frustrated and bored by a job that is uninteresting, but must be completed. The interaction presents a system that is unintelligible at first (the rules are unclear), followed by a methodical and fairly uninteresting elimination of information (finding similar numbers). </p><p class="">Both of these feelings (confusion and methodical practice) are strongly present in the puzzle genre. However, Florence does not support these borrowed feelings with the variety of other emotions and structures present in most puzzle games. Normally, the confusion gives way to clarity and analysis, and the tedium shrinks as puzzles become more challenging. By choosing to keep only the initial structures of the puzzle genre, Florence also keeps these initial feelings as well. Metaphorically these are the feelings that map to Florence’s own struggle.</p><p class="">We, as humans, are well conditioned. When we hear music, we are instantly reminded of the feelings (either good or bad) that we have associated with this music in the past. Play someone a song and you drive emotion: a gut memory about a time and place, a loathing for a certain style, or the apathy of mild distaste. Games do this too. When a player is presented with a puzzle structure, this creates emotions. For puzzle lovers, it may be the excitement of seeing a new system to solve. For puzzle loathers it may be the exhaustion and annoyance of having to figure it out. Even non-players have experienced puzzle structures in their lives, and have associated feelings and reactions. </p><p class="">In Florence, the puzzle structures do not represent themselves. They are metaphors for the feelings and associations we’ve built for them. </p><p class="">We tend to label these games as “Narrative Games”, however this dismisses the role of these borrowed mechanics. A game like <em>Florence</em> or <em>What Remains of Edith Finch </em>has few meaningful narrative choices. Rather, it is these <strong>loaner mechanics</strong> that are used to built the core emotion of the game. </p><p class="">It isn’t an “Interactive Narrative”. It is an “Interactive Metaphor”, wrapped with a story. </p>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <figure class="
+ sqs-block-image-figure
+ intrinsic
+ "
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782982225-QUOSALI28505S7AR8ONQ/image-asset.jpeg" data-image-dimensions="1200x675" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782982225-QUOSALI28505S7AR8ONQ/image-asset.jpeg?format=1000w" width="1200" height="675" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add("loaded")" srcset="https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782982225-QUOSALI28505S7AR8ONQ/image-asset.jpeg?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782982225-QUOSALI28505S7AR8ONQ/image-asset.jpeg?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782982225-QUOSALI28505S7AR8ONQ/image-asset.jpeg?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782982225-QUOSALI28505S7AR8ONQ/image-asset.jpeg?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782982225-QUOSALI28505S7AR8ONQ/image-asset.jpeg?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782982225-QUOSALI28505S7AR8ONQ/image-asset.jpeg?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5b1340bcee17595af30b6b97/1555782982225-QUOSALI28505S7AR8ONQ/image-asset.jpeg?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">
+
+
+
+
+
+
+
+
+
+ <figcaption class="image-caption-wrapper">
+ <p class="">“Interactive Metaphor”: balancing fantasy and mundanity in <em>What Remains of Edith Finch.</em></p>
+ </figcaption>
+
+
+ </figure>]]></description></item></channel></rss>
+\ No newline at end of file