<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: KamilVDono</title>
    <description>The latest articles on Forem by KamilVDono (@kamilvdono).</description>
    <link>https://forem.com/kamilvdono</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F283423%2F8988f53f-740d-48fb-9be0-8baa61959a37.png</url>
      <title>Forem: KamilVDono</title>
      <link>https://forem.com/kamilvdono</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kamilvdono"/>
    <language>en</language>
    <item>
      <title>Deep dive Tainted Grail [4] - MipMaps streaming</title>
      <dc:creator>KamilVDono</dc:creator>
      <pubDate>Sun, 21 Dec 2025 12:32:14 +0000</pubDate>
      <link>https://forem.com/kamilvdono/deep-dive-tainted-grail-4-mipmaps-streaming-1aj3</link>
      <guid>https://forem.com/kamilvdono/deep-dive-tainted-grail-4-mipmaps-streaming-1aj3</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What it is?
&lt;/h3&gt;

&lt;p&gt;A texture can have mipmaps. Mipmap level 0 is the original texture, level 1 is downscaled by a factor of 2, level 2 by a factor of 4, and so on.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9bj2prrzbnixhgmdkf2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9bj2prrzbnixhgmdkf2w.png" alt="MipMaps example" width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mipmaps are generated so that far textures can sample higher mip levels, which means a more pleasant visual output and cheaper sampling. &lt;a href="https://en.wikipedia.org/wiki/Mipmap#/media/File:Mipmap_Aliasing_Comparison.png" rel="noopener noreferrer"&gt;Wikipedia has a very nice example&lt;/a&gt; of that phenomenon, as well as the &lt;a href="https://gdbooks.gitbooks.io/legacyopengl/content/Chapter7/Mip.html" rel="noopener noreferrer"&gt;OpenGL GitBook&lt;/a&gt;.&lt;br&gt;
The downside is that we need to use more memory:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3kzkish84g17smja8gpm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3kzkish84g17smja8gpm.png" alt="Texture sizes table" width="632" height="387"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why use it?
&lt;/h3&gt;

&lt;p&gt;Most of the time we don't render textures with mip 0, which means we don't need it in memory. That means almost 50% memory reduction per texture. But further mips like mip 1, mip 2, and so on aren't always needed either. That means even more frugal textures.&lt;br&gt;
&lt;em&gt;Screenshots are from the editor. &lt;code&gt;Total texture memory&lt;/code&gt; in the editor is wrong because in the editor Unity keeps track of all ever-loaded textures and takes them into account for that stat.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here are stats of a working system. You can see how many textures are at which mip level. There is 292.4MB of non-streaming textures and 1.2GB overall memory usage, therefore 1.0GB is from streamed textures.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg1wbhnuvnj9lk1hvdqmx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg1wbhnuvnj9lk1hvdqmx.png" alt="MipMaps streaming stats" width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If I force textures to be at mip 0, then memory usage goes up to 3.4GB.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fll6gtv76z06hfj70yhlp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fll6gtv76z06hfj70yhlp.png" alt="MipMaps streaming stats - forced mip 0" width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Forced mip 1 results in memory pressure at the 1.1GB level, but makes textures blurred. It may not be very visible in this screenshot, but it's very visible for others scenarios.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1pl8udds6gj27g5xxvf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1pl8udds6gj27g5xxvf.png" alt="MipMaps streaming stats - forced mip 1" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Forced mip 5 reduces textures to occupy 299.8MB. If you subtract 292.4MB of non-streamed textures, then you see that 1667 textures take just 7.4MB. &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgi2bin6idcxbe1f0i0ww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgi2bin6idcxbe1f0i0ww.png" alt="MipMaps streaming stats - forced mip 5" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a small table with a summary (by accident I moved a bit, so measurements are a bit different):&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmbz9401qqyjd8zm8gz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmbz9401qqyjd8zm8gz6.png" alt="MipMaps memory table" width="643" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More examples can be found at the end of this article.&lt;/p&gt;

&lt;p&gt;So as you can see, there are massive savings to achieve here.&lt;/p&gt;
&lt;h3&gt;
  
  
  Other considerations
&lt;/h3&gt;

&lt;p&gt;The mip level to sample is calculated by the GPU when sampling is performed. Therefore the CPU doesn't know which mip level is needed. To obtain that knowledge we need to calculate it ourselves. For every texture we need to calculate the minimum mip level (mip 0 is original resolution) and either load it if not present (along with all higher levels) or unload unused levels (that step could be skipped if we are under the budget).&lt;/p&gt;

&lt;p&gt;That means CPU overhead. Mip level approximation is calculated from UV distribution, distance to camera, and camera setup. That indicates potentially different mip levels for every renderer.&lt;/p&gt;
&lt;h3&gt;
  
  
  Unity's out of box
&lt;/h3&gt;

&lt;p&gt;Unity inside its engine code has support for CPU mip level approximation for MeshRenderers and SkinnedMeshRenderers. For unknown reasons it's very slow, so Unity implemented sparse updates, so only &lt;a href="https://docs.unity3d.com/ScriptReference/QualitySettings-streamingMipmapsRenderersPerFrame.html" rel="noopener noreferrer"&gt;X renderers are processed per frame&lt;/a&gt;.&lt;br&gt;
It's hidden from C# view. That means you can just override an unknown internal value. That means you can either use the texture as is and stick to vanilla renderers, or fully reimplement it yourself.&lt;/p&gt;

&lt;p&gt;As you know, we have custom rendering solutions (also the ECS way where there is no mipmaps streaming implemented by Unity – really good job), which indicates we must implement mipmaps streaming (the CPU level calculations and requests part of it; the GPU resources handling is still on Unity's side and cannot be reimplemented) on our own.&lt;/p&gt;
&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;p&gt;The first requirements were as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Must be fast&lt;/li&gt;
&lt;li&gt;Avoid runtime allocations&lt;/li&gt;
&lt;li&gt;Debug capabilities:

&lt;ul&gt;
&lt;li&gt;Check when a texture got blurred&lt;/li&gt;
&lt;li&gt;Detect textures which should be streamed but are misconfigured&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the first version more requirements appeared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The system must have universal inputs, because Drake, Leshy, Medusa, and HLODs must have support for mipmaps streaming.&lt;/li&gt;
&lt;li&gt;Easily expandable as more systems may be introduced (and Kandra was added after that point, and the connection between both systems was done in a few lines of code)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Implementations
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Version 1
&lt;/h3&gt;

&lt;p&gt;At that time most of the data was held by ECS, so it felt natural to implement it as a System, but at that point we were still using Entities and Entities Graphics as external packages. That meant we needed to duplicate some code related to material registration. Extracting textures from a material is a very costly operation (Unity's shaders and materials pipeline is very sluggish), but slicing in an ECS system is very hard and odd. We needed to maintain multiple caches for materials, textures, refcounts, and mappings.&lt;/p&gt;

&lt;p&gt;Duplicated code means unnecessary slowdowns.&lt;br&gt;
A complex and performant caching strategy means giga-complex code, a debugging nightmare, and extending headaches.&lt;br&gt;
Systems are more or less singletons, but still, accessing them and storing data feels awkward.&lt;/p&gt;

&lt;p&gt;It wasn't impossible to maintain and plug new rendering systems into this version, but it was a long process.&lt;/p&gt;

&lt;p&gt;Debugging capabilities were also nearly non-existent.&lt;/p&gt;
&lt;h3&gt;
  
  
  Version 2
&lt;/h3&gt;

&lt;p&gt;As I described, there were problems with the first version as it was very rigid and error-prone. Therefore we added requirements that it must be easily expandable to plug in other systems or make changes or fixes.&lt;br&gt;
To simplify the whole code, a small layer of indirection was introduced.&lt;br&gt;
System can register a Material and &lt;code&gt;Material_MipMaps&lt;/code&gt; will take care of tracking textures of that Material. Then it's expected that in a background thread that System will provide a &lt;code&gt;Mip Factor&lt;/code&gt; associated with &lt;code&gt;MaterialHandle&lt;/code&gt;.&lt;br&gt;
For any Material, factors are gathered and &lt;code&gt;Interlocked.CompareExchange&lt;/code&gt; is used in a way that we store the min factor.&lt;br&gt;
Then the Textures part of the system takes it and applies similar logic to textures extracted from registered Materials.&lt;/p&gt;

&lt;p&gt;You can see that it is very easy to add a new system – just look at the Kandra example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BurstCompile&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;KandraMipmapsFactorJob&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IJobFor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CameraData&lt;/span&gt; &lt;span class="n"&gt;cameraData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnsafeBitmask&lt;/span&gt; &lt;span class="n"&gt;takenSlots&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnsafeBitmask&lt;/span&gt; &lt;span class="n"&gt;toUnregister&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnsafeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnsafeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ys&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnsafeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;zs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnsafeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;radii&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnsafeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;float4x4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rootBoneMatrices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnsafeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reciprocalUvDistributions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UnsafeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UnsafeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MipmapsStreamingMasterMaterials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaterialId&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;materialIndices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;MipmapsStreamingMasterMaterials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParallelWriter&lt;/span&gt; &lt;span class="n"&gt;outMipmapsStreamingWriter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;uIndex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;takenSlots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;toUnregister&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uIndex&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;float3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;zs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uIndex&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;radii&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;square&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rootBoneMatrices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uIndex&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;Scale&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;factorFactor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MipmapsStreamingUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CalculateMipmapFactorFactor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cameraData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fullFactor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reciprocalUvDistributions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;factorFactor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;subMaterialIndices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;materialIndices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;subMaterialIndices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;outMipmapsStreamingWriter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UpdateMipFactor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subMaterialIndices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;fullFactor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are still caveats. The biggest is that you can (Un-)Register a Material mostly during the Update phase. During EarlyUpdate we extract textures if there are newly added Materials. PreLateUpdate: jobs, like the one above, are started. Finally, PostLateUpdate: jobs are completed and per-texture requested levels are updated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Mipmaps streaming is a very important technique to lower memory pressure, which is even more important in Unity as it has big memory pressure to start with. Virtual Textures would be better and more impactful, but there is no real solution from the Unity side, and we are not brave enough to do it ourselves (or adopt any solution from GitHub).&lt;/p&gt;

&lt;p&gt;Overall, by making the code simpler I was able to make it faster too. A bit of indirection (as opposed to abstraction) makes it easier to think and reason about the topic, and from thinking and reasoning the best optimizations are born.&lt;br&gt;
When designing architecture, be aware of the mental map/image of the tools you are about to use. An ECS System may not be far from a Singleton-like System, nevertheless it could lead to very different architectures – one feeling like a nightmare while the other more like a dream.&lt;/p&gt;

&lt;h2&gt;
  
  
  More examples of stats
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Act I
&lt;/h3&gt;

&lt;p&gt;Starting beach:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftl0xoti2uld7fun0j8si.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftl0xoti2uld7fun0j8si.png" alt="MipMaps stats - Starting beach" width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Graveyard:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fei5o8k7l2zpodx3d076e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fei5o8k7l2zpodx3d076e.png" alt="MipMaps stats - Crypt graveyard" width="800" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under bridge:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftuqvujt36j0mo1ywil50.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftuqvujt36j0mo1ywil50.png" alt="MipMaps stats - Under bridge" width="800" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Act II
&lt;/h3&gt;

&lt;p&gt;Wyrd-tower:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4nc8l4qlw6epof4e33l6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4nc8l4qlw6epof4e33l6.png" alt="MipMaps stats - Wyrd-tower" width="800" height="649"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Farmlands:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0d9z12909pb6u3hwfuzm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0d9z12909pb6u3hwfuzm.png" alt="MipMaps stats - Farmlands" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Inside interior (overworld is loaded in background):&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7qghgre74mrylglw2nu1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7qghgre74mrylglw2nu1.png" alt="MipMaps stats - Interior" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cuanacht city:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffooywczpumh6t62pdy64.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffooywczpumh6t62pdy64.png" alt="MipMaps stats - Cuanacht city" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Act II
&lt;/h3&gt;

&lt;p&gt;Village:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd78gkyzaxplv1kfwto52.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd78gkyzaxplv1kfwto52.png" alt="MipMaps stats - Village" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Capitol:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm03l1tqcynqzhu3cvrvy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm03l1tqcynqzhu3cvrvy.png" alt="MipMaps stats - Capitol" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Random nowhere:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F17y7r7rvy7rk3zbwubys.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F17y7r7rvy7rk3zbwubys.png" alt="MipMaps stats - Nowhere" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>optimization</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Deep dive Tainted Grail [3] - Medusa - blazing fast cliffs rendering</title>
      <dc:creator>KamilVDono</dc:creator>
      <pubDate>Sun, 19 Oct 2025 14:50:14 +0000</pubDate>
      <link>https://forem.com/kamilvdono/deep-dive-tainted-grail-3-medusa-blazing-fast-cliffs-rendering-4pi1</link>
      <guid>https://forem.com/kamilvdono/deep-dive-tainted-grail-3-medusa-blazing-fast-cliffs-rendering-4pi1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Name
&lt;/h3&gt;

&lt;p&gt;In Greek mythology, Medusa is an entity which possesses the ability to turn others into stone. Such a nice name for a system which was created to render cliffs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0ds3y5p94quxuj4b8d8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0ds3y5p94quxuj4b8d8.png" alt="Medusa from mythology image" width="728" height="549"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Application
&lt;/h3&gt;

&lt;p&gt;As mentioned above, Medusa was created to accelerate the rendering of cliffs, later rephrased as a very CPU- and memory-friendly way of rendering always visible and fully static meshes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why new system
&lt;/h2&gt;

&lt;p&gt;As you know from previous posts we already have &lt;a href="https://dev.to/kamilvdono/deep-dive-tainted-grail-1-drake-runtime-entity-renderer-registration-system-1751"&gt;Drake&lt;/a&gt; and &lt;a href="https://dev.to/kamilvdono/deep-dive-tainted-grail-2-leshy-vegetation-streaming-and-rendering-4ndk"&gt;Leshy&lt;/a&gt;. Why not to use them?&lt;/p&gt;

&lt;h3&gt;
  
  
  Drake
&lt;/h3&gt;

&lt;p&gt;Drake uses a slightly modified Entities Graphics for rendering, which means you need to create an entity for each renderer, keep all data on the CPU, and every frame the data is sent to the GPU. There is also binning code, chunk upkeep costs, and other parts which make Entities Graphics and Entities themselves a very generic solution.&lt;/p&gt;

&lt;p&gt;Medusa is the opposite of that; it's highly specialized. All data is baked into a file and, at scene startup, loaded into the CPU and sent a single time to the GPU buffers. Meshes and materials are directly referenced. There is no support for data modification.&lt;/p&gt;

&lt;p&gt;Another significant difference is frustum culling. Medusa uses sphere-vs-frustum instead of the traditional box-vs-frustum used by Entities Graphics. All of this makes it very CPU-friendly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leshy
&lt;/h3&gt;

&lt;p&gt;Leshy is a very nice system, but for cliffs, we need colliders to be present at a much larger distance than is required for Leshy. Also, cliffs are a landscaping tool, so they must be visible from a very, very long distance, meaning cell streaming from Leshy is not needed here. The last requirement is to not change the level designers' workflow; therefore, authoring must be done manually with &lt;code&gt;MeshRenderers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From this, we can see that Medusa only requires the rendering part from Leshy.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Colliders
&lt;/h3&gt;

&lt;p&gt;Colliders are always present for the scene; honestly, nothing changes here - we left them unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rendering
&lt;/h3&gt;

&lt;p&gt;Based on experience (and code) from Leshy, Medusa uses BRG with a few optimizations. The GPU side is immutable for a scene; once created at scene start, it persists with the same data and layout until scene unloading (when Medusa disposes). This means buffers are ideally linear and fully utilized. Batches are also ideal; for a given submesh, there is always just a single draw call.&lt;/p&gt;

&lt;p&gt;Frustum culling is a highly optimized (SIMD in a multithreaded environment) sphere-vs-frustum, with no need for chunking, preprocessing, or any other fancy technique. This is because it's so fast that the entire cost of frustum culling and draw call preparation is hidden by HDRP's render graph main thread interactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Baking
&lt;/h3&gt;

&lt;p&gt;As already mentioned, Medusa's objects are just plain Unity rendering primitives (LODGroup + MeshRenderers), so that data must be baked into an optimized data structure, which can be pushed to BRG without wasting user resources. It's also important to have the same data and representation in the final build as well as in Play Mode. To achieve this behavior, we used &lt;code&gt;IProcessSceneWithReport&lt;/code&gt;, which is super useful for ensuring consistency and parity between Play Mode and build, but it must be used carefully. Long baking times not only slow down releases but also affect the day-to-day work of everyone who needs to quickly check changes in Play Mode.&lt;/p&gt;

&lt;p&gt;If you have a single system that needs to be baked and baking takes 1 second, then it's not a big deal. If you have 15 systems where each bakes in 1 second, that's 15 seconds of idle time for every entry into Play Mode. That adds up quickly: 15 seconds * 10 entries per hour * 8 hours is 20 minutes; that means for every 24 people in the team, one person-day is just idle waiting for Play Mode.&lt;/p&gt;

&lt;p&gt;The data is baked into a separate file in &lt;code&gt;StreamingAssets&lt;/code&gt;, from where it will be loaded via &lt;code&gt;AsyncReadManager&lt;/code&gt;, a fast and parallel file buffer reading API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;That was a short one, but its brevity shows the strength of experience and solid foundations.&lt;/p&gt;

&lt;p&gt;Reuse of already working, established, and well-known systems accelerates progress greatly. I would advise you to copy-paste relevant code and make modifications without haste. After that, compare the source system(s) and the new one to generalize where needed and possible.&lt;/p&gt;

&lt;p&gt;Medusa is also a confirmation of the Data-Oriented Design mantra: "Know your domain." We could have stuck to cliffs rendered as common Drake renderers, but after a close look, we saw that there are few cases covered by Drake, and cliffs are so common that we can introduce meaningful optimizations here.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>optimization</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Deep dive Tainted Grail [2] - Leshy - vegetation streaming and rendering</title>
      <dc:creator>KamilVDono</dc:creator>
      <pubDate>Sat, 23 Aug 2025 11:48:38 +0000</pubDate>
      <link>https://forem.com/kamilvdono/deep-dive-tainted-grail-2-leshy-vegetation-streaming-and-rendering-4ndk</link>
      <guid>https://forem.com/kamilvdono/deep-dive-tainted-grail-2-leshy-vegetation-streaming-and-rendering-4ndk</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Name
&lt;/h3&gt;

&lt;p&gt;In Slavic mythology Leshy is a guardian of forests. Which makes perfect name for vegetation related system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdesxs17zvsf0tbff3ewt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdesxs17zvsf0tbff3ewt.png" alt="leshy_image" width="728" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Application
&lt;/h3&gt;

&lt;p&gt;Leshy is a system for rendering vegetation and streaming vegetation placement data.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd0wjgnn18h8oe79elwq6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd0wjgnn18h8oe79elwq6.gif" alt="Vegetation overview" width="8" height="4"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  VSP
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://store.steampowered.com/app/1199030/Tainted_Grail_Conquest/" rel="noopener noreferrer"&gt;Conquest&lt;/a&gt; was using &lt;code&gt;VegetationStudioPro&lt;/code&gt; (VSP) as a vegetation solution, as the world is procedural, vegetation placement needed to be runtime.&lt;br&gt;
&lt;a href="https://store.steampowered.com/app/1466060/Tainted_Grail_The_Fall_of_Avalon/?curator_clanid=42629110" rel="noopener noreferrer"&gt;Fall of Avalon&lt;/a&gt; has static offline pre-prepared world, that means more optimizations can be introduced.&lt;br&gt;
VSP has storage for offline baking, but VSP is no longer developed nor maintained. That means the issues won't be fixed by package update, we would need to fix these manually and code of VSP is complicated.&lt;/p&gt;
&lt;h3&gt;
  
  
  Issues
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shaders&lt;/strong&gt; - VSP uses "old" instancing model, which makes shaders a lot harder to maintain. Every time we upgraded Unity, all vegetation was glitching and required long days of fixing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variations limitation&lt;/strong&gt; - we needed the same prefab spawned with different rules, for VSP every such item () was a separate item, resulting in a lot of small jobs and memory duplication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow open world streaming&lt;/strong&gt; - streaming was slow and required a lot of small jobs to be scheduled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High memory footprint&lt;/strong&gt; - rotation and scale weren't compressed&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  What was good
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Editor setup&lt;/strong&gt; - level designers and artists were already familiar with VSP setup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Procedural placement&lt;/strong&gt; - algorithm of procedural placement of vegetation is the strongest part of VSP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baking&lt;/strong&gt; - VSP can output generated vegetation placement into a file&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;
&lt;h4&gt;
  
  
  VSP
&lt;/h4&gt;

&lt;p&gt;From the list above you can find that editor mode part of VSP was not bad, therefore we kept it. That means vegetation setup, placement, and baking are done by VSP. Then our system will pick up baked data and transform it (baking time) and at runtime it will stream cells, render vegetation and place colliders next to the player.&lt;/p&gt;
&lt;h4&gt;
  
  
  Entities
&lt;/h4&gt;

&lt;p&gt;As you know, from the previous post, we had experience with Entities. Entities are promoted as fast spawn, fast rendering, low memory overhead. Sounds like a perfect solution.&lt;br&gt;
With transformed data in place, I did a few tests. Every test failed, having hundreds of thousands of vegetation entities spawned and removed is too much for entities, operations were too slow.&lt;br&gt;
Additionally, memory footprint was a few times the number from VSP, as there was a requirement for some additional components and transform is &lt;code&gt;float4x4&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  Custom solution
&lt;/h4&gt;

&lt;p&gt;After Entities attempts failed, only fully custom solutions were possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There was possibility to rewrite VSP jobs and how it deals with streaming and variants. The shader problem would still be in place.&lt;/li&gt;
&lt;li&gt;Custom kind of GPU driven pipeline, most complicated, with most unknowns.&lt;/li&gt;
&lt;li&gt;BRG-based rendering, new shiny tech empowering entities with low documentation (still more than other approaches), but entities is good usage example.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With all that, we chose BRG, GPU driven pipeline may yield better performance and memory, but shaders were unknown and may not mean we would achieve it. After time spent on Entities attempts, we wanted a more guaranteed path (and faster to do). &lt;/p&gt;
&lt;h2&gt;
  
  
  BRG
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BatchRendererGroup (BRG)&lt;/strong&gt; is the heart of modern Unity rendering. I mentioned that Entities Graphics uses it, but new GPU Resident Drawer also uses it.&lt;br&gt;
BRG has a few parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resources registration - register meshes and materials to obtain burstable handles to them&lt;/li&gt;
&lt;li&gt;Batches - batch is a graphics buffer with description of the data it contains&lt;/li&gt;
&lt;li&gt;Culling callback - callback with struct to fill with drawing data, for most cases you will also perform frustum culling here and maybe other cullings if you are using them (custom LOD solution, custom occlusion culling). You can decide which rendering types you are supporting by passing them to &lt;code&gt;SetEnabledViewTypes&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Resources registration
&lt;/h3&gt;

&lt;p&gt;You need to register meshes and materials, you do it by respectively calling &lt;code&gt;RegisterMesh&lt;/code&gt; and &lt;code&gt;RegisterMaterial&lt;/code&gt;, which give, respectively, &lt;code&gt;BatchMeshID&lt;/code&gt; and &lt;code&gt;BatchMaterialID&lt;/code&gt;. After you are done with any asset, you must call &lt;code&gt;UnregisterMesh&lt;/code&gt; and &lt;code&gt;UnregisterMaterial&lt;/code&gt; with appropriate ID.&lt;/p&gt;
&lt;h3&gt;
  
  
  Batches
&lt;/h3&gt;

&lt;p&gt;Batch consists of &lt;code&gt;metadata&lt;/code&gt; and &lt;code&gt;buffer&lt;/code&gt;. You can register batch with &lt;code&gt;AddBatch&lt;/code&gt; and that will give you &lt;code&gt;BatchID&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;Metadata&lt;/code&gt; is an array of &lt;code&gt;MetadataValue&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;MetadataValue&lt;/code&gt; defines which property it defines, how to interpret data, and where it starts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;MetadataValue&lt;/span&gt; &lt;span class="nf"&gt;CreateMetadataValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nameID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;gpuOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isPerInstance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;IsPerInstanceBit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0x80000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;MetadataValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;NameID&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nameID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;gpuOffset&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isPerInstance&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;IsPerInstanceBit&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most common are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;unity_ObjectToWorld&lt;/code&gt; - Local2World transform in form of matrix with 4 rows and 3 columns&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unity_WorldToObject&lt;/code&gt; - Inverse of above&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unity_MatrixPreviousM&lt;/code&gt; - Previous frame Local2World transform required to generate motion vectors&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Example
&lt;/h4&gt;

&lt;p&gt;Let's say we want batch where there is a max of 3 instances (to keep visualization possible), support motion vectors, per instance &lt;code&gt;_TintColor&lt;/code&gt; and per batch &lt;code&gt;_DebugColor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create batch buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;maxInstances&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;uintMatrixSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PackedMatrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;uintColorSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transformsSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uintMatrixSize&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;maxInstances&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// (unity_ObjectToWorld, unity_WorldToObject, unity_MatrixPreviousM)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;colorsSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uintColorSize&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;maxInstances&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// _TintColor&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;debugColorSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uintColorSize&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Per batch singe value of _DebugColor&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fullSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transformsSize&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;colorsSize&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;debugColorSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;graphicsBuffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GraphicsBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GraphicsBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fullSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create metadata descriptor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MetadataValue&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Allocator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NativeArrayOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UninitializedMemory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateMetadataValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_DebugColor"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// I like to keep per batch data first&lt;/span&gt;
&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Per batch mean it is just single value&lt;/span&gt;

&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateMetadataValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unity_ObjectToWorld"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PackedMatrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;maxInstances&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateMetadataValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unity_WorldToObject"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PackedMatrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;maxInstances&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateMetadataValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unity_MatrixPreviousM"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PackedMatrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;maxInstances&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateMetadataValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PropertyToID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_TintColor"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;maxInstances&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// If you have more data keep adding it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register and dispose no longer needed memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;batchID&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_brg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;graphicsBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bufferHandle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With sizes presented as:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3z9cfy33r294gbqqb4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3z9cfy33r294gbqqb4e.png" alt="Sizes when uint is a base (so each cell is a 4 bytes)" width="800" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the buffer would be like this:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdl3jynwmqrssaiiu1809.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdl3jynwmqrssaiiu1809.png" alt="Continuous buffer" width="800" height="131"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But it's hard to read, so let me break that semi-continuous into more described image:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fys2qc6cm6d808zrib2p1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fys2qc6cm6d808zrib2p1.png" alt="Described buffer" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Culling callback
&lt;/h3&gt;

&lt;p&gt;This callback must fill draw data inside &lt;code&gt;BatchCullingOutput&lt;/code&gt;. It returns JobHandle therefore the code can be bursted. The callback also contains &lt;code&gt;BatchCullingContext&lt;/code&gt; which contains information and all data for performing right culling and producing draw data.&lt;/p&gt;

&lt;p&gt;You should perform a few operations here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frustum culling&lt;/li&gt;
&lt;li&gt;Layer culling&lt;/li&gt;
&lt;li&gt;Split calculation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you implement your own Occlusion Culling you can apply it here as well, Leshy doesn't have occlusion culling.&lt;/p&gt;

&lt;p&gt;You also need to determine which views your callback methods support. There are following views available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Camera&lt;/code&gt; - is the main rendering, GBuffer pass, called once per camera&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Light&lt;/code&gt; - is the shadows maps pass, called for every light with shadows&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Picking&lt;/code&gt; - when you click in scene view, this callback gets called to determine what you clicked on&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SelectionOutline&lt;/code&gt; - as the name indicates, used to outline rendering in scene view&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Filtering&lt;/code&gt; - I think it is used when you are using graying out non-matching when searching in hierarchy window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For main functionality you want to handle &lt;code&gt;Camera&lt;/code&gt; and &lt;code&gt;Light&lt;/code&gt;. &lt;br&gt;
For camera &lt;code&gt;splitMask&lt;/code&gt; is not important, but you must assign it correctly for shadows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data
&lt;/h2&gt;

&lt;p&gt;To prepare for the streaming chapter you must know how the data is represented for Leshy.&lt;br&gt;
A vegetation item has rendering data (meshes, materials, lod distances, bounds, and optional collider prefab).&lt;br&gt;
Each vegetation item has its own 2D grid, where each instance is assigned to a grid cell. Instance is represented by &lt;code&gt;float3 position&lt;/code&gt;, &lt;code&gt;half4 quaternion&lt;/code&gt; and &lt;code&gt;half3 scale&lt;/code&gt;. Rotation could be &lt;code&gt;uint&lt;/code&gt; but I wasn't aware of such compression method possibility at the time. I said 2D grid cell, but after instances are assigned, cell is no longer represented as square, but instead it is represented as circle.&lt;br&gt;
Last piece is GPU data, the format is described at the BRG section. Leshy is fixed cost here, as it allocates the whole GPU memory at initialization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming
&lt;/h2&gt;

&lt;p&gt;With rendering handled, the next part was streaming. Very important as it minimizes required memory; it also reduces work done inside culling callback as not present cells don't need to be processed.&lt;/p&gt;

&lt;p&gt;Streaming has the following steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calculate visible cells - cell has position and radius, cell visibility is determined by two factors, which are ORed. Cell frustum visibility and cell distance to camera. Cell is checked as circle as sphere vs frustum is faster than box vs frustum.&lt;/li&gt;
&lt;li&gt;Cell content loading - cell data is stored in a file, to start loading we need to have loading slot available (to not load too much at once), then loading from file into CPU buffer starts, it will take several frames&lt;/li&gt;
&lt;li&gt;Filter data - particular vegetation types support lowering density (so you can lower vegetation density to boost performance), to do that we randomly remove X% of the loaded instances&lt;/li&gt;
&lt;li&gt;Cell transfer to GPU - the CPU buffer is then transferred to BRG buffer on the GPU, but that step requires a few actions:

&lt;ul&gt;
&lt;li&gt;Find free cell(s) - the single vegetation cell into a single memory block on GPU is preferred, but sometimes it must be split into multiple GPU memory blocks&lt;/li&gt;
&lt;li&gt;Copy CPU minimal data to transfer buffer - CPU data needs padding to fill GPU buffer, so &lt;code&gt;MemCpyStride&lt;/code&gt; is used to copy CPU buffer into CPU transfer buffer which will be transferred to GPU memory&lt;/li&gt;
&lt;li&gt;Expand GPU data - CPU data with padding is not what is required by BRG so, we transfer minimal data from CPU to GPU, then compute shader is used to copy and expand data on GPU, from small transfer buffer into big BRG buffer&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Stream-out is simple, just clear CPU buffers, and mark CPU representation of GPU memory block(s) as free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collisions
&lt;/h2&gt;

&lt;p&gt;Large vegetation (trees, boulders) must have collision, otherwise it would be very odd. Having all spawned vegetation with colliders would kill Unity, so the idea is to place colliders only around the player. To do it, we go with the following steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dispose bitmask for removed cells&lt;/li&gt;
&lt;li&gt;For each cell, check if in radius&lt;/li&gt;
&lt;li&gt;If in radius, check each instance and generate bitmask which indicates which instance should have collider&lt;/li&gt;
&lt;li&gt;Move pooled colliders to the place where we need them&lt;/li&gt;
&lt;li&gt;Hide colliders which are not used anymore&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quality
&lt;/h2&gt;

&lt;p&gt;Vegetation has its own setting in graphics settings. The artists must set up what each quality level does for each vegetation type for the following properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Density&lt;/code&gt; - artists generate vegetation with Ultra density (1.0), then we can specify reduction for lower quality, density cannot change for vegetation with collisions as it would affect gameplay&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SpawnDistance&lt;/code&gt; - defines the distance from which the cell will be marked as to spawn&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Shadows&lt;/code&gt; - choose if vegetation should cast shadows&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BillboardDistance&lt;/code&gt; - for vegetation with billboards defines when billboard starts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Handplacing
&lt;/h2&gt;

&lt;p&gt;Last part of the system, introduced after a few months was the possibility to manually place vegetation and then Leshy during baking will pick it up. For Leshy runtime there is no difference between procedural and handplaced vegetation, all end up in the same data stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue
&lt;/h2&gt;

&lt;p&gt;Leshy entered the main branch with no major issues or bugs. Also with knowledge from &lt;a href="https://dev.to/kamilvdono/deep-dive-tainted-grail-1-drake-runtime-entity-renderer-registration-system-1751"&gt;Drake&lt;/a&gt; development, Leshy was smooth. For a few months only minor workflow improvements were needed along with optimizations for baking process.&lt;br&gt;
Biggest issue was discovered when we started to play on Xbox, the slower the game was running, the more weirdly the vegetation behaved. We tracked the issues were present for all new graphics APIs (DX12 for PC, DX12 for Xbox and PS5 API), deeper investigation resulted in discovery that there were two system changes required.&lt;/p&gt;

&lt;p&gt;First:&lt;br&gt;
Group data binding and compute shader dispatch within command queue, that eliminated some part of misbehavior but not all.&lt;/p&gt;

&lt;p&gt;Second:&lt;br&gt;
Don't use &lt;code&gt;LockBufferForWrite&lt;/code&gt;. It sounds like a great feature for transferring CPU data to GPU transfer buffer, which then is expanded through compute shader, but even with fence and waiting the data sometimes were corrupted. It may be we did something wrong, but the bug was present mostly on Xbox so debugging became very time consuming, with that in mind we decided to pay additional 0.01ms and go with ordinary &lt;code&gt;SetData&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;You can see how important experience is. If no exploration was done during Drake development then the idea to just use BRG in a custom way won't pop in the mind. We would probably go with custom GPU pipeline, complex shaders tool and encounter more roadblocks. BRG is nicely integrated into engine (especially picking and shaders generation), provides burstable API and eliminates a lot of manual wiring and boilerplate when compared to doing similar instancing system written from scratch, so huge amount of time saved.&lt;/p&gt;

&lt;p&gt;With more time I would do vegetation harvesting (so you can remove a particular instance from rendering) and LOD cross-fade. Probably in reverse order, because lack of LOD cross-fade you can see annoying pop-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  BRG
&lt;/h3&gt;

&lt;p&gt;When Leshy was developed, BRG was a new thing which felt like Unity's future of rendering, that had underlying promise of idea expansion, new features and so on. It was kind of true, new GPU Resident Drawer is based on BRG, it also has GPU occlusion culling. Unfortunately, occlusion culling is not made in a way that can be used by custom BRG-based system, what a shame. That puts a big question mark for how the future of BRG will look. Would it be frozen in its current state for custom projects and only will be expanded and developed for Unity's internal systems?&lt;/p&gt;

&lt;p&gt;Another missing pieces are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batch lighting - right now you need to deal with each shadow-casting light separately, more for that issue &lt;a href="https://github.com/Dreaming381/Latios-Framework-Documentation/blob/main/Optimization%20Adventures/Part%2015%20-%20Frustum%20Culling%202.md" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Keyword override - possibility to enable keyword for every material registered would be great, right now if you want such behaviour, you need to create a material copy and manually enable keyword, and somehow track changes to original material and redo them to the copy.&lt;/li&gt;
&lt;li&gt;Depth prepass view - right now camera view is used for depth prepass as well as for GBuffer passes. With separate culling I could skip depth prepass for most vegetation, and keep prepass only for big and close renderers. So for 20% of draws I would get 80% of final depth, that is how most of the engines do it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>optimization</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Deep dive Tainted Grail [1] - Drake - runtime entity renderer registration system</title>
      <dc:creator>KamilVDono</dc:creator>
      <pubDate>Thu, 19 Jun 2025 08:04:23 +0000</pubDate>
      <link>https://forem.com/kamilvdono/deep-dive-tainted-grail-1-drake-runtime-entity-renderer-registration-system-1751</link>
      <guid>https://forem.com/kamilvdono/deep-dive-tainted-grail-1-drake-runtime-entity-renderer-registration-system-1751</guid>
      <description>&lt;h2&gt;
  
  
  The Name
&lt;/h2&gt;

&lt;p&gt;Drake is a type of dragon. Dragons are gold keepers, and this system is also golden. :) With this system, I started a naming convention related to mythological creatures/entities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;We faced significant performance issues with &lt;code&gt;MeshRenderers&lt;/code&gt;. Internal managers for &lt;code&gt;MeshRenderers&lt;/code&gt; were (and may still be) very CPU-intensive, as was frustum culling. However, we needed to use many renderers.&lt;br&gt;
At the time, Entities were nearing version 1.0, and my tests with heavy renderer loads showed much better performance. The idea was straightforward: use Entities for rendering. The requirements were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimal changes to artists' workflows&lt;/li&gt;
&lt;li&gt;Only rendering changes (physics and other systems remain unchanged)&lt;/li&gt;
&lt;li&gt;Interactivity with Unity's OOP world (most renderers are static, but hundreds require some interaction):

&lt;ul&gt;
&lt;li&gt;Follow transforms&lt;/li&gt;
&lt;li&gt;Copy enable/disable state from corresponding &lt;code&gt;GameObject&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Process material change requests (a recent addition)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Streaming via Addressables, as Entities' content management would duplicate assets, and we were unaware of Addressables' scaling limitations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Subscene systems were not feasible for this use case.&lt;/p&gt;
&lt;h2&gt;
  
  
  First steps
&lt;/h2&gt;

&lt;p&gt;Let's start simple. Just take &lt;code&gt;MeshRenderer&lt;/code&gt; and convert it at runtime to entity. It's easy, just use &lt;code&gt;RenderMeshUtility.AddComponents&lt;/code&gt;, nice win.&lt;br&gt;
The next step was to remove &lt;code&gt;MeshRenderer&lt;/code&gt; and move all required data to &lt;code&gt;DrakeMeshRenderer&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Mesh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Materials&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RenderMeshDescription&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Editor code was implemented to convert &lt;code&gt;MeshRenderer&lt;/code&gt; to &lt;code&gt;DrakeMeshRenderer&lt;/code&gt;. The &lt;code&gt;DrakeMeshRenderer&lt;/code&gt; itself is just a few lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_materials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;renderMeshArray&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RenderMeshArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_materials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Mesh&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_mesh&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Allocation :(&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;localToWorld&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LocalToWorld&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localToWorldMatrix&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;_entities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;NativeArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Allocator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NativeArrayOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UninitializedMemory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt;  &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateEntity&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;RenderMeshUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// Heavy :(&lt;/span&gt;
        &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;renderMeshArray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MaterialMeshInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromRenderMeshArrayIndices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddComponentData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;localToWorld&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_entities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  LOD's
&lt;/h2&gt;

&lt;p&gt;Next, we needed to handle &lt;code&gt;LODGroup&lt;/code&gt; conversion by creating entity with &lt;code&gt;LODGroup&lt;/code&gt; data and notifying rendering Entities that they are linked to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entities LodGroup
&lt;/h3&gt;

&lt;p&gt;We need &lt;code&gt;MeshLODGroupComponent&lt;/code&gt;. It's easy code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LODGroup&lt;/span&gt; &lt;span class="n"&gt;lodGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lodGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;worldSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LodUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetWorldSpaceScale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;lodGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;localReferencePoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lodGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localReferencePoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Local space&lt;/span&gt;

    &lt;span class="n"&gt;lodDistances0&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;float4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PositiveInfinity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;odDistances1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;float4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PositiveInfinity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lods&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lodGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetLODs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;worldSize&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;lods&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;screenRelativeTransitionHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// But here world space ugh&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;lodDistances0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;lodDistances1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  DrakeRendererManager and modifications of DrakeMeshRenderer
&lt;/h3&gt;

&lt;p&gt;Now &lt;code&gt;DrakeMeshRenderer&lt;/code&gt;needs to add to its entities &lt;code&gt;MeshLODComponent&lt;/code&gt;. So we need to save lod mask at conversion time and we need entity spawned by &lt;code&gt;DrakeLodGroup&lt;/code&gt;.&lt;br&gt;
Let me introduce &lt;code&gt;DrakeRendererManager&lt;/code&gt;, which creates both renderer and LOD group Entities. so after lod group entity is created we can pass them to spawning of renderers.&lt;/p&gt;

&lt;p&gt;For simplicity, we maintain a link between &lt;code&gt;DrakeLodGroup&lt;/code&gt; and &lt;code&gt;DrakeMeshRenderer&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DrakeMeshRenderer&lt;/code&gt; has an optional reference to &lt;code&gt;DrakeLodGroup&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DrakeLodGroup&lt;/code&gt; has an array of &lt;code&gt;DrakeMeshRenderer&lt;/code&gt; references, requiring at least one entry (otherwise, it’s an invalid LODGroup).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;DrakeRendererManager&lt;/code&gt; has two &lt;code&gt;Register&lt;/code&gt; methods: one for &lt;code&gt;DrakeLodGroup&lt;/code&gt; (&lt;code&gt;DLG&lt;/code&gt;) and one for &lt;code&gt;DrakeMeshRenderer&lt;/code&gt; (&lt;code&gt;DMR&lt;/code&gt;), also called from &lt;code&gt;DLG&lt;/code&gt; for each child. Both &lt;code&gt;DLG&lt;/code&gt; and &lt;code&gt;DMR&lt;/code&gt; call the appropriate method from &lt;code&gt;Start&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Streaming
&lt;/h2&gt;

&lt;p&gt;Streaming is critical to load only necessary meshes and materials. &lt;code&gt;RenderMeshArray&lt;/code&gt; and &lt;code&gt;MaterialMeshInfo&lt;/code&gt; must be added and removed dynamically.&lt;br&gt;
Addressables uses strings as keys, which cannot be bursted, so we store them indirectly.&lt;/p&gt;

&lt;p&gt;Ladies and gentlemen, &lt;code&gt;DrakeMaterialMeshInfo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;DrakeMaterialMeshInfo&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IComponentData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;meshIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;materialIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;submesh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Marvelous one. This component stores material and mesh indices, which require arrays to index them. This led to the creation of &lt;code&gt;DrakeAddressablesManager&lt;/code&gt;.&lt;br&gt;
If C# would has header files, for &lt;code&gt;DrakeAddressablesManager&lt;/code&gt; it would be like that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DrakeAddressablesManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ushort&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_meshKeyToIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ushort&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_materialKeyToIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AddressableLoadingData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mesh&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_meshLoadingData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AddressableLoadingData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Material&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_materialLoadingData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="n"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;RegisterMaterial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;materialKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;StartLoadingMaterial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;materialIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;StartUnloadingMaterial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;materialIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;RegisterMesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;meshKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;StartLoadingMesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;meshIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;StartUnloadingMesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;meshIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BatchMaterialID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TryGetLoadedMaterial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;materialIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BatchMeshID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TryGetMesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;meshIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and if you wonder, &lt;code&gt;AddressableLoadingData&amp;lt;T&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;AddressableLoadingData&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Object&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;AsyncOperationHandle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loadingHandle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During registration, we register keys and add &lt;code&gt;DrakeMaterialMeshInfo&lt;/code&gt; instead of &lt;code&gt;RenderMeshArray&lt;/code&gt; and &lt;code&gt;MaterialMeshInfo&lt;/code&gt;. Then other simple checks if the renderer should be loaded (based on a distance-to-camera vs. LOD distance comparison). If so, &lt;code&gt;StartLoadingMaterial&lt;/code&gt; and &lt;code&gt;StartLoadingMesh&lt;/code&gt; are called with the corresponding indices. &lt;br&gt;
The next frame, another system checks &lt;code&gt;TryGetLoadedMaterial&lt;/code&gt; and &lt;code&gt;TryGetMesh&lt;/code&gt;. If both succeed, &lt;code&gt;MaterialMeshInfo&lt;/code&gt; is created and added to the entity.&lt;/p&gt;

&lt;p&gt;You may ask:&lt;br&gt;
What about &lt;code&gt;RenderMeshArray&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;It’s very wasteful, as we know when resources are loaded. We manually replicate &lt;code&gt;RenderMeshArray&lt;/code&gt;’s functionality by obtaining &lt;code&gt;EntitiesGraphicsSystem&lt;/code&gt; and calling &lt;code&gt;(Un)RegisterMesh&lt;/code&gt; or &lt;code&gt;(Un)RegisterMaterial&lt;/code&gt;. This eliminates multiple managed array creations - double win.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mipmaps streaming
&lt;/h3&gt;

&lt;p&gt;It's not topic of this post, but quick mention.&lt;br&gt;
Once mesh loading is complete, we collect UV distribution data. When material loading is finished, we register the material to the mipmap streaming system, which provides a &lt;code&gt;MaterialMipMapsStreamingHandle&lt;/code&gt;. After both the mesh and material are loaded and the Entity is spawning, &lt;code&gt;UVDistributionComponent&lt;/code&gt; and &lt;code&gt;MipmapsStreamingComponent&lt;/code&gt; are added to the Entity. From then on, mipmap streaming systems can process these and update the required mip level.&lt;/p&gt;
&lt;h2&gt;
  
  
  Let move it
&lt;/h2&gt;

&lt;p&gt;Another requirement: possibility to link transform with renderer entity.&lt;br&gt;
Transforms are one of Unity’s Four Horsemen of the Apocalypse. To avoid killing performance, we use &lt;code&gt;TransformAccessArray&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We model this as: after the simulation step, run &lt;code&gt;IJobParallelForTransform&lt;/code&gt; to write transform data back to linked Entities.&lt;br&gt;
The challenge lies in the near-nonexistent documentation for &lt;code&gt;TransformAccessArray&lt;/code&gt;. Let me reiterate: one of the most impactful and essential optimization tool lacks explanation - good job Unity as always :)&lt;/p&gt;

&lt;p&gt;It's worth mentioning how we store transforms in an ECS unmanaged component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;LinkedTransformComponent&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IComponentData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IEquatable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LinkedTransformComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;UnityObjectRef&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;LinkedTransformComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LinkedTransformComponent&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;GetHashCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetHashCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The registration method collects new entities and registers each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_freeIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lastIndex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_freeIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_freeIds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lastIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;_freeIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RemoveAtSwapBack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_transformsArray&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_linkedTransformEntities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;newId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_transformsArray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_transformsArray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_linkedTransformEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;newId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_freeIds&lt;/code&gt; tracks unoccupied transform indices in &lt;code&gt;_transformsArray&lt;/code&gt;. At the time, leaving removed transforms in &lt;code&gt;TransformAccessArray&lt;/code&gt; was faster than removing them and adjusting Entity indices. We also use &lt;code&gt;LinkedTransformIndexComponent&lt;/code&gt;, an &lt;code&gt;ICleanupComponentData&lt;/code&gt; storing the index to free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two way link
&lt;/h2&gt;

&lt;p&gt;We can read managed data (track transform values), but we also need to manipulate Entities from the managed side.&lt;br&gt;
If requested, &lt;code&gt;LinkedEntitiesAccess&lt;/code&gt; &lt;code&gt;MonoBehaviour&lt;/code&gt; is added during registration, holding a list of Entities linked to the &lt;code&gt;GameObject&lt;/code&gt;. A challenge arises because registration uses &lt;code&gt;EntityCommandBuffer&lt;/code&gt;, so the Entity isn’t immediately available. As a workaround, registration creates a &lt;code&gt;LinkedEntitiesAccessRequest&lt;/code&gt; with a &lt;code&gt;UnityRef&lt;/code&gt; to the &lt;code&gt;LinkedEntitiesAccess&lt;/code&gt; &lt;code&gt;MonoBehaviour&lt;/code&gt;. After &lt;code&gt;ECB&lt;/code&gt; playback, a system processes requests and registers valid Entities to the given &lt;code&gt;LinkedEntitiesAccess&lt;/code&gt;. This requires a managed, unbursted system, but I’m unsure if there’s a better approach.&lt;/p&gt;
&lt;h3&gt;
  
  
  Enable/Disable/Destroy
&lt;/h3&gt;

&lt;p&gt;With access to entities we can react to &lt;code&gt;GameObject&lt;/code&gt; state changes: hide entities when &lt;code&gt;GameObject&lt;/code&gt; is disabled, show them when is enabled, and destroy renderers alongside with &lt;code&gt;GameObject&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Materials
&lt;/h3&gt;

&lt;p&gt;Often there is need to modify material of renderers (strictly for renderer not material itself). There are two main modifications:&lt;/p&gt;
&lt;h4&gt;
  
  
  Replace value
&lt;/h4&gt;

&lt;p&gt;This uses custom components tagged with &lt;code&gt;MaterialPropertyAttribute&lt;/code&gt; from Entities Graphics. No custom modifications are required, only a smart editor to display possible changes.&lt;/p&gt;
&lt;h4&gt;
  
  
  Replace whole material
&lt;/h4&gt;

&lt;p&gt;To replace a material, we first register the new material with &lt;code&gt;DrakeResourcesManager&lt;/code&gt;, which returns a material index for creating a &lt;code&gt;DrakeMaterialMeshInfo&lt;/code&gt;.&lt;br&gt;
Next step is tricky, as it depends on the current state of &lt;code&gt;DrakeMeshRenderer&lt;/code&gt;. There are five states to address, but all involve replacing the old &lt;code&gt;DrakeMaterialMeshInfo&lt;/code&gt; with a new one. Additional operations primarily include unloading the previously loaded material (decrementing its refcount) and adding or removing certain state components.&lt;/p&gt;
&lt;h2&gt;
  
  
  Scene unloaded
&lt;/h2&gt;

&lt;p&gt;Scene lifetime management is required.&lt;br&gt;
Startup is easy using &lt;code&gt;MonoBehaviour&lt;/code&gt;’s &lt;code&gt;Start&lt;/code&gt; (preferred, as spawning agents can edit properties, e.g., mark as non-static).&lt;br&gt;
Unloading is much trickier, as no &lt;code&gt;MonoBehaviour&lt;/code&gt; remains (see Optimize/Leftovers section). To solve that issue I created &lt;code&gt;SystemRelatedLifeTime&amp;lt;T&amp;gt;&lt;/code&gt; class, with nested &lt;code&gt;IdComponent&lt;/code&gt;. &lt;code&gt;IdComponent&lt;/code&gt; has just &lt;code&gt;int id&lt;/code&gt; and it's &lt;code&gt;ISharedComponentData&lt;/code&gt;.&lt;br&gt;
To make such generic component work you need to register every usage with line like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;RegisterGenericComponentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SystemRelatedLifeTime&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DrakeRendererManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;IdComponent&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can make query over that component, with filter for specific &lt;code&gt;IdComponent&lt;/code&gt;, and destroy all matching entities. It’s highly efficient and elegant.&lt;br&gt;
Id for scene is just scene handle value.&lt;/p&gt;
&lt;h2&gt;
  
  
  Vertex snap hack
&lt;/h2&gt;

&lt;p&gt;Our artists often use vertex snap tool in editor. Therefore Drake needs to support it, unfortunately there is no such functionality in entites. As a workaround, we made special mode when instead of creating entities we spawn old &lt;code&gt;LodGroups&lt;/code&gt; and &lt;code&gt;MeshRenderers&lt;/code&gt; as hidden in hierarchy children. That made editor flow very complicated (need to keep track of all Drakes, spawn in two ways, enter/exit play mode edge-cases, prefab editing nightmare, duplication edge-cases, undo and so on).&lt;br&gt;
There is no much more to say, just that Unity editor support is very painful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Optimize
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Leftovers
&lt;/h3&gt;

&lt;p&gt;Once Drakes are registered, there is no need to keep their corresponding &lt;code&gt;MonoBehaviours&lt;/code&gt;. In most cases, these are the only &lt;code&gt;components&lt;/code&gt; on a &lt;code&gt;GameObject&lt;/code&gt;, allowing the &lt;code&gt;GameObject&lt;/code&gt; to be destroyed. Often, this leaves the parent &lt;code&gt;GameObject&lt;/code&gt; as a leaf node with no &lt;code&gt;components&lt;/code&gt;, which can also be purged, and the cycle continues up to needed &lt;code&gt;GameObject&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This results in shallower hierarchies and allows many small memory allocations to be reclaimed faster (after a GarbageCollector run, of course).&lt;/p&gt;
&lt;h3&gt;
  
  
  Static
&lt;/h3&gt;

&lt;p&gt;If you check entities graphics code, you can find that &lt;code&gt;LodGroup&lt;/code&gt; is just to update components on rendering entities.&lt;br&gt;
For static groups with no moving parts, we can skip spawning &lt;code&gt;LODGroup&lt;/code&gt; and assign values at spawn time.&lt;br&gt;
The plan is simple: for static group, spawn only fully set up rendering entities. First, we need to determine if the prefab is static. I used the static flag from the &lt;code&gt;GameObject&lt;/code&gt;, but since this flag is unavailable in builds, it must be cached during baking.&lt;/p&gt;
&lt;h3&gt;
  
  
  EntityCommandBuffer
&lt;/h3&gt;

&lt;p&gt;You might hear that &lt;code&gt;EntityManager&lt;/code&gt; should be used whenever possible because it’s faster than &lt;code&gt;EntityCommandBuffer&lt;/code&gt;. We tested this, but it was significantly slower in our case. At Start, we spawn thousands of entities for &lt;code&gt;Renderers&lt;/code&gt; and hundreds for &lt;code&gt;LODGroups&lt;/code&gt; in chains of operations on these entities. Most operations occur after a scene loads, though a few take place during gameplay.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;LodGroup&lt;/code&gt; chain is like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create &lt;code&gt;Entity&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;LocalToWorld&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;MeshLODGroupComponent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;LinkedTransformComponent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;IdComponent&lt;/code&gt; (shared component)&lt;/li&gt;
&lt;li&gt;[Optionally] Add &lt;code&gt;LinkedEntitiesAccessRequest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;code&gt;Renderer&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create &lt;code&gt;Entity&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;LocalToWorld&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;WorldRenderBounds&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;DrakeMeshMaterialComponent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[Optionally] Set &lt;code&gt;MeshLODComponent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[Optionally] Set &lt;code&gt;DrakeRendererVisibleRangeComponent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[Optionally] Set &lt;code&gt;LODRange&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[Optionally] Set &lt;code&gt;LODWorldReferencePoint&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[Optionally] Set &lt;code&gt;LinkedTransformComponent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[Optionally] Set &lt;code&gt;RenderBounds&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[Optionally] Set &lt;code&gt;LinkedTransformLocalToWorldOffsetComponent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;RenderFilterSettings&lt;/code&gt; (shared component)&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;IdComponent&lt;/code&gt; (shared component)&lt;/li&gt;
&lt;li&gt;[Optionally] Add &lt;code&gt;LinkedEntitiesAccessRequest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Archetypes
&lt;/h3&gt;

&lt;p&gt;Adding components is expensive, so we create Entities with the correct archetype using &lt;code&gt;DrakeRendererArchetypeKey&lt;/code&gt;. It covers all configurations, and the manager generates archetypes for each.&lt;br&gt;
Main logic looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;DrakeRendererArchetypeKey&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;CreateAllValues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// _static * _isTransparent * _hasLod * _inMotionPass * _lightProbeUsage * _hasShadowsOverriden * _hasLocalToWorldOffset&lt;/span&gt;
    &lt;span class="n"&gt;DrakeRendererArchetypeKey&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DrakeRendererArchetypeKey&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;isStatic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;isTransparent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hasLod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inMotion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lightProbeUsage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;LightProbeUsage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Off&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LightProbeUsage&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hasShadowsOverriden&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hasLocalToWorldOffset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                                &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;++]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DrakeRendererArchetypeKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isStatic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isTransparent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hasLod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inMotion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lightProbeUsage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hasShadowsOverriden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hasLocalToWorldOffset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;archetypeKeys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DrakeRendererArchetypeKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;_entityArchetypes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;NativeHashMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DrakeRendererArchetypeKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EntityArchetype&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Allocator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;archetypeKey&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;archetypeKeys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_entityArchetypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;CreateArchetype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From Unity.Rendering.RenderMeshUtility.EntitiesGraphicsComponentTypes&lt;/span&gt;
&lt;span class="n"&gt;EntityArchetype&lt;/span&gt; &lt;span class="nf"&gt;CreateArchetype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DrakeRendererArchetypeKey&lt;/span&gt; &lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EntityManager&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;UnsafeList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARAlloc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Temp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WorldRenderBounds&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DrakeMeshMaterialComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PerInstanceCullingTag&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WorldToLocal_Tag&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LocalToWorld&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MipmapsFactorComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;

        &lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChunkComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChunkWorldRenderBounds&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;

        &lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RenderFilterSettings&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SystemRelatedLifeTime&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DrakeRendererManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;IdComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShadowsProcessedTag&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isStatic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Static&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LinkedTransformComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RenderBounds&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inMotionPass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BuiltinMaterialPropertyUnity_MatrixPreviousM&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasLocalToWorldOffset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LinkedTransformLocalToWorldOffsetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasLodGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DrakeRendererVisibleRangeComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LODRange&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LODWorldReferencePoint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isStatic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MeshLODComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DrakeRendererLoadRequestTag&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isTransparent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DepthSorted_Tag&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lightProbeUsage&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;LightProbeUsage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BlendProbes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BlendProbeTag&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lightProbeUsage&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;LightProbeUsage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomProvided&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomProbeTag&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archetypeKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasShadowsOverriden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShadowsChangedTag&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#if UNITY_EDITOR
#if DEBUG
&lt;/span&gt;    &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CullingDistancePreviewComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;UnityEditor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EditorPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"showEntities"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EntityGuid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EditorRenderData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;archetype&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateArchetype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNativeArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;archetype&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces runtime operations significantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scene parts hide/show
&lt;/h3&gt;

&lt;p&gt;In the “Two-Way Link” section, I mentioned that entities access is managed per LOD hierarchy. That isn't always optimal. In game, we handle several map region changes, that involve hiding or showing many such hierarchies, leading to numerous small operations for each action and and requires many &lt;code&gt;LinkedEntitiesAccess&lt;/code&gt; &lt;code&gt;MonoBehaviours&lt;/code&gt;.&lt;br&gt;
To address it, we introduced &lt;code&gt;SharedLinkedEntitiesAccess&lt;/code&gt;: if available in a parent, we register entities to it instead of creating a new &lt;code&gt;LinkedEntitiesAccess&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merged drake
&lt;/h3&gt;

&lt;p&gt;We have a lot static drakes, each renderer and LOD group requires short living &lt;code&gt;MonoBehaviour&lt;/code&gt;. That mean a lot of small allocations.&lt;br&gt;
Lot of small allocations is definition of bad memory management.&lt;br&gt;
Solution is fairly simple:&lt;/p&gt;

&lt;p&gt;At build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect all static Drakes from scene&lt;/li&gt;
&lt;li&gt;Create a single &lt;code&gt;MergedDrake&lt;/code&gt; &lt;code&gt;GameObject&lt;/code&gt; with GUID&lt;/li&gt;
&lt;li&gt;Collect all data required to spawn collected Drakes&lt;/li&gt;
&lt;li&gt;Serialize data as binary into file in &lt;code&gt;StreamingAssets&lt;/code&gt;, file is addressed by GUID &lt;/li&gt;
&lt;li&gt;Remove processed Drakes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load data into &lt;code&gt;Temp&lt;/code&gt; native allocation&lt;/li&gt;
&lt;li&gt;Register all meshes and materials&lt;/li&gt;
&lt;li&gt;Spawn Drakes via a bursted job&lt;/li&gt;
&lt;li&gt;Release data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This requires only one &lt;code&gt;MonoBehaviour&lt;/code&gt; with single field (GUID) and, as a bonus, spawning is now bursted. Big win for simple batching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;As you can see, even a 'simple' "runtime entity renderer registration system" can be complex.&lt;br&gt;
At the same time, you may notice that even a straightforward system, when scaled, reveals new optimization opportunities.&lt;br&gt;
To see it in action, try playing &lt;a href="https://store.steampowered.com/app/1466060/Tainted_Grail_The_Fall_of_Avalon/" rel="noopener noreferrer"&gt;Tainted Grail: The Fall of Avalon&lt;/a&gt; and count how many rendering is going on (be aware there is no occulusion culling) :)&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>unity3d</category>
      <category>optimizations</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Deep dive Tainted Grail [0] - Introduction</title>
      <dc:creator>KamilVDono</dc:creator>
      <pubDate>Sat, 31 May 2025 11:54:41 +0000</pubDate>
      <link>https://forem.com/kamilvdono/deep-dive-tainted-grail-0-introduction-5gka</link>
      <guid>https://forem.com/kamilvdono/deep-dive-tainted-grail-0-introduction-5gka</guid>
      <description>&lt;h2&gt;
  
  
  Best place to start is from beginning
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Inheritance
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://store.steampowered.com/app/1466060/Tainted_Grail_The_Fall_of_Avalon/" rel="noopener noreferrer"&gt;&lt;em&gt;Fall of Avalon&lt;/em&gt;&lt;/a&gt; is second digital game in &lt;em&gt;Tainted Grail&lt;/em&gt; IP. First game in series is &lt;a href="https://store.steampowered.com/app/1199030/Tainted_Grail_Conquest/" rel="noopener noreferrer"&gt;&lt;em&gt;Conquest&lt;/em&gt;&lt;/a&gt;. Development history of &lt;em&gt;Conquest&lt;/em&gt; could fill a book, tltr it started as small indie project and grew into medium-sized indie game. We had systems designed for small indie game, but managed to scale them up for medium-sized one. These systems were then copied over to &lt;em&gt;Fall of Avalon&lt;/em&gt; as foundation.&lt;/p&gt;

&lt;p&gt;Another big success we carried over from &lt;em&gt;Conquest&lt;/em&gt; was the team. Team building is topic worthy of PhD thesis and in many ways more impactful than tech itself in determining game's overall success.&lt;/p&gt;

&lt;p&gt;So we had two key components: systems built on top of Unity and team that performed well with Unity and the tools (a subset of those systems).&lt;/p&gt;

&lt;h3&gt;
  
  
  (Not so) Fresh start
&lt;/h3&gt;

&lt;p&gt;You have team proficient with Unity and tools built on top of it. You have tools within Unity and systems (which tools rely on). So it made sense to take these and start new project, especially since &lt;em&gt;Fall of Avalon&lt;/em&gt; wasn’t entirely different from &lt;em&gt;Conquest&lt;/em&gt;. But there was catch: systems were originally built for small indie game. We already scaled them up once for &lt;em&gt;Conquest&lt;/em&gt;, so we thought we could do it again. It kind of worked, but not without issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  Systems
&lt;/h3&gt;

&lt;p&gt;Here is list of main systems at beginning of &lt;em&gt;TG:FoA&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MVC (custom)&lt;/li&gt;
&lt;li&gt;Unnamed event bus (integrated with MVC, so I’ll treat it as part of MVC for now)&lt;/li&gt;
&lt;li&gt;Story graphs (custom node system for story)&lt;/li&gt;
&lt;li&gt;Addressables&lt;/li&gt;
&lt;li&gt;HDRP&lt;/li&gt;
&lt;li&gt;FMod&lt;/li&gt;
&lt;li&gt;Visual scripting&lt;/li&gt;
&lt;li&gt;A* project&lt;/li&gt;
&lt;li&gt;VFX Graph&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The cornerstone: MV(C)
&lt;/h2&gt;

&lt;p&gt;Let’s start with core system inherited from &lt;em&gt;Conquest&lt;/em&gt;: &lt;strong&gt;MV(C)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;M&lt;/strong&gt; - Model: where logical data lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;V&lt;/strong&gt; - View: prefab spawned to visualize model data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C&lt;/strong&gt; - Controller: but not existed :)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S&lt;/strong&gt;(ilient) - Service: global functionality that’s too broad to fit into model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yes, the C part of MVC is missing but not big issue, you can get used to it.&lt;/p&gt;

&lt;p&gt;Let’s dive deeper into actual components, starting from end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Service
&lt;/h3&gt;

&lt;p&gt;Services are straightforward: they provide globally available functionality that’s too specialized to be model. Good examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Regrowable Service&lt;/strong&gt;: lets you register ID and time when you want to be pinged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ID Service&lt;/strong&gt;: provides next unique ID for given model type.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Service is singleton you can query by type. There’s no dependency injection, so any dependencies need to be handled manually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Injected during initialization (in constructor or initialize method).&lt;/li&gt;
&lt;li&gt;Queried when method that requires dependency is called.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  View
&lt;/h3&gt;

&lt;p&gt;View defines prefab, where to spawn it, and contains glue code between Unity objects and models. Exact nature of this glue code isn’t strictly defined, so you might see fat view paired with gameplay model or thin view with UI model and gameplay model.&lt;/p&gt;

&lt;p&gt;This setup introduces clean separation: View handles Unity-specific elements, keeping model layer almost engine-agnostic, which is nice feature. But since this separation is more about model, let’s finish View first.&lt;/p&gt;

&lt;p&gt;Regarding prefabs: they’re optional. If you don’t provide path, new GameObject is created and View MonoBehaviour is attached to it. If you do provide path, prefab is loaded from Resources—yes, old method, but we need synchronous loading and Addressables aren’t reliable for that. So prefabs can’t contain heavy content; heavy content is lazy-loaded via Addressables.&lt;/p&gt;

&lt;p&gt;One thing to note: we don’t rely on Unity’s object lifetime. MVC has its own lifetime management. Spawned prefab with View MonoBehaviour is valid MVC object, even if it’s not yet "Awake" in Unity or has been "Destroyed".&lt;/p&gt;

&lt;h3&gt;
  
  
  Model
&lt;/h3&gt;

&lt;p&gt;Model contains logic and data/state (classic OOP). Entire gameplay is built from models. Often UI creates its own intermediate models to reduce logic inside view.&lt;/p&gt;

&lt;p&gt;Single model can spawn zero to multiple views when it’s created. For example, hero model might spawn 3D model in game world and portrait with HP and MP on HUD. Views can also be removed at any time without disposing of model. In most cases, model-view relationship is managed automatically by MVC.&lt;/p&gt;

&lt;p&gt;Models have four main types of relationships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hierarchy&lt;/strong&gt; - Model can be child of another model, it's main (or at least should be) main type. For example, FastTravel element is child of Location model. Parent knows when child is added or removed, and child’s lifetime is tied to parent—it can’t exist without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard Ref Field&lt;/strong&gt; - Model can have direct reference to another model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak Ref Field&lt;/strong&gt; - Model can have field with another model’s ID, resolving reference each time and checking if it’s still valid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named Relation&lt;/strong&gt; - It is possible to create a class that defines the relationship between models. For example, one Item can be owned by one Character; one Character can own zero, one, or multiple Items. Such relationships have automatic management by MVC. For example, a relationship will be automatically terminated if one of the sides is disposed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While this hierarchy promotes a composition-over-inheritance approach, there's nothing to discourage you from using inheritance.&lt;/p&gt;

&lt;p&gt;As mentioned in View section, models are designed to be engine-agnostic. In &lt;em&gt;Conquest&lt;/em&gt;, where Unity was mostly visualization layer for logic and state, this worked well. But in &lt;em&gt;TG:FoA&lt;/em&gt;, gameplay logic is tightly coupled with physics, visuals, and Unity’s state. So what was strength became weakness: model lifetimes are independent of Unity object lifetimes, but logic is deeply intertwined with Unity. Welcome to lifetime management hell. :)&lt;/p&gt;

&lt;p&gt;Since the Model is a base class, there is no support for structs. While this wasn't an initial concern, it ultimately led to two massive pessimization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The MVC architecture cannot be bursted at all.&lt;/li&gt;
&lt;li&gt;Even temporary allocations are practically impossible with native or stack allocations; mostly only slow managed allocations or static caches are possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This, combined with an 'every-call-is-cache-miss' scenario, results in a massive performance hit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Events
&lt;/h3&gt;

&lt;p&gt;Event system is at core of MVC, so let’s cover it here.&lt;/p&gt;

&lt;p&gt;Events are defined by an ID, generic constraints on the model (specifying that the event can only be triggered on a particular model type or its inheritors), and a payload type.&lt;br&gt;
Any model can trigger any event on any other model (as long as generic constraints are met), and any model can listen to any event on any model. You can also listen for all events of certain type, not just on specific model.&lt;/p&gt;

&lt;p&gt;To clarify, here’s some pseudocode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Event declaration&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IGrounded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TeleportData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Teleported&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IGrounded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TeleportData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Teleported&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Location&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IGrounded&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Teleport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;newPosition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Call event on myself&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Teleported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TeleportData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oldPosition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newPosition&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LocationTeleportListener&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SingularListen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
        &lt;span class="c1"&gt;// Listen for specific model for Events.Teleported&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Teleported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AnyListen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Listen for all event of type Events.Teleported&lt;/span&gt;
        &lt;span class="n"&gt;EventsSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ListenAny&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Teleported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TeleportData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Do something&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 'any event, any model, any listener' aspect might sound like a readability nightmare, but it's not so scary in practice. That said, discovering the implicit connections between models can be tedious at times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Saving (and loading)
&lt;/h3&gt;

&lt;p&gt;Models have built-in support for saving. System automatically dumps eligible models into JSON blob. It uses reflection to determine which fields to include, and then JSON engine serializes them.&lt;br&gt;
Loading is straightforward: read JSON, deserialize models, and call their restoration methods. There's nothing particularly complex about it.&lt;/p&gt;

&lt;p&gt;There's nothing fancy here, but you can probably tell that reflection and JSON are the antithesis of performance. So, you can expect a future post dedicated to saving and loading optimizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other systems
&lt;/h2&gt;

&lt;p&gt;Other systems are more or less publicly available, or were created as part of TG:FoA so it's not necessary to introduce them.&lt;br&gt;
Only the "technological stack" built on our very proprietary baseline required an introduction.&lt;br&gt;
Nevertheless, it's beneficial to show what we ended up with and what will likely be covered in future posts.&lt;/p&gt;

&lt;p&gt;You can find a list of the initial systems at the beginning of the document, and here is a list of the final systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MVC&lt;/li&gt;
&lt;li&gt;Addressables (never again)&lt;/li&gt;
&lt;li&gt;HDRP (with small custom optimizations)&lt;/li&gt;
&lt;li&gt;FMod (with optimizations at Unity glue-code level)&lt;/li&gt;
&lt;li&gt;Visual scripting (never again)&lt;/li&gt;
&lt;li&gt;A* project (with custom optimizations)&lt;/li&gt;
&lt;li&gt;Entities with graphics (with custom optimizations and fixes)&lt;/li&gt;
&lt;li&gt;VFX Graph&lt;/li&gt;
&lt;li&gt;Leshy (custom system for vegetation streaming and rendering)&lt;/li&gt;
&lt;li&gt;Medusa (custom system for static environment assets like cliffs and terrains)&lt;/li&gt;
&lt;li&gt;Drake (custom runtime rendering baking to ECS, with optimizations and systems for OOP interactions)&lt;/li&gt;
&lt;li&gt;Scenes baking (system to merge multiple scenes into one, then split into dynamic and static parts, and flatten hierarchy)&lt;/li&gt;
&lt;li&gt;Kandra (custom system for rendering skeletal meshes with mesh-mesh culling)&lt;/li&gt;
&lt;li&gt;HLODs (custom system for clustering and merging multiple renderers into single simplified mesh, ECS-based rendering)&lt;/li&gt;
&lt;li&gt;Mipmaps streaming (built on Unity’s system, adapted for ECS and other custom systems)&lt;/li&gt;
&lt;li&gt;Binary serialization via source generator&lt;/li&gt;
&lt;li&gt;Various smaller utilities (rain textures, cheap VFX mesh sampling, debug tools, flocks with vertex animations, cheap splatmap sampling, Pix and Superluminal markers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn’t order I’ll cover them in future posts. I plan to discuss custom systems chronologically, starting with Drake and then Leshy.&lt;/p&gt;

&lt;p&gt;In addition to custom systems, I’ll also cover some key optimizations we implemented. Stay tuned for those and future posts.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>unity3d</category>
      <category>optimizations</category>
      <category>deepdive</category>
    </item>
  </channel>
</rss>
