Jekyll2020-10-20T09:46:27+02:00https://www.ronja-tutorials.com/feed.xmlRonja’s Shader TutorialsI'm making shader tutorials for Unity aimed at beginners.Ronja BöhringerGraphics.DrawProcedural2020-09-16T00:00:00+02:002020-09-16T00:00:00+02:00https://www.ronja-tutorials.com/2020/09/16/draw-procedural<p>The <a href="/2020/07/26/compute-shader.html">last tutorial</a> was about compute shader basics, how to generate values, read them back to the cpu and use them. One critical aspect in all that is that copying data from the cpu to the gpu (from the ram to the vram) or back takes some time, so wouldn’t it be neat if there was a way to just render the data directly from the GPU without copying it around? Thats where Graphics.DrawProcedural and similar methods come into play, they allow us to do exactly that. So lets build on the result of that tutorial but without using GameObjects for rendering.</p>
<video width="300" height="300" preload="metadata" autoplay="" loop="" muted="" loading="lazy"><source src="/assets/images/posts/051/result.mp4" type="video/mp4; codecs="avc1.42E01E, mp4a.40.2"" /></video>
<h2 id="buffer-handling-in-c">Buffer handling in C#</h2>
<p>First lets remove all code connected to rendering using GameObjects. We don’t need the output array anymore, not the GameObject instances where we stored the Transforms, we don’t need to create them in the start method and don’t need to update their positions in the Update method.</p>
<p>The way Graphics.DrawProcedural works is that all the shader gets as information is the index of the current vertex. That also means we’ll have to make the vertices available to shader ourselves via more compute buffers and then write a custom shader to read from those buffers. Lets add a mesh and a material to our public class variables so we can set them from the inspector and use their data for rendering. Since we’re filling the mentioned buffers ourselves its not nessecary for that data to come from a mesh or even exist in the ram at any point.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// class variables</span>
<span class="k">public</span> <span class="n">Mesh</span> <span class="n">Mesh</span><span class="p">;</span>
<span class="k">public</span> <span class="n">Material</span> <span class="n">Material</span><span class="p">;</span>
</code></pre></div></div>
<p>In this case we don’t need uv coordinates or normals, so we’ll just create 2 buffers, one for the triangles and one for the positions. The way those work is that each pair of 3 integers in the triangles array define a triangle to be rendered by its index positions in the vertex position array. Just like the previous result array we also need to discard this one at some point (In our case in the <code class="language-plaintext highlighter-rouge">OnDestroy</code> method).</p>
<p>The stride (size of a single variable in the buffer) is simply the size of the base type times the components (1 for scalar values and 3 for 3d vectors). And after creating the buffers we already fill them once at the start.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// class variables</span>
<span class="n">ComputeBuffer</span> <span class="n">meshTriangles</span><span class="p">;</span>
<span class="n">ComputeBuffer</span> <span class="n">meshPositions</span><span class="p">;</span>
</code></pre></div></div>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">///in Start method</span>
<span class="c1">//gpu buffers for the mesh</span>
<span class="kt">int</span><span class="p">[]</span> <span class="n">triangles</span> <span class="p">=</span> <span class="n">Mesh</span><span class="p">.</span><span class="n">triangles</span><span class="p">;</span>
<span class="n">meshTriangles</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ComputeBuffer</span><span class="p">(</span><span class="n">triangles</span><span class="p">.</span><span class="n">Length</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">int</span><span class="p">));</span>
<span class="n">meshTriangles</span><span class="p">.</span><span class="nf">SetData</span><span class="p">(</span><span class="n">triangles</span><span class="p">);</span>
<span class="n">Vector3</span><span class="p">[]</span> <span class="n">positions</span> <span class="p">=</span> <span class="n">Mesh</span><span class="p">.</span><span class="n">vertices</span><span class="p">;</span>
<span class="n">meshPositions</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ComputeBuffer</span><span class="p">(</span><span class="n">positions</span><span class="p">.</span><span class="n">Length</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">float</span><span class="p">)</span> <span class="p">*</span> <span class="m">3</span><span class="p">);</span>
<span class="n">meshPositions</span><span class="p">.</span><span class="nf">SetData</span><span class="p">(</span><span class="n">positions</span><span class="p">);</span>
</code></pre></div></div>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">void</span> <span class="nf">OnDestroy</span><span class="p">()</span>
<span class="p">{</span>
<span class="n">resultBuffer</span><span class="p">.</span><span class="nf">Dispose</span><span class="p">();</span>
<span class="n">meshTriangles</span><span class="p">.</span><span class="nf">Dispose</span><span class="p">();</span>
<span class="n">meshPositions</span><span class="p">.</span><span class="nf">Dispose</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>
<p>To allow the material to read from the buffers we just call <code class="language-plaintext highlighter-rouge">SetBuffer</code> just like we’d call <code class="language-plaintext highlighter-rouge">SetColor</code> or <code class="language-plaintext highlighter-rouge">SetTexture</code>.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">///in Start method</span>
<span class="c1">//give data to shaders</span>
<span class="n">Material</span><span class="p">.</span><span class="nf">SetBuffer</span><span class="p">(</span><span class="s">"SphereLocations"</span><span class="p">,</span> <span class="n">resultBuffer</span><span class="p">);</span>
<span class="n">Material</span><span class="p">.</span><span class="nf">SetBuffer</span><span class="p">(</span><span class="s">"Triangles"</span><span class="p">,</span> <span class="n">meshTriangles</span><span class="p">);</span>
<span class="n">Material</span><span class="p">.</span><span class="nf">SetBuffer</span><span class="p">(</span><span class="s">"Positions"</span><span class="p">,</span> <span class="n">meshPositions</span><span class="p">);</span>
</code></pre></div></div>
<p>Now we have most things we need to call the DrawProcedural method, but if you take a look at it one of the non-optional arguments we havent accounted for is still the bounds of what we’re drawing. This is here for frustum culling and in our case we can quickly calculate them by creating 40x40 units bounds at the origin and passing them. The reason for the bounds in this case are the compute shader spits out positions in a sphere with a 20 unit radius around the origin.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">///class variable</span>
<span class="n">Bounds</span> <span class="n">bounds</span><span class="p">;</span>
</code></pre></div></div>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">///in start method</span>
<span class="c1">//bounds for frustum culling (20 is a magic number (radius) from the compute shader)</span>
<span class="n">bounds</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Bounds</span><span class="p">(</span><span class="n">Vector3</span><span class="p">.</span><span class="n">zero</span><span class="p">,</span> <span class="n">Vector3</span><span class="p">.</span><span class="n">one</span> <span class="p">*</span> <span class="m">20</span><span class="p">);</span>
</code></pre></div></div>
<p>After the material and the bounds are passed the next argument is the topology. Changing that allows us to draw lines, dots or even quads (though they’re slow), but in most cases we like to stick to triangles and its also what our mesh data gave us. Then we pass in the length of the triangle array since in this case we want to run the vertex stage once per trangle corner, not once per position. And we pass the amount of spheres as the instance count. We could also queue the triangle amount times the sphere amount and figure out which sphere we’re in in the shader but this approach makes writing our shader a whole bit easier.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">///in Update</span>
<span class="c1">//draw result</span>
<span class="n">Graphics</span><span class="p">.</span><span class="nf">DrawProcedural</span><span class="p">(</span><span class="n">Material</span><span class="p">,</span> <span class="n">bounds</span><span class="p">,</span> <span class="n">MeshTopology</span><span class="p">.</span><span class="n">Triangles</span><span class="p">,</span> <span class="n">meshTriangles</span><span class="p">.</span><span class="n">count</span><span class="p">,</span> <span class="n">SphereAmount</span><span class="p">);</span>
</code></pre></div></div>
<h2 id="the-shader">The Shader</h2>
<p>The shader is actually pretty straightforward. We can use the the result of <a href="/basics.md">the basics series</a> to start. Then the first few changes are that we throw out the texture rendering and to mark the color property with <code class="language-plaintext highlighter-rouge">[HDR]</code> to allow us to go to values beyond 1 and play with bloom up there.</p>
<p>Next we can just delete the <code class="language-plaintext highlighter-rouge">appdata</code> and <code class="language-plaintext highlighter-rouge">v2f</code> structs. We’re not getting any data from a mesh and we’re not passing any data to the fragment stage. Instead we add our 3 buffers, since we don’t have to write to them we can just make them StructuredBuffers without the read-write functionality.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//buffers</span>
<span class="n">StructuredBuffer</span><span class="o"><</span><span class="n">float3</span><span class="o">></span> <span class="n">SphereLocations</span><span class="p">;</span>
<span class="n">StructuredBuffer</span><span class="o"><</span><span class="kt">int</span><span class="o">></span> <span class="n">Triangles</span><span class="p">;</span>
<span class="n">StructuredBuffer</span><span class="o"><</span><span class="n">float3</span><span class="o">></span> <span class="n">Positions</span><span class="p">;</span>
</code></pre></div></div>
<p>Then we change the return type of our vertex function to <code class="language-plaintext highlighter-rouge">float4</code> and give it the <code class="language-plaintext highlighter-rouge">: SV_POSITION</code> attribute, similarly to the <code class="language-plaintext highlighter-rouge">SV_TARGET</code> of the fragment function. The arguments our vertex function now takes are the vertex id as well as the instance id, marked via <code class="language-plaintext highlighter-rouge">SV_VertexID</code> and <code class="language-plaintext highlighter-rouge">SV_InstanceID</code>.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//the vertex shader function</span>
<span class="n">float4</span> <span class="n">vert</span><span class="p">(</span><span class="kt">uint</span> <span class="n">vertex_id</span><span class="o">:</span> <span class="n">SV_VertexID</span><span class="p">,</span> <span class="kt">uint</span> <span class="n">instance_id</span><span class="o">:</span> <span class="n">SV_InstanceID</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">{</span>
<span class="c1">//return position?</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Our first step is now to get the index of the position in the position list thats saved in the triangle buffer, just like in the compute shader we can access the buffer here like we would with an array in most languages. After getting the position index based on the vertex id, we use that to get the actual position from the position buffer. Then we also add the location of the current sphere by using instance id to the position.</p>
<p>And lastly we transform the position from worldspace to clip space by multiplying the view-projection matrix with it. This isn’t anything we haven’t done yet, it was just hidden in <code class="language-plaintext highlighter-rouge">UnityObjectToClipPos</code> until now, which internally only does <code class="language-plaintext highlighter-rouge">mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));</code> where the inner <code class="language-plaintext highlighter-rouge">mul</code> transforms the position to the world coordinates and the outer one to clip coordinates.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//the vertex shader function</span>
<span class="n">float4</span> <span class="n">vert</span><span class="p">(</span><span class="kt">uint</span> <span class="n">vertex_id</span><span class="o">:</span> <span class="n">SV_VertexID</span><span class="p">,</span> <span class="kt">uint</span> <span class="n">instance_id</span><span class="o">:</span> <span class="n">SV_InstanceID</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">{</span>
<span class="c1">//get vertex position</span>
<span class="kt">int</span> <span class="n">positionIndex</span> <span class="o">=</span> <span class="n">Triangles</span><span class="p">[</span><span class="n">vertex_id</span><span class="p">];</span>
<span class="n">float3</span> <span class="n">position</span> <span class="o">=</span> <span class="n">Positions</span><span class="p">[</span><span class="n">positionIndex</span><span class="p">];</span>
<span class="c1">//add sphere position</span>
<span class="n">position</span> <span class="o">+=</span> <span class="n">SphereLocations</span><span class="p">[</span><span class="n">instance_id</span><span class="p">];</span>
<span class="c1">//convert the vertex position from world space to clip space</span>
<span class="k">return</span> <span class="n">mul</span><span class="p">(</span><span class="n">UNITY_MATRIX_VP</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">position</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And with all of that you rendered objects at positions the cpu never knew about! And depending on your situation that can be pretty fast.</p>
<video width="300" height="300" preload="metadata" autoplay="" loop="" muted="" loading="lazy"><source src="/assets/images/posts/051/result.mp4" type="video/mp4; codecs="avc1.42E01E, mp4a.40.2"" /></video>
<h2 id="tiny-tweaks">Tiny Tweaks</h2>
<p>Because we left the world of GameObjects and Transforms theres no easy way to resize objects in this thing, which is a thing I want to do. So lets add that functionality by just multiplying all positions with a value before putting them into the buffer with a linq function.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Vector3</span><span class="p">[]</span> <span class="n">positions</span> <span class="p">=</span> <span class="n">Mesh</span><span class="p">.</span><span class="n">vertices</span><span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">p</span> <span class="p">=></span> <span class="n">p</span> <span class="p">*</span> <span class="n">Scale</span><span class="p">).</span><span class="nf">ToArray</span><span class="p">();</span> <span class="c1">//adjust scale here</span>
<span class="n">meshPositions</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ComputeBuffer</span><span class="p">(</span><span class="n">positions</span><span class="p">.</span><span class="n">Length</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">float</span><span class="p">)</span> <span class="p">*</span> <span class="m">3</span><span class="p">);</span>
<span class="n">meshPositions</span><span class="p">.</span><span class="nf">SetData</span><span class="p">(</span><span class="n">positions</span><span class="p">);</span>
</code></pre></div></div>
<p>Also as a tiny note microsoft is very clear in their docs that they prefer the content type of structured buffers to have a stride thats a power of 2 (more specifically a value 128 is dividable by), but in my playing around with float3 and float4 they seemed very similar in performance. Do with that information what you will.</p>
<p>Oh, and I moved the the calculation of the thread group count as well as the setting of the result buffer to the compute kernel to the Start method.</p>
<h2 id="sources">Sources</h2>
<h3 id="proceduralcomputespherescs">ProceduralComputeSpheres.cs</h3>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/051_DrawProcedural/ProceduralComputeSpheres.cs">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/051_DrawProcedural/ProceduralComputeSpheres.cs</a></li>
</ul>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Collections</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Collections.Generic</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Linq</span><span class="p">;</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ProceduralComputeSpheres</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
<span class="c1">//rough outline for data</span>
<span class="k">public</span> <span class="kt">int</span> <span class="n">SphereAmount</span> <span class="p">=</span> <span class="m">17</span><span class="p">;</span>
<span class="k">public</span> <span class="n">ComputeShader</span> <span class="n">Shader</span><span class="p">;</span>
<span class="c1">//what is rendered</span>
<span class="k">public</span> <span class="n">Mesh</span> <span class="n">Mesh</span><span class="p">;</span>
<span class="k">public</span> <span class="n">Material</span> <span class="n">Material</span><span class="p">;</span>
<span class="k">public</span> <span class="kt">float</span> <span class="n">Scale</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span>
<span class="c1">//internal data</span>
<span class="n">ComputeBuffer</span> <span class="n">resultBuffer</span><span class="p">;</span>
<span class="n">ComputeBuffer</span> <span class="n">meshTriangles</span><span class="p">;</span>
<span class="n">ComputeBuffer</span> <span class="n">meshPositions</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">kernel</span><span class="p">;</span>
<span class="kt">uint</span> <span class="n">threadGroupSize</span><span class="p">;</span>
<span class="n">Bounds</span> <span class="n">bounds</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">threadGroups</span><span class="p">;</span>
<span class="k">void</span> <span class="nf">Start</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">//program we're executing</span>
<span class="n">kernel</span> <span class="p">=</span> <span class="n">Shader</span><span class="p">.</span><span class="nf">FindKernel</span><span class="p">(</span><span class="s">"Spheres"</span><span class="p">);</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">GetKernelThreadGroupSizes</span><span class="p">(</span><span class="n">kernel</span><span class="p">,</span> <span class="k">out</span> <span class="n">threadGroupSize</span><span class="p">,</span> <span class="k">out</span> <span class="n">_</span><span class="p">,</span> <span class="k">out</span> <span class="n">_</span><span class="p">);</span>
<span class="c1">//amount of thread groups we'll need to dispatch</span>
<span class="n">threadGroups</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span> <span class="p">((</span><span class="n">SphereAmount</span> <span class="p">+</span> <span class="p">(</span><span class="n">threadGroupSize</span> <span class="p">-</span> <span class="m">1</span><span class="p">))</span> <span class="p">/</span> <span class="n">threadGroupSize</span><span class="p">);</span>
<span class="c1">//gpu buffer for the sphere positions</span>
<span class="n">resultBuffer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ComputeBuffer</span><span class="p">(</span><span class="n">SphereAmount</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">float</span><span class="p">)</span> <span class="p">*</span> <span class="m">3</span><span class="p">);</span>
<span class="c1">//gpu buffers for the mesh</span>
<span class="kt">int</span><span class="p">[]</span> <span class="n">triangles</span> <span class="p">=</span> <span class="n">Mesh</span><span class="p">.</span><span class="n">triangles</span><span class="p">;</span>
<span class="n">meshTriangles</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ComputeBuffer</span><span class="p">(</span><span class="n">triangles</span><span class="p">.</span><span class="n">Length</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">int</span><span class="p">));</span>
<span class="n">meshTriangles</span><span class="p">.</span><span class="nf">SetData</span><span class="p">(</span><span class="n">triangles</span><span class="p">);</span>
<span class="n">Vector3</span><span class="p">[]</span> <span class="n">positions</span> <span class="p">=</span> <span class="n">Mesh</span><span class="p">.</span><span class="n">vertices</span><span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">p</span> <span class="p">=></span> <span class="n">p</span> <span class="p">*</span> <span class="n">Scale</span><span class="p">).</span><span class="nf">ToArray</span><span class="p">();</span> <span class="c1">//adjust scale here</span>
<span class="n">meshPositions</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ComputeBuffer</span><span class="p">(</span><span class="n">positions</span><span class="p">.</span><span class="n">Length</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">float</span><span class="p">)</span> <span class="p">*</span> <span class="m">3</span><span class="p">);</span>
<span class="n">meshPositions</span><span class="p">.</span><span class="nf">SetData</span><span class="p">(</span><span class="n">positions</span><span class="p">);</span>
<span class="c1">//give data to shaders</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">SetBuffer</span><span class="p">(</span><span class="n">kernel</span><span class="p">,</span> <span class="s">"Result"</span><span class="p">,</span> <span class="n">resultBuffer</span><span class="p">);</span>
<span class="n">Material</span><span class="p">.</span><span class="nf">SetBuffer</span><span class="p">(</span><span class="s">"SphereLocations"</span><span class="p">,</span> <span class="n">resultBuffer</span><span class="p">);</span>
<span class="n">Material</span><span class="p">.</span><span class="nf">SetBuffer</span><span class="p">(</span><span class="s">"Triangles"</span><span class="p">,</span> <span class="n">meshTriangles</span><span class="p">);</span>
<span class="n">Material</span><span class="p">.</span><span class="nf">SetBuffer</span><span class="p">(</span><span class="s">"Positions"</span><span class="p">,</span> <span class="n">meshPositions</span><span class="p">);</span>
<span class="c1">//bounds for frustum culling (20 is a magic number (radius) from the compute shader)</span>
<span class="n">bounds</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Bounds</span><span class="p">(</span><span class="n">Vector3</span><span class="p">.</span><span class="n">zero</span><span class="p">,</span> <span class="n">Vector3</span><span class="p">.</span><span class="n">one</span> <span class="p">*</span> <span class="m">20</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">void</span> <span class="nf">Update</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">//calculate positions</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">SetFloat</span><span class="p">(</span><span class="s">"Time"</span><span class="p">,</span> <span class="n">Time</span><span class="p">.</span><span class="n">time</span><span class="p">);</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">Dispatch</span><span class="p">(</span><span class="n">kernel</span><span class="p">,</span> <span class="n">threadGroups</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span>
<span class="c1">//draw result</span>
<span class="n">Graphics</span><span class="p">.</span><span class="nf">DrawProcedural</span><span class="p">(</span><span class="n">Material</span><span class="p">,</span> <span class="n">bounds</span><span class="p">,</span> <span class="n">MeshTopology</span><span class="p">.</span><span class="n">Triangles</span><span class="p">,</span> <span class="n">meshTriangles</span><span class="p">.</span><span class="n">count</span><span class="p">,</span> <span class="n">SphereAmount</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">void</span> <span class="nf">OnDestroy</span><span class="p">()</span>
<span class="p">{</span>
<span class="n">resultBuffer</span><span class="p">.</span><span class="nf">Dispose</span><span class="p">();</span>
<span class="n">meshTriangles</span><span class="p">.</span><span class="nf">Dispose</span><span class="p">();</span>
<span class="n">meshPositions</span><span class="p">.</span><span class="nf">Dispose</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="proceduralspheresshader">ProceduralSpheres.shader</h3>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/051_DrawProcedural/ProceduralSpheres.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/051_DrawProcedural/ProceduralSpheres.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/051_ProceduralSpheres"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="p">[</span><span class="n">HDR</span><span class="p">]</span> <span class="n">_Color</span> <span class="p">(</span><span class="s">"Tint"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span> <span class="p">}</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//include useful shader functions</span>
<span class="cp">#include "UnityCG.cginc"
</span>
<span class="c1">//define vertex and fragment shader functions</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//tint of the texture</span>
<span class="n">fixed4</span> <span class="n">_Color</span><span class="p">;</span>
<span class="c1">//buffers</span>
<span class="n">StructuredBuffer</span><span class="o"><</span><span class="n">float3</span><span class="o">></span> <span class="n">SphereLocations</span><span class="p">;</span>
<span class="n">StructuredBuffer</span><span class="o"><</span><span class="kt">int</span><span class="o">></span> <span class="n">Triangles</span><span class="p">;</span>
<span class="n">StructuredBuffer</span><span class="o"><</span><span class="n">float3</span><span class="o">></span> <span class="n">Positions</span><span class="p">;</span>
<span class="c1">//the vertex shader function</span>
<span class="n">float4</span> <span class="n">vert</span><span class="p">(</span><span class="kt">uint</span> <span class="n">vertex_id</span><span class="o">:</span> <span class="n">SV_VertexID</span><span class="p">,</span> <span class="kt">uint</span> <span class="n">instance_id</span><span class="o">:</span> <span class="n">SV_InstanceID</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">{</span>
<span class="c1">//get vertex position</span>
<span class="kt">int</span> <span class="n">positionIndex</span> <span class="o">=</span> <span class="n">Triangles</span><span class="p">[</span><span class="n">vertex_id</span><span class="p">];</span>
<span class="n">float3</span> <span class="n">position</span> <span class="o">=</span> <span class="n">Positions</span><span class="p">[</span><span class="n">positionIndex</span><span class="p">];</span>
<span class="c1">//add sphere position</span>
<span class="n">position</span> <span class="o">+=</span> <span class="n">SphereLocations</span><span class="p">[</span><span class="n">instance_id</span><span class="p">];</span>
<span class="c1">//convert the vertex position from world space to clip space</span>
<span class="k">return</span> <span class="n">mul</span><span class="p">(</span><span class="n">UNITY_MATRIX_VP</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">position</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
<span class="p">}</span>
<span class="c1">//the fragment shader function</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">()</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//return the final color to be drawn on screen</span>
<span class="k">return</span> <span class="n">_Color</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">Fallback</span> <span class="s">"VertexLit"</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="basiccomputecompute-unchanged">BasicCompute.compute (unchanged)</h3>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/051_DrawProcedural/BasicCompute.compute">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/051_DrawProcedural/BasicCompute.compute</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Each #kernel tells which function to compile; you can have many kernels</span>
<span class="cp">#pragma kernel Spheres
</span>
<span class="cp">#include "Random.cginc"
</span>
<span class="c1">//variables</span>
<span class="n">RWStructuredBuffer</span><span class="o"><</span><span class="n">float3</span><span class="o">></span> <span class="n">Result</span><span class="p">;</span>
<span class="k">uniform</span> <span class="kt">float</span> <span class="n">Time</span><span class="p">;</span>
<span class="p">[</span><span class="n">numthreads</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)]</span>
<span class="kt">void</span> <span class="nf">Spheres</span> <span class="p">(</span><span class="n">uint3</span> <span class="n">id</span> <span class="o">:</span> <span class="n">SV_DispatchThreadID</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">//generate 2 orthogonal vectors</span>
<span class="n">float3</span> <span class="n">baseDir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">rand1dTo3d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">rand1dTo1d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">)</span><span class="o">*</span><span class="mi">0</span><span class="p">.</span><span class="mi">9</span><span class="o">+</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="p">);</span>
<span class="n">float3</span> <span class="n">orthogonal</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">cross</span><span class="p">(</span><span class="n">baseDir</span><span class="p">,</span> <span class="n">rand1dTo3d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="mi">7</span><span class="p">.</span><span class="mi">1393</span><span class="p">)</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">))</span> <span class="o">*</span> <span class="p">(</span><span class="n">rand1dTo1d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="o">+</span><span class="mi">3</span><span class="p">.</span><span class="mi">7443</span><span class="p">)</span><span class="o">*</span><span class="mi">0</span><span class="p">.</span><span class="mi">9</span><span class="o">+</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="p">);</span>
<span class="c1">//scale the time and give it a random offset</span>
<span class="kt">float</span> <span class="n">scaledTime</span> <span class="o">=</span> <span class="n">Time</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="n">rand1dTo1d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">*</span> <span class="mi">712</span><span class="p">.</span><span class="mi">131234</span><span class="p">;</span>
<span class="c1">//calculate a vector based on vectors</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">baseDir</span> <span class="o">*</span> <span class="n">sin</span><span class="p">(</span><span class="n">scaledTime</span><span class="p">)</span> <span class="o">+</span> <span class="n">orthogonal</span> <span class="o">*</span> <span class="n">cos</span><span class="p">(</span><span class="n">scaledTime</span><span class="p">);</span>
<span class="n">Result</span><span class="p">[</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">]</span> <span class="o">=</span> <span class="n">float3</span><span class="p">(</span><span class="n">dir</span> <span class="o">*</span> <span class="mi">20</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>Ronja BöhringerThe last tutorial was about compute shader basics, how to generate values, read them back to the cpu and use them. One critical aspect in all that is that copying data from the cpu to the gpu (from the ram to the vram) or back takes some time, so wouldn’t it be neat if there was a way to just render the data directly from the GPU without copying it around? Thats where Graphics.DrawProcedural and similar methods come into play, they allow us to do exactly that. So lets build on the result of that tutorial but without using GameObjects for rendering.Compute Shader2020-07-26T00:00:00+02:002020-07-26T00:00:00+02:00https://www.ronja-tutorials.com/2020/07/26/compute-shader<p>So far we always used shaders to render with a fixed pipeline into textures, but modern graphics card can do way more than just that (sometimes they’re also referred to as GPGPU for “general purpose graphics processing unit” because of that). To do things that arent in the fix pipeline we’re using so far we have to use compute shaders.</p>
<p>If you’re asking yourself why we’d do that, the CPU is performant enough, especially once we use multithreading then I’m here to tell you that you’re 100% correct. You don’t need the GPU for you non graphics tasks. Using it will give you way more confusing error behaviour. The debugger wont give you as nice information and you can’t have breakpoints. Optimizing is weirder since you always have to think about what data you’re pushing around. The paralell nature of the GPU forces you to think way differently… If you don’t need compute shaders, especially when you’re not experienced, think about why you want to and if it’s making your more work than its worth.</p>
<p>If you’re still here, lets get going. In Unity you can check whether your GPU supports compute shaders by checking <a href="https://docs.unity3d.com/ScriptReference/SystemInfo-supportsComputeShaders.html">SystemInfo.supportsComputeShaders</a>.</p>
<p><img src="/assets/images/posts/050/result.gif" alt="" /></p>
<h2 id="basic-compute-shader">Basic Compute Shader</h2>
<p>We can get a simple compute shader by just doing <code class="language-plaintext highlighter-rouge">rclick>Create>Shader>Compute Shader</code>. The default shader does a few calculations to write a pattern into a texture, but for this tutorial I want to go one step simpler and just write positions into an array.</p>
<p>In compute land an array we can write into is a <code class="language-plaintext highlighter-rouge">RWStructuredBuffer</code> with a certain type (<code class="language-plaintext highlighter-rouge">StructuredBuffer</code> is a array we can only read from). This type can be next to any type including vectors or structs. In our case we’ll use a float3 vector.</p>
<p>The function that calculates stuff is called the kernel. We have to add a <code class="language-plaintext highlighter-rouge">numthreads</code> attribute in front of the kernel and the kernel takes in one argument which tells us which iteration of the kernel we’re in. In our case we define the number of threads as 64 threads in the <code class="language-plaintext highlighter-rouge">x</code> dimension and 1 in both <code class="language-plaintext highlighter-rouge">y</code> and <code class="language-plaintext highlighter-rouge">z</code>. Those values should work for most platforms that support compute shaders and its in 1 dimension so we dont have to do any rethinking when writing to our 1d array. The input argument is also 3d for that reason but for now we only care about the x part. We do have to mark it as <code class="language-plaintext highlighter-rouge">SV_DispatchThreadID</code> though so the correct value is assigned to it.</p>
<p>To tell unity which functions are just regular functions that are called from somewhere else and which ones are the kernel functions we add a pragma statement, so <code class="language-plaintext highlighter-rouge">#pragma kernel <functionname></code>. Its possible to have multiple kernels per shader file.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Each #kernel tells which function to compile; you can have many kernels</span>
<span class="cp">#pragma kernel Spheres
</span>
<span class="c1">//variables</span>
<span class="n">RWStructuredBuffer</span><span class="o"><</span><span class="n">float3</span><span class="o">></span> <span class="n">Result</span><span class="p">;</span>
<span class="p">[</span><span class="n">numthreads</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)]</span>
<span class="kt">void</span> <span class="nf">Spheres</span> <span class="p">(</span><span class="n">uint3</span> <span class="n">id</span> <span class="o">:</span> <span class="n">SV_DispatchThreadID</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">//compute shader code.</span>
<span class="p">}</span>
</code></pre></div></div>
<p>As a starting point I’ll just let the program write a id,0,0 positions in the buffer we set up.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">numthreads</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)]</span>
<span class="kt">void</span> <span class="nf">Spheres</span> <span class="p">(</span><span class="n">uint3</span> <span class="n">id</span> <span class="o">:</span> <span class="n">SV_DispatchThreadID</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">Result</span><span class="p">[</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">]</span> <span class="o">=</span> <span class="n">float3</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="executing-compute-shaders">Executing Compute Shaders</h2>
<p>Unlike graphics shaders compute shaders can’t just be assigned to a material and via that to a object. Instead we trigger the execution ourselves from c# code.</p>
<p>In the C# component we create we reference our compute shader via a variable of the type <code class="language-plaintext highlighter-rouge">ComputeShader</code>. We also create a integer to store the identifier of our kernel after getting it once in the start method. To get the kernel identifier we call the <code class="language-plaintext highlighter-rouge">FindKernel(<kernelname>)</code> function on our compute shader. And after getting the kernel identifier we can use it to get the size of each thread group (thats equal to our <code class="language-plaintext highlighter-rouge">numthreads</code> in the shader). We store the x size in a variable and discard the others by passing in <code class="language-plaintext highlighter-rouge">_</code> as a output variable.</p>
<p>Lets also make the length of the buffer we’re filling a public property so we can change that in the future. With that information available we can also create the gpu array we’ll write to as a <code class="language-plaintext highlighter-rouge">ComputeBuffer</code>. Its constructor takes in the amount of elements as the first parameter and the size of its content as the second parameter. Since we’re using float3 on the shader side, we can get the size(in bytes) of a float with the <code class="language-plaintext highlighter-rouge">sizeof</code> function and multiply the result by 3(getting the size of a Vector3 or a float3 of the new mathematics lib is also possible, but only in a unsafe context and that sounds scary(it isn’t really, but whatever)). Paralell to the ComputeBuffer lets also create a regular Vector3 array of the same size, we’ll use it later to copy the data back to the ram where we can use it. If we want to write clean code (we kinda do) we should also call <code class="language-plaintext highlighter-rouge">Dispose</code> on our buffer when the component is destroyed so unity can do garbage collection, so lets add that to the OnDestroy method.</p>
<p>With all of that set up we can use the shader in the update method. First we declare set the buffer of the shader to be our buffer, this buffer is set per kernel so we also have to pass in our kernel identifier.
To dispatch the shader we first calculate how many threadgroups we need, in our case we want the amount of threads to be the length of the array, so the thread groups should be that amount divided by the thread size rounded up. When dealing with integers the easiest way of doing a division and getting the rounded up result is to add the divisor minus one before the division, that adds 1 to the result unless the dividend is a exact multiple of the divisor.
After thats done we can dispatch the shader and tell it how many thread groups it should run (the amount we just calculated in <code class="language-plaintext highlighter-rouge">x</code>, and 1 in <code class="language-plaintext highlighter-rouge">y</code> and <code class="language-plaintext highlighter-rouge">z</code>). And then we can already get the data out of the buffer into ram we can work with in C# with the aptly named <code class="language-plaintext highlighter-rouge">GetData</code> function.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">BasicComputeSpheres</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
<span class="k">public</span> <span class="kt">int</span> <span class="n">SphereAmount</span> <span class="p">=</span> <span class="m">17</span><span class="p">;</span>
<span class="k">public</span> <span class="n">ComputeShader</span> <span class="n">Shader</span><span class="p">;</span>
<span class="n">ComputeBuffer</span> <span class="n">resultBuffer</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">kernel</span><span class="p">;</span>
<span class="kt">uint</span> <span class="n">threadGroupSize</span><span class="p">;</span>
<span class="n">Vector3</span><span class="p">[]</span> <span class="n">output</span><span class="p">;</span>
<span class="k">void</span> <span class="nf">Start</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">//program we're executing</span>
<span class="n">kernel</span> <span class="p">=</span> <span class="n">Shader</span><span class="p">.</span><span class="nf">FindKernel</span><span class="p">(</span><span class="s">"Spheres"</span><span class="p">);</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">GetKernelThreadGroupSizes</span><span class="p">(</span><span class="n">kernel</span><span class="p">,</span> <span class="k">out</span> <span class="n">threadGroupSize</span><span class="p">,</span> <span class="k">out</span> <span class="n">_</span><span class="p">,</span> <span class="k">out</span> <span class="n">_</span><span class="p">);</span>
<span class="c1">//buffer on the gpu in the ram</span>
<span class="n">resultBuffer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ComputeBuffer</span><span class="p">(</span><span class="n">SphereAmount</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">float</span><span class="p">)</span> <span class="p">*</span> <span class="m">3</span><span class="p">);</span>
<span class="n">output</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Vector3</span><span class="p">[</span><span class="n">SphereAmount</span><span class="p">];</span>
<span class="p">}</span>
<span class="k">void</span> <span class="nf">Update</span><span class="p">()</span>
<span class="p">{</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">SetBuffer</span><span class="p">(</span><span class="n">kernel</span><span class="p">,</span> <span class="s">"Result"</span><span class="p">,</span> <span class="n">resultBuffer</span><span class="p">);</span>
<span class="kt">int</span> <span class="n">threadGroups</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span> <span class="p">((</span><span class="n">SphereAmount</span> <span class="p">+</span> <span class="p">(</span><span class="n">threadGroupSize</span> <span class="p">-</span> <span class="m">1</span><span class="p">))</span> <span class="p">/</span> <span class="n">threadGroupSize</span><span class="p">);</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">Dispatch</span><span class="p">(</span><span class="n">kernel</span><span class="p">,</span> <span class="n">threadGroups</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span>
<span class="n">resultBuffer</span><span class="p">.</span><span class="nf">GetData</span><span class="p">(</span><span class="n">output</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">void</span> <span class="nf">OnDestroy</span><span class="p">()</span>
<span class="p">{</span>
<span class="n">resultBuffer</span><span class="p">.</span><span class="nf">Dispose</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now we have the data but we can’t see it. There are ways of rendering the buffer directly on the GPU, but this is not the tutorial for that, instead I decided to instantiate a bunch of prefabs and use them for visualisation. The instanced transforms are saved in a private array which is created and filled with new prefab instances in the start method. The length of the array is the same length of the buffer.</p>
<p>In update method we then copy the positions from the output struct to the local position of the objects.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// in start method</span>
<span class="c1">//spheres we use for visualisation</span>
<span class="n">instances</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Transform</span><span class="p">[</span><span class="n">SphereAmount</span><span class="p">];</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p"><</span> <span class="n">SphereAmount</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
<span class="p">{</span>
<span class="n">instances</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">=</span> <span class="nf">Instantiate</span><span class="p">(</span><span class="n">Prefab</span><span class="p">,</span> <span class="n">transform</span><span class="p">).</span><span class="n">transform</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//in update method</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p"><</span> <span class="n">instances</span><span class="p">.</span><span class="n">Length</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
<span class="n">instances</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">localPosition</span> <span class="p">=</span> <span class="n">output</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/050/Row.png" alt="" /></p>
<h2 id="a-tiny-bit-more-complex-compute-shader">A tiny bit more complex Compute Shader</h2>
<p>The rest of this is just to make stuff look nice, its just plain hlsl like in all my other tutorials.</p>
<p>In the Compute Shader I first include the functions from <a href="/2018/09/02/white-noise.html">my tutorial on randomness</a> and add a new variable called time.
In the kernel function I get a random vector(based on the kernel index), normalize it and get it to a random length between 0.1 and 1 (if I let it go too short bad math can happen too easily and some points become NaN). Then I generate a new vector thats orthogonal to that one by taking the cross product between the vector and a different random vector(this isn’t guaranteed to work, if the vectors are paralell the cross product is <code class="language-plaintext highlighter-rouge">(0,0,0)</code> and can’t e normalized, but it works well enough) and give its length the same treatment to make it be between 0.1 and 0.9. The random looking numbers I add to the inputs are to avoid some of the symmetry so not all random functions return the same result. Then I get a time variable by multiplying the time by 2(that 2 could be a uniform value if you want to adjust the speed manually) and give it a offset by a random value multiplied by some big-ish odd number.</p>
<p>Those values can then be combined by multiplying one of the vectors by the sine of the time and the other by the cosine of the time and adding the 2 results. I then also multiplied it by 20 to make it bigger, but you should also consider using a property settable from outside here(do as I say not as I do).</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Each #kernel tells which function to compile; you can have many kernels</span>
<span class="cp">#pragma kernel Spheres
</span>
<span class="cp">#include "Random.cginc"
</span>
<span class="c1">//variables</span>
<span class="n">RWStructuredBuffer</span><span class="o"><</span><span class="n">float3</span><span class="o">></span> <span class="n">Result</span><span class="p">;</span>
<span class="k">uniform</span> <span class="kt">float</span> <span class="n">Time</span><span class="p">;</span>
<span class="p">[</span><span class="n">numthreads</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)]</span>
<span class="kt">void</span> <span class="nf">Spheres</span> <span class="p">(</span><span class="n">uint3</span> <span class="n">id</span> <span class="o">:</span> <span class="n">SV_DispatchThreadID</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">//generate 2 orthogonal vectors</span>
<span class="n">float3</span> <span class="n">baseDir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">rand1dTo3d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">rand1dTo1d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">)</span><span class="o">*</span><span class="mi">0</span><span class="p">.</span><span class="mi">9</span><span class="o">+</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="p">);</span>
<span class="n">float3</span> <span class="n">orthogonal</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">cross</span><span class="p">(</span><span class="n">baseDir</span><span class="p">,</span> <span class="n">rand1dTo3d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="mi">7</span><span class="p">.</span><span class="mi">1393</span><span class="p">)</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">))</span> <span class="o">*</span> <span class="p">(</span><span class="n">rand1dTo1d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="o">+</span><span class="mi">3</span><span class="p">.</span><span class="mi">7443</span><span class="p">)</span><span class="o">*</span><span class="mi">0</span><span class="p">.</span><span class="mi">9</span><span class="o">+</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="p">);</span>
<span class="c1">//scale the time and give it a random offset</span>
<span class="kt">float</span> <span class="n">scaledTime</span> <span class="o">=</span> <span class="n">Time</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="n">rand1dTo1d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">*</span> <span class="mi">712</span><span class="p">.</span><span class="mi">131234</span><span class="p">;</span>
<span class="c1">//calculate a vector based on vectors</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">baseDir</span> <span class="o">*</span> <span class="n">sin</span><span class="p">(</span><span class="n">scaledTime</span><span class="p">)</span> <span class="o">+</span> <span class="n">orthogonal</span> <span class="o">*</span> <span class="n">cos</span><span class="p">(</span><span class="n">scaledTime</span><span class="p">);</span>
<span class="n">Result</span><span class="p">[</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">]</span> <span class="o">=</span> <span class="n">dir</span> <span class="o">*</span> <span class="mi">20</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Then all thats missing is passing in the time from the C# code and you should have a nice orbiting swarm.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span><span class="p">.</span><span class="nf">SetFloat</span><span class="p">(</span><span class="s">"Time"</span><span class="p">,</span> <span class="n">Time</span><span class="p">.</span><span class="n">time</span><span class="p">);</span>
</code></pre></div></div>
<p>With a emissive material and a simple bloom from the postprocessing stack it can look like this:</p>
<p><img src="/assets/images/posts/050/result.gif" alt="" /></p>
<h2 id="source">Source</h2>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/050_Compute_Shader/BasicCompute.compute">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/050_Compute_Shader/BasicCompute.compute</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Each #kernel tells which function to compile; you can have many kernels</span>
<span class="cp">#pragma kernel Spheres
</span>
<span class="cp">#include "Random.cginc"
</span>
<span class="c1">//variables</span>
<span class="n">RWStructuredBuffer</span><span class="o"><</span><span class="n">float3</span><span class="o">></span> <span class="n">Result</span><span class="p">;</span>
<span class="k">uniform</span> <span class="kt">float</span> <span class="n">Time</span><span class="p">;</span>
<span class="p">[</span><span class="n">numthreads</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)]</span>
<span class="kt">void</span> <span class="nf">Spheres</span> <span class="p">(</span><span class="n">uint3</span> <span class="n">id</span> <span class="o">:</span> <span class="n">SV_DispatchThreadID</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">//generate 2 orthogonal vectors</span>
<span class="n">float3</span> <span class="n">baseDir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">rand1dTo3d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">rand1dTo1d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">)</span><span class="o">*</span><span class="mi">0</span><span class="p">.</span><span class="mi">9</span><span class="o">+</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="p">);</span>
<span class="n">float3</span> <span class="n">orthogonal</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">cross</span><span class="p">(</span><span class="n">baseDir</span><span class="p">,</span> <span class="n">rand1dTo3d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="mi">7</span><span class="p">.</span><span class="mi">1393</span><span class="p">)</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">))</span> <span class="o">*</span> <span class="p">(</span><span class="n">rand1dTo1d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="o">+</span><span class="mi">3</span><span class="p">.</span><span class="mi">7443</span><span class="p">)</span><span class="o">*</span><span class="mi">0</span><span class="p">.</span><span class="mi">9</span><span class="o">+</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="p">);</span>
<span class="c1">//scale the time and give it a random offset</span>
<span class="kt">float</span> <span class="n">scaledTime</span> <span class="o">=</span> <span class="n">Time</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="n">rand1dTo1d</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">*</span> <span class="mi">712</span><span class="p">.</span><span class="mi">131234</span><span class="p">;</span>
<span class="c1">//calculate a vector based on vectors</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">baseDir</span> <span class="o">*</span> <span class="n">sin</span><span class="p">(</span><span class="n">scaledTime</span><span class="p">)</span> <span class="o">+</span> <span class="n">orthogonal</span> <span class="o">*</span> <span class="n">cos</span><span class="p">(</span><span class="n">scaledTime</span><span class="p">);</span>
<span class="n">Result</span><span class="p">[</span><span class="n">id</span><span class="p">.</span><span class="n">x</span><span class="p">]</span> <span class="o">=</span> <span class="n">dir</span> <span class="o">*</span> <span class="mi">20</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/050_Compute_Shader/BasicComputeSpheres.cs">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/050_Compute_Shader/BasicComputeSpheres.cs</a></li>
</ul>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Collections</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Collections.Generic</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">BasicComputeSpheres</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
<span class="k">public</span> <span class="kt">int</span> <span class="n">SphereAmount</span> <span class="p">=</span> <span class="m">17</span><span class="p">;</span>
<span class="k">public</span> <span class="n">ComputeShader</span> <span class="n">Shader</span><span class="p">;</span>
<span class="k">public</span> <span class="n">GameObject</span> <span class="n">Prefab</span><span class="p">;</span>
<span class="n">ComputeBuffer</span> <span class="n">resultBuffer</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">kernel</span><span class="p">;</span>
<span class="kt">uint</span> <span class="n">threadGroupSize</span><span class="p">;</span>
<span class="n">Vector3</span><span class="p">[]</span> <span class="n">output</span><span class="p">;</span>
<span class="n">Transform</span><span class="p">[]</span> <span class="n">instances</span><span class="p">;</span>
<span class="k">void</span> <span class="nf">Start</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">//program we're executing</span>
<span class="n">kernel</span> <span class="p">=</span> <span class="n">Shader</span><span class="p">.</span><span class="nf">FindKernel</span><span class="p">(</span><span class="s">"Spheres"</span><span class="p">);</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">GetKernelThreadGroupSizes</span><span class="p">(</span><span class="n">kernel</span><span class="p">,</span> <span class="k">out</span> <span class="n">threadGroupSize</span><span class="p">,</span> <span class="k">out</span> <span class="n">_</span><span class="p">,</span> <span class="k">out</span> <span class="n">_</span><span class="p">);</span>
<span class="c1">//buffer on the gpu in the ram</span>
<span class="n">resultBuffer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ComputeBuffer</span><span class="p">(</span><span class="n">SphereAmount</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">float</span><span class="p">)</span> <span class="p">*</span> <span class="m">3</span><span class="p">);</span>
<span class="n">output</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Vector3</span><span class="p">[</span><span class="n">SphereAmount</span><span class="p">];</span>
<span class="c1">//spheres we use for visualisation</span>
<span class="n">instances</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Transform</span><span class="p">[</span><span class="n">SphereAmount</span><span class="p">];</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p"><</span> <span class="n">SphereAmount</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
<span class="p">{</span>
<span class="n">instances</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">=</span> <span class="nf">Instantiate</span><span class="p">(</span><span class="n">Prefab</span><span class="p">,</span> <span class="n">transform</span><span class="p">).</span><span class="n">transform</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">void</span> <span class="nf">Update</span><span class="p">()</span>
<span class="p">{</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">SetFloat</span><span class="p">(</span><span class="s">"Time"</span><span class="p">,</span> <span class="n">Time</span><span class="p">.</span><span class="n">time</span><span class="p">);</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">SetBuffer</span><span class="p">(</span><span class="n">kernel</span><span class="p">,</span> <span class="s">"Result"</span><span class="p">,</span> <span class="n">resultBuffer</span><span class="p">);</span>
<span class="kt">int</span> <span class="n">threadGroups</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span> <span class="p">((</span><span class="n">SphereAmount</span> <span class="p">+</span> <span class="p">(</span><span class="n">threadGroupSize</span> <span class="p">-</span> <span class="m">1</span><span class="p">))</span> <span class="p">/</span> <span class="n">threadGroupSize</span><span class="p">);</span>
<span class="n">Shader</span><span class="p">.</span><span class="nf">Dispatch</span><span class="p">(</span><span class="n">kernel</span><span class="p">,</span> <span class="n">threadGroups</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span>
<span class="n">resultBuffer</span><span class="p">.</span><span class="nf">GetData</span><span class="p">(</span><span class="n">output</span><span class="p">);</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p"><</span> <span class="n">instances</span><span class="p">.</span><span class="n">Length</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
<span class="n">instances</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">localPosition</span> <span class="p">=</span> <span class="n">output</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
<span class="p">}</span>
<span class="k">void</span> <span class="nf">OnDestroy</span><span class="p">()</span>
<span class="p">{</span>
<span class="n">resultBuffer</span><span class="p">.</span><span class="nf">Dispose</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>Ronja BöhringerSo far we always used shaders to render with a fixed pipeline into textures, but modern graphics card can do way more than just that (sometimes they’re also referred to as GPGPU for “general purpose graphics processing unit” because of that). To do things that arent in the fix pipeline we’re using so far we have to use compute shaders.Sprite Outlines2020-07-23T00:00:00+02:002020-07-23T00:00:00+02:00https://www.ronja-tutorials.com/2020/07/23/sprite-outlines<p>I already talked about 2 ways of generating outlines in your programs, <a href="/2018/07/15/postprocessing-outlines.html">by analyzing the depth and normals of your scene</a> or <a href="/2018/07/21/hull-outline.html">by rendering the model twice with a hull</a>. Both of those assume we’re using opaque meshes that write into the depth buffer, if we’re using 2d sprites neither approach works.
The approach for this tutorial uses the alpha channel of a texture to generate 2d outlines.</p>
<h2 id="basic-implementation">Basic Implementation</h2>
<p>The idea is that we sample the texture at multiple spots around the uv point and remember the biggest value of the alpha channel we find. When a Pixel is not visible for the original texture sample, but we can find a higher alpha value when looking at the neighboring pixels, then we color in the outline.</p>
<p>The base for our code is from <a href="/2018/04/13/sprite-shaders.html">my sprite shader tutorial</a>. In the fragment function we start by making an array of directions we want to sample in. You could sacrifice some speed for more flexibility and get the directions via <code class="language-plaintext highlighter-rouge">sin</code> and <code class="language-plaintext highlighter-rouge">cos</code>, but thats your choice. I chose to sample in 8 directions, the for cardinal directions as well as diagonals. Important here is that the diagonal directions should also have a length of 1, if we just use <code class="language-plaintext highlighter-rouge">(1, 1)</code> they’d have a length of <code class="language-plaintext highlighter-rouge">sqrt(2)</code> (you can easily get that via the pythogoras (<code class="language-plaintext highlighter-rouge">sqrt(1² + 1²)</code>)), instead we divide each component by <code class="language-plaintext highlighter-rouge">sqrt(2)</code>, so use <code class="language-plaintext highlighter-rouge">1 / sqrt(2)</code> and all is fine.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define DIV_SQRT_2 0.70710678118
</span><span class="n">float2</span> <span class="n">directions</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="n">float2</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span>
<span class="n">float2</span><span class="p">(</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="n">DIV_SQRT_2</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="n">DIV_SQRT_2</span><span class="p">),</span>
<span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">)};</span>
</code></pre></div></div>
<p>Before the loop we declare the “maximum alpha” variable and initialize it to zero. The loop is a simple for loop over all 8 indices of the array (you could also make it count to 4 for a cheaper outline without diagonals). Inside the loop we first calculate the sample point and then put the maximum of the maximum alpha so far and the alpha at that point in the maximum alpha variable.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//generate border</span>
<span class="kt">float</span> <span class="n">maxAlpha</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">for</span><span class="p">(</span><span class="kt">uint</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">index</span><span class="o"><</span><span class="mi">8</span><span class="p">;</span> <span class="n">index</span><span class="o">++</span><span class="p">){</span>
<span class="n">float2</span> <span class="n">sampleUV</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">directions</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">*</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="cm">/*magic number*/</span><span class="p">;</span>
<span class="n">maxAlpha</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="n">maxAlpha</span><span class="p">,</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">sampleUV</span><span class="p">).</span><span class="n">a</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>After figuring out the maximum alpha of those points we can apply the border by first making everything that isn’t visible in the original sprite have the color of our outline. Then we set the color to the maximum value between the alpha so far and the maximum alpha of our border samples.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//apply border</span>
<span class="n">col</span><span class="p">.</span><span class="n">rgb</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">float3</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span><span class="cm">/*magic color*/</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">rgb</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">a</span><span class="p">);</span>
<span class="n">col</span><span class="p">.</span><span class="n">a</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="n">col</span><span class="p">.</span><span class="n">a</span><span class="p">,</span> <span class="n">maxAlpha</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
</code></pre></div></div>
<p>This should net you something like this. Not pretty, not flexible, but a outline!</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fixed4</span> <span class="nf">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//get regular color</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">);</span>
<span class="n">col</span> <span class="o">*=</span> <span class="n">_Color</span><span class="p">;</span>
<span class="n">col</span> <span class="o">*=</span> <span class="n">i</span><span class="p">.</span><span class="n">color</span><span class="p">;</span>
<span class="c1">//sample directions</span>
<span class="cp">#define DIV_SQRT_2 0.70710678118
</span> <span class="n">float2</span> <span class="n">directions</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="n">float2</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span>
<span class="n">float2</span><span class="p">(</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="n">DIV_SQRT_2</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="n">DIV_SQRT_2</span><span class="p">),</span>
<span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">)};</span>
<span class="c1">//generate border</span>
<span class="kt">float</span> <span class="n">maxAlpha</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">for</span><span class="p">(</span><span class="kt">uint</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">index</span><span class="o"><</span><span class="mi">8</span><span class="p">;</span> <span class="n">index</span><span class="o">++</span><span class="p">){</span>
<span class="n">float2</span> <span class="n">sampleUV</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">directions</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">*</span> <span class="mi">0</span><span class="p">.</span><span class="mo">01</span><span class="p">;</span>
<span class="n">maxAlpha</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="n">maxAlpha</span><span class="p">,</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">sampleUV</span><span class="p">).</span><span class="n">a</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">//apply border</span>
<span class="n">col</span><span class="p">.</span><span class="n">rgb</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">float3</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">col</span><span class="p">.</span><span class="n">rgb</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">a</span><span class="p">);</span>
<span class="n">col</span><span class="p">.</span><span class="n">a</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="n">col</span><span class="p">.</span><span class="n">a</span><span class="p">,</span> <span class="n">maxAlpha</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="err">}</span>
</code></pre></div></div>
<p><img src="\assets\images\posts\049\FirstShot.png" alt="" /></p>
<h2 id="cleanup">Cleanup</h2>
<p>Lets add two properties so we can change the way our outline looks without having to recompile the shader. One of them is for the width of the outline and one for the color.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//in properties block</span>
<span class="n">_OutlineColor</span> <span class="p">(</span><span class="s">"Outline Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">_OutlineWidth</span> <span class="p">(</span><span class="s">"Outline Width"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">))</span> <span class="o">=</span> <span class="mi">1</span>
</code></pre></div></div>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//in CGPROGRAM</span>
<span class="n">fixed4</span> <span class="n">_OutlineColor</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">_OutlineWidth</span><span class="p">;</span>
</code></pre></div></div>
<p>Then in our function we use the color property instead of the hardcoded red value.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//apply border</span>
<span class="n">col</span><span class="p">.</span><span class="n">rgb</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">_OutlineColor</span><span class="p">.</span><span class="n">rgb</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">rgb</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">a</span><span class="p">);</span>
</code></pre></div></div>
<p>Next lets fix up the outline size, currently its declared via a magic number in uv space. A easy fix is to declare how many texture pixels we want the outline to be. We can get the size of one texture pixel (or texel) by creating a new variable called <TextureName>_TexelSize, so in our case _MainTex_TexelSize. Then we can multiply our property with the x and y components of that variable (<code class="language-plaintext highlighter-rouge">x</code> and <code class="language-plaintext highlighter-rouge">y</code> are texel size in uv distance, <code class="language-plaintext highlighter-rouge">z</code> and <code class="language-plaintext highlighter-rouge">w</code> are texture size in pixels) and use the result as a scale for the outline width instead.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">float2</span> <span class="n">sampleDistance</span> <span class="o">=</span> <span class="n">_MainTex_TexelSize</span><span class="p">.</span><span class="n">xy</span> <span class="o">*</span> <span class="n">_OutlineWidth</span><span class="p">;</span>
<span class="c1">//generate border</span>
<span class="kt">float</span> <span class="n">maxAlpha</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">for</span><span class="p">(</span><span class="kt">uint</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">index</span><span class="o"><</span><span class="mi">8</span><span class="p">;</span> <span class="n">index</span><span class="o">++</span><span class="p">){</span>
<span class="n">float2</span> <span class="n">sampleUV</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">directions</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">*</span> <span class="n">sampleDistance</span><span class="p">;</span>
<span class="n">maxAlpha</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="n">maxAlpha</span><span class="p">,</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">sampleUV</span><span class="p">).</span><span class="n">a</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="\assets\images\posts\049\Properties.gif" alt="" /></p>
<h2 id="world-distance-outline">World Distance Outline</h2>
<p>You might not always want to have your outline scale in texture pixels though. You might want a outline width in screen pixels, in screen percent, in world distance. I’m not gonna go through all of those possibilities there, but I am going to show the most complex of those, world space width.</p>
<p>We just need the uv distance per world distance and then we can multiply that with our outline width like we’re doing so far with the texel size, so lets write a function for that. Calculating that is possible via <a href="/2019/11/29/fwidth.html">screenspace partial derivatives, better known as ddx, ddy and fwidth</a>.</p>
<p>The derivatives allow us to get the change in uv per screen pixel as well as the change in worldspace position per screen pixel. We have to get the absolute value of the uv change to not accidentally get negative values as well as get the length of the change in world position to get correct distances in case our camera is rotated.</p>
<p>With those values we can get the uv per unit in both x and y axis by dividing the uv per pixel by the units per pixel. After getting that for x and y we simply add the two values and return it.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">float2</span> <span class="nf">uvPerWorldUnit</span><span class="p">(</span><span class="n">float2</span> <span class="n">uv</span><span class="p">,</span> <span class="n">float2</span> <span class="n">space</span><span class="p">){</span>
<span class="n">float2</span> <span class="n">uvPerPixelX</span> <span class="o">=</span> <span class="n">abs</span><span class="p">(</span><span class="n">ddx</span><span class="p">(</span><span class="n">uv</span><span class="p">));</span>
<span class="n">float2</span> <span class="n">uvPerPixelY</span> <span class="o">=</span> <span class="n">abs</span><span class="p">(</span><span class="n">ddy</span><span class="p">(</span><span class="n">uv</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">unitsPerPixelX</span> <span class="o">=</span> <span class="n">length</span><span class="p">(</span><span class="n">ddx</span><span class="p">(</span><span class="n">space</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">unitsPerPixelY</span> <span class="o">=</span> <span class="n">length</span><span class="p">(</span><span class="n">ddy</span><span class="p">(</span><span class="n">space</span><span class="p">));</span>
<span class="n">float2</span> <span class="n">uvPerUnitX</span> <span class="o">=</span> <span class="n">uvPerPixelX</span> <span class="o">/</span> <span class="n">unitsPerPixelX</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uvPerUnitY</span> <span class="o">=</span> <span class="n">uvPerPixelY</span> <span class="o">/</span> <span class="n">unitsPerPixelY</span><span class="p">;</span>
<span class="k">return</span> <span class="p">(</span><span class="n">uvPerUnitX</span> <span class="o">+</span> <span class="n">uvPerUnitY</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>You might have notives that I was talking about using the world position even though we don’t have access to that yet, so lets quickly add that, I havent made a tutorial specifically about that, but <a href="/2018/04/23/planar-mapping.html">the planar mapping one</a> uses the world pos and not much else.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">worldPos</span> <span class="o">:</span> <span class="n">TEXCOORD1</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">color</span> <span class="o">:</span> <span class="n">COLOR</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">v2f</span> <span class="nf">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">worldPos</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_ObjectToWorld</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">uv</span> <span class="o">=</span> <span class="n">TRANSFORM_TEX</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">uv</span><span class="p">,</span> <span class="n">_MainTex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">color</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">color</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//in fragment function</span>
<span class="n">float2</span> <span class="n">sampleDistance</span> <span class="o">=</span> <span class="n">uvPerWorldUnit</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">worldPos</span><span class="p">.</span><span class="n">xy</span><span class="p">)</span> <span class="o">*</span> <span class="n">_OutlineWidth</span><span class="p">;</span>
</code></pre></div></div>
<p>with this you can freely scale, rotate, whatever your sprites and you’ll always have a consistent outline.</p>
<p><img src="/assets/images/posts/049/WorldOutlines.gif" alt="" /></p>
<h2 id="limitations">Limitations</h2>
<p>One huge limitation of this method is that we can only draw the outline where theres already a mesh, setting the mesh type in our sprites to “Full Rect” as well as adding padding to the sprites helps by just rendering more by default, but it also adds overdraw to your scene and it also can’t always avoid artefacts, I tried thinking about how to do that but couldn’t come up with a quick tutorial-able solution.</p>
<p>In addition to that this method is really bad at generating outlines of small or pointy features, often generating spikes in the outline. If you want to have 2d outlines but a more perfect approach, heres a article about how you might try to do that: <a href="https://medium.com/@bgolus/the-quest-for-very-wide-outlines-ba82ed442cd9">https://medium.com/@bgolus/the-quest-for-very-wide-outlines-ba82ed442cd9</a>.</p>
<h2 id="source">Source</h2>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/049_Sprite_Outline/SpriteOutline.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/049_Sprite_Outline/SpriteOutline.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/049_SpriteOutline"</span><span class="p">{</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_Color</span> <span class="p">(</span><span class="s">"Tint"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">_OutlineColor</span> <span class="p">(</span><span class="s">"OutlineColor"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">_OutlineWidth</span> <span class="p">(</span><span class="s">"OutlineWidth"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">_MainTex</span> <span class="p">(</span><span class="s">"Texture"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="n">Tags</span><span class="p">{</span>
<span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Transparent"</span>
<span class="s">"Queue"</span><span class="o">=</span><span class="s">"Transparent"</span>
<span class="p">}</span>
<span class="n">Blend</span> <span class="n">SrcAlpha</span> <span class="n">OneMinusSrcAlpha</span>
<span class="n">ZWrite</span> <span class="n">off</span>
<span class="n">Cull</span> <span class="n">off</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="cp">#include "UnityCG.cginc"
</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="kt">sampler2D</span> <span class="n">_MainTex</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">_MainTex_ST</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">_MainTex_TexelSize</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_Color</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_OutlineColor</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">_OutlineWidth</span><span class="p">;</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">color</span> <span class="o">:</span> <span class="n">COLOR</span><span class="p">;</span>
<span class="p">};</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">worldPos</span> <span class="o">:</span> <span class="n">TEXCOORD1</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">color</span> <span class="o">:</span> <span class="n">COLOR</span><span class="p">;</span>
<span class="p">};</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">worldPos</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_ObjectToWorld</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">uv</span> <span class="o">=</span> <span class="n">TRANSFORM_TEX</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">uv</span><span class="p">,</span> <span class="n">_MainTex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">color</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">color</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">float2</span> <span class="n">uvPerWorldUnit</span><span class="p">(</span><span class="n">float2</span> <span class="n">uv</span><span class="p">,</span> <span class="n">float2</span> <span class="n">space</span><span class="p">){</span>
<span class="n">float2</span> <span class="n">uvPerPixelX</span> <span class="o">=</span> <span class="n">abs</span><span class="p">(</span><span class="n">ddx</span><span class="p">(</span><span class="n">uv</span><span class="p">));</span>
<span class="n">float2</span> <span class="n">uvPerPixelY</span> <span class="o">=</span> <span class="n">abs</span><span class="p">(</span><span class="n">ddy</span><span class="p">(</span><span class="n">uv</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">unitsPerPixelX</span> <span class="o">=</span> <span class="n">length</span><span class="p">(</span><span class="n">ddx</span><span class="p">(</span><span class="n">space</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">unitsPerPixelY</span> <span class="o">=</span> <span class="n">length</span><span class="p">(</span><span class="n">ddy</span><span class="p">(</span><span class="n">space</span><span class="p">));</span>
<span class="n">float2</span> <span class="n">uvPerUnitX</span> <span class="o">=</span> <span class="n">uvPerPixelX</span> <span class="o">/</span> <span class="n">unitsPerPixelX</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uvPerUnitY</span> <span class="o">=</span> <span class="n">uvPerPixelY</span> <span class="o">/</span> <span class="n">unitsPerPixelY</span><span class="p">;</span>
<span class="k">return</span> <span class="p">(</span><span class="n">uvPerUnitX</span> <span class="o">+</span> <span class="n">uvPerUnitY</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//get regular color</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">);</span>
<span class="n">col</span> <span class="o">*=</span> <span class="n">_Color</span><span class="p">;</span>
<span class="n">col</span> <span class="o">*=</span> <span class="n">i</span><span class="p">.</span><span class="n">color</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">sampleDistance</span> <span class="o">=</span> <span class="n">uvPerWorldUnit</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">worldPos</span><span class="p">.</span><span class="n">xy</span><span class="p">)</span> <span class="o">*</span> <span class="n">_OutlineWidth</span><span class="p">;</span>
<span class="c1">//sample directions</span>
<span class="cp">#define DIV_SQRT_2 0.70710678118
</span> <span class="n">float2</span> <span class="n">directions</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="n">float2</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span>
<span class="n">float2</span><span class="p">(</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="n">DIV_SQRT_2</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="n">DIV_SQRT_2</span><span class="p">),</span>
<span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">),</span> <span class="n">float2</span><span class="p">(</span><span class="n">DIV_SQRT_2</span><span class="p">,</span> <span class="o">-</span><span class="n">DIV_SQRT_2</span><span class="p">)};</span>
<span class="c1">//generate border</span>
<span class="kt">float</span> <span class="n">maxAlpha</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">for</span><span class="p">(</span><span class="kt">uint</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">index</span><span class="o"><</span><span class="mi">8</span><span class="p">;</span> <span class="n">index</span><span class="o">++</span><span class="p">){</span>
<span class="n">float2</span> <span class="n">sampleUV</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">directions</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">*</span> <span class="n">sampleDistance</span><span class="p">;</span>
<span class="n">maxAlpha</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="n">maxAlpha</span><span class="p">,</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">sampleUV</span><span class="p">).</span><span class="n">a</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">//apply border</span>
<span class="n">col</span><span class="p">.</span><span class="n">rgb</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">_OutlineColor</span><span class="p">.</span><span class="n">rgb</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">rgb</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">a</span><span class="p">);</span>
<span class="n">col</span><span class="p">.</span><span class="n">a</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="n">col</span><span class="p">.</span><span class="n">a</span><span class="p">,</span> <span class="n">maxAlpha</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>Ronja BöhringerI already talked about 2 ways of generating outlines in your programs, by analyzing the depth and normals of your scene or by rendering the model twice with a hull. Both of those assume we’re using opaque meshes that write into the depth buffer, if we’re using 2d sprites neither approach works. The approach for this tutorial uses the alpha channel of a texture to generate 2d outlines.Instancing and Material Property Blocks2020-02-11T00:00:00+01:002020-02-11T00:00:00+01:00https://www.ronja-tutorials.com/2020/02/11/material-property-blocks<h2 id="current-state">Current State</h2>
<p>I’m going to go off a <a href="/basics.html">basic unlit shader</a> in this tutorial. In all tutorials since that one we always set the properties at a “per material” basis. This allows us to do everything we ever need to do in theory, but depending on the circumstances it might also force us to use tons of different materials. This not only makes a scene harder to author, but can also significantly slow down your game as by default objects with different materials cannot be instanced together and switching drawcalls is one of the main performance sinks of rendering. Material property blocks (MPBs) allow us a way around that which we can use to change properties on a per object basis without</p>
<p>Important to mention here is that if you’re using one of Unity’s new scriptable render pipelines the performance slowdown might be way less and you can even make the performance worse by using property blocks. That’s because of the new SRP batcher which is able to batch models with different materials, but doesn’t support property block as far as I know (it’s better to read yourself into the current state of tech yourself and trying out what works better).</p>
<p><img src="/assets/images/posts/048/SimpleMaterials.png" alt="" /></p>
<h2 id="changing-properties-via-script">Changing Properties via Script</h2>
<p>If we want to decouple some properties from the materials, we sadly can’t set them from the material inspector as that would modify all objects with that material. Instead we create a new C# script which will allow us to set our materials from there. In this instance we just add a public color field and set the <code class="language-plaintext highlighter-rouge">_Color</code> property of the shader every time any field is changed in the inspector, which is when the <code class="language-plaintext highlighter-rouge">OnValidate</code> method is called automatically.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ColorPropertySetter</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
<span class="k">public</span> <span class="n">Color</span> <span class="n">MaterialColor</span><span class="p">;</span>
<span class="c1">// OnValidate is called in the editor after the component is edited</span>
<span class="k">void</span> <span class="nf">OnValidate</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">//Get a renderer component either of the own gameobject or of a child</span>
<span class="n">Renderer</span> <span class="n">renderer</span> <span class="p">=</span> <span class="n">GetComponentInChildren</span><span class="p"><</span><span class="n">Renderer</span><span class="p">>();</span>
<span class="c1">//get the material of the renderer</span>
<span class="n">Material</span> <span class="n">mat</span> <span class="p">=</span> <span class="n">renderer</span><span class="p">.</span><span class="n">material</span><span class="p">;</span>
<span class="c1">//set the color property</span>
<span class="n">mat</span><span class="p">.</span><span class="nf">SetColor</span><span class="p">(</span><span class="s">"_Color"</span><span class="p">,</span> <span class="n">MaterialColor</span><span class="p">);</span>
<span class="c1">//reassign the material to the renderer</span>
<span class="n">renderer</span><span class="p">.</span><span class="n">material</span> <span class="p">=</span> <span class="n">mat</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/048/PropertySetter.png" alt="" /></p>
<p>With this setup we can change the properties via a script on a per object basis. What unity hides from us and what makes this very bad is that by using the <code class="language-plaintext highlighter-rouge">.material</code> field of the renderer like this, we create a new Material every time we want to modify it. Unity even gives us a little warning to nevr do this in editor code. To get the material without cloning the existing one, we have to use the <code class="language-plaintext highlighter-rouge">.sharedMaterial</code> property instead. When we use this property, we also don’t have to reassign the material in the last line of the function because we’re getting a real reference instead of just a copy.</p>
<p>If you used the previous version of the script, be sure to apply the same material to all copies again since the renderers are using the clone materials we don’t want now.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// OnValidate is called in the editor after the component is edited</span>
<span class="k">void</span> <span class="nf">OnValidate</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">//Get a renderer component either of the own gameobject or of a child</span>
<span class="n">Renderer</span> <span class="n">renderer</span> <span class="p">=</span> <span class="n">GetComponentInChildren</span><span class="p"><</span><span class="n">Renderer</span><span class="p">>();</span>
<span class="c1">//get the material of the renderer</span>
<span class="n">Material</span> <span class="n">mat</span> <span class="p">=</span> <span class="n">renderer</span><span class="p">.</span><span class="n">sharedMaterial</span><span class="p">;</span>
<span class="c1">//set the color property</span>
<span class="n">mat</span><span class="p">.</span><span class="nf">SetColor</span><span class="p">(</span><span class="s">"_Color"</span><span class="p">,</span> <span class="n">MaterialColor</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/048/ChangeSharedMaterial.gif" alt="" /></p>
<p>Now editing the material via the script is the same as editing it via the material inspector, with the same disadvantages. You can use this knowledge to change materials whenever you want, you can clone a material by passing a new material a old one to clone like <code class="language-plaintext highlighter-rouge">Material clone = new Material(oldMaterial);</code>. You can do this in runtime, at awake and if you want a complex system without needing the performance benefits (for example when using the SRP batcher) this should be a good place to start. But to change materials on a per object basis without cloning them, we still need material property blocks.</p>
<h2 id="setting-material-property-blocks">Setting Material Property Blocks</h2>
<p>To pass MPBs to shaders, we first have to create a representation of them in C#. It’s generally recommended to create them once and reuse it every frame. I check whether a property block already exists at the start of the function and create a new one if it doesn’t, if you’re only changing the propertyblock in the game and not in the editor creating it during the <code class="language-plaintext highlighter-rouge">Awake</code> function is probably more reasonable.
With the MPB created we can use the same functions as we did on the material on the propertyblock, after setting the properties we apply it to the renderer via the <code class="language-plaintext highlighter-rouge">SetPropertyBlock</code> function.</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ColorPropertySetter</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
<span class="c1">//The color of the object</span>
<span class="k">public</span> <span class="n">Color</span> <span class="n">MaterialColor</span><span class="p">;</span>
<span class="c1">//The material property block we pass to the GPU</span>
<span class="k">private</span> <span class="n">MaterialPropertyBlock</span> <span class="n">propertyBlock</span><span class="p">;</span>
<span class="c1">// OnValidate is called in the editor after the component is edited</span>
<span class="k">void</span> <span class="nf">OnValidate</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">//create propertyblock only if none exists</span>
<span class="k">if</span> <span class="p">(</span><span class="n">propertyBlock</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
<span class="n">propertyBlock</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MaterialPropertyBlock</span><span class="p">();</span>
<span class="c1">//Get a renderer component either of the own gameobject or of a child</span>
<span class="n">Renderer</span> <span class="n">renderer</span> <span class="p">=</span> <span class="n">GetComponentInChildren</span><span class="p"><</span><span class="n">Renderer</span><span class="p">>();</span>
<span class="c1">//set the color property</span>
<span class="n">propertyBlock</span><span class="p">.</span><span class="nf">SetColor</span><span class="p">(</span><span class="s">"_Color"</span><span class="p">,</span> <span class="n">MaterialColor</span><span class="p">);</span>
<span class="c1">//apply propertyBlock to renderer</span>
<span class="n">renderer</span><span class="p">.</span><span class="nf">SetPropertyBlock</span><span class="p">(</span><span class="n">propertyBlock</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/048/CorrectPropertyBlocks.png" alt="" /></p>
<p>With this we are setting the MPBs correctly and we can see 3 models with the same material, but different colors. Sadly our material doesn’t support instancing yet, so we don’t get the performance benefits of using material property blocks and still have to pay with one drawcall per unique MPB.</p>
<h2 id="making-your-shader-support-instancing">Making your Shader support Instancing</h2>
<p>First we have to tell Unity that the shader is able to be instanced, to do this we add the line <code class="language-plaintext highlighter-rouge">#pragma multi_compile_instancing</code> next to the <code class="language-plaintext highlighter-rouge">#pragma</code> declarations for the shader functions, this makes the material inspector show the “Enable GPU Instancing” option which we want to enable. In the case of surface shaders this shouldn’t be needed.</p>
<p><img src="/assets/images/posts/048/InstancingOption.png" alt="" /></p>
<p>If you don’t use MaterialPropertyBlocks this is all you need to do to enable instancing, and if you’re using hundreds of thousands of instances of the same model, it can save you a good bit of performance. If we look into the frame debugger we can see that using MaterialPropertyBlocks currently breaks our instancing though because the properties aren’t setup for instancing yet. (You can find the frame debugger under <code class="language-plaintext highlighter-rouge">Window > Analysis > Frame Debugger</code>)</p>
<p><img src="/assets/images/posts/048/CantInstanceDebugger.png" alt="" /></p>
<p>First we have to set up the instance id. In Unity theres macros for that, so we’ll use those. We add <code class="language-plaintext highlighter-rouge">UNITY_VERTEX_INPUT_INSTANCE_ID</code> to both the appdata struct as well as the v2f struct. Then in the vertex function we use <code class="language-plaintext highlighter-rouge">UNITY_SETUP_INSTANCE_ID(input_stuct)</code> to do the setup in the appdata input stuct and then pass the ID to the vertex to fragment struct for use in the fragment shader via the <code class="language-plaintext highlighter-rouge">UNITY_TRANSFER_INSTANCE_ID(input_stuct, output_stuct)</code> macro. In the fragment shader we add another <code class="language-plaintext highlighter-rouge">UNITY_SETUP_INSTANCE_ID</code> to also do the setup there.</p>
<p>With this work done, we can actually look at the properties to convert. For this we have to add a code block in our hlsl area, but outside of any functions thats framed by the two macros <code class="language-plaintext highlighter-rouge">UNITY_INSTANCING_BUFFER_START(name)</code> and <code class="language-plaintext highlighter-rouge">UNITY_INSTANCING_BUFFER_END(name)</code>. Inside this block we can then define variables via the <code class="language-plaintext highlighter-rouge">UNITY_DEFINE_INSTANCED_PROP(datatype, variable_name)</code> macro. After that is also done, we can finally access the properties of the MPB via the <code class="language-plaintext highlighter-rouge">UNITY_ACCESS_INSTANCED_PROP(buffer_name, variable_name)</code> macro.</p>
<p>I also added the <code class="language-plaintext highlighter-rouge">[PerRendererData]</code> attribute to the <code class="language-plaintext highlighter-rouge">_Color</code> property in the property definitions at the top, though this should only make it so the property isn’t shown in the regular material inspector where it doesn’t have any effect anymore when a MPB is used.</p>
<p>With all of this done, the shader looks like this:</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/048_Instancing"</span> <span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="p">[</span><span class="n">PerRendererData</span><span class="p">]</span> <span class="n">_Color</span> <span class="p">(</span><span class="s">"Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span><span class="p">}</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//allow instancing</span>
<span class="cp">#pragma multi_compile_instancing
</span>
<span class="c1">//shader functions</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//use unity shader library</span>
<span class="cp">#include "UnityCG.cginc"
</span>
<span class="c1">//per vertex data that comes from the model/parameters</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="n">UNITY_VERTEX_INPUT_INSTANCE_ID</span>
<span class="p">};</span>
<span class="c1">//per vertex data that gets passed from the vertex to the fragment function</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">UNITY_VERTEX_INPUT_INSTANCE_ID</span>
<span class="p">};</span>
<span class="n">UNITY_INSTANCING_BUFFER_START</span><span class="p">(</span><span class="n">Props</span><span class="p">)</span>
<span class="n">UNITY_DEFINE_INSTANCED_PROP</span><span class="p">(</span><span class="n">float4</span><span class="p">,</span> <span class="n">_Color</span><span class="p">)</span>
<span class="n">UNITY_INSTANCING_BUFFER_END</span><span class="p">(</span><span class="n">Props</span><span class="p">)</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//setup instance id</span>
<span class="n">UNITY_SETUP_INSTANCE_ID</span><span class="p">(</span><span class="n">v</span><span class="p">);</span>
<span class="n">UNITY_TRANSFER_INSTANCE_ID</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">o</span><span class="p">);</span>
<span class="c1">//calculate the position in clip space to render the object</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//setup instance id</span>
<span class="n">UNITY_SETUP_INSTANCE_ID</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
<span class="c1">//get _Color Property from buffer</span>
<span class="n">fixed4</span> <span class="n">color</span> <span class="o">=</span> <span class="n">UNITY_ACCESS_INSTANCED_PROP</span><span class="p">(</span><span class="n">Props</span><span class="p">,</span> <span class="n">_Color</span><span class="p">);</span>
<span class="c1">//Return the color the Object is rendered in</span>
<span class="k">return</span> <span class="n">color</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And when checking the frame debugger we can actually see that all of our objects are drawn in a single drawcall.</p>
<p><img src="/assets/images/posts/048/CanInstanceDebugger.png" alt="" /></p>
<h2 id="profiling">Profiling</h2>
<p>If you’re asking yourself what the use of all of this was apart from some abstract concept of “drawcalls” I made a little test. Lots of small meshes with small different changes like color are a best case szenario for instancing, but it can show you what instancing is capable of in theory.</p>
<p><img src="/assets/images/posts/048/BallPit.png" alt="" /></p>
<p>For profiling I used a new script which assigns a random color instead of a authored one:</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">RandomColorPropertySetter</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
<span class="c1">//The material property block we pass to the GPU</span>
<span class="n">MaterialPropertyBlock</span> <span class="n">propertyBlock</span><span class="p">;</span>
<span class="c1">// OnValidate is called in the editor after the component is edited</span>
<span class="k">void</span> <span class="nf">OnValidate</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">//create propertyblock only if none exists</span>
<span class="k">if</span> <span class="p">(</span><span class="n">propertyBlock</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
<span class="n">propertyBlock</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MaterialPropertyBlock</span><span class="p">();</span>
<span class="c1">//Get a renderer component either of the own gameobject or of a child</span>
<span class="n">Renderer</span> <span class="n">renderer</span> <span class="p">=</span> <span class="n">GetComponentInChildren</span><span class="p"><</span><span class="n">Renderer</span><span class="p">>();</span>
<span class="c1">//set the color property</span>
<span class="n">propertyBlock</span><span class="p">.</span><span class="nf">SetColor</span><span class="p">(</span><span class="s">"_Color"</span><span class="p">,</span> <span class="nf">GetRandomColor</span><span class="p">());</span>
<span class="c1">//apply propertyBlock to renderer</span>
<span class="n">renderer</span><span class="p">.</span><span class="nf">SetPropertyBlock</span><span class="p">(</span><span class="n">propertyBlock</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">static</span> <span class="n">Color</span> <span class="nf">GetRandomColor</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">return</span> <span class="n">Color</span><span class="p">.</span><span class="nf">HSVToRGB</span><span class="p">(</span><span class="n">Random</span><span class="p">.</span><span class="k">value</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="p">.</span><span class="m">9f</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>With 4800 Spheres with our newly written shader plus this random color script the whole screen renders with 13 batches and takes about 7-8ms on the CPU while taking 1.1ms on the GPU (That means the frame takes 7-8ms and the program is “CPU bound” meaning optimisations should focon on how to lessen the CPU load). Taking the CPU into consideration is fair here because the CPU has to figure out which meshes can be batches each frame and dispatch the data. When disabling instancing the batches jump up to 4803 taking around 14-15ms on the CPU and 11ms on the GPU, what I consider a clear improvement. As always if you have performance critical things in your own application theres no magic bullet, the best thing you can have is being aware of the possibilities and trying all of them and seeing the advantages and disadvantages for your use case.</p>
<p><img src="/assets/images/posts/048/StatsComparison.png" alt="" /></p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/048_Instancing/ColorPropertySetter.cs">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/048_Instancing/ColorPropertySetter.cs</a></li>
</ul>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ColorPropertySetter</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
<span class="c1">//The color of the object</span>
<span class="k">public</span> <span class="n">Color</span> <span class="n">MaterialColor</span><span class="p">;</span>
<span class="c1">//The material property block we pass to the GPU</span>
<span class="k">private</span> <span class="n">MaterialPropertyBlock</span> <span class="n">propertyBlock</span><span class="p">;</span>
<span class="c1">// OnValidate is called in the editor after the component is edited</span>
<span class="k">void</span> <span class="nf">OnValidate</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">//create propertyblock only if none exists</span>
<span class="k">if</span> <span class="p">(</span><span class="n">propertyBlock</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
<span class="n">propertyBlock</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MaterialPropertyBlock</span><span class="p">();</span>
<span class="c1">//Get a renderer component either of the own gameobject or of a child</span>
<span class="n">Renderer</span> <span class="n">renderer</span> <span class="p">=</span> <span class="n">GetComponentInChildren</span><span class="p"><</span><span class="n">Renderer</span><span class="p">>();</span>
<span class="c1">//set the color property</span>
<span class="n">propertyBlock</span><span class="p">.</span><span class="nf">SetColor</span><span class="p">(</span><span class="s">"_Color"</span><span class="p">,</span> <span class="n">MaterialColor</span><span class="p">);</span>
<span class="c1">//apply propertyBlock to renderer</span>
<span class="n">renderer</span><span class="p">.</span><span class="nf">SetPropertyBlock</span><span class="p">(</span><span class="n">propertyBlock</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/048_Instancing/MPBShader.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/048_Instancing/MPBShader.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/048_Instancing"</span> <span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="p">[</span><span class="n">PerRendererData</span><span class="p">]</span> <span class="n">_Color</span> <span class="p">(</span><span class="s">"Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span><span class="p">}</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//allow instancing</span>
<span class="cp">#pragma multi_compile_instancing
</span>
<span class="c1">//shader functions</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//use unity shader library</span>
<span class="cp">#include "UnityCG.cginc"
</span>
<span class="c1">//per vertex data that comes from the model/parameters</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="n">UNITY_VERTEX_INPUT_INSTANCE_ID</span>
<span class="p">};</span>
<span class="c1">//per vertex data that gets passed from the vertex to the fragment function</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">UNITY_VERTEX_INPUT_INSTANCE_ID</span>
<span class="p">};</span>
<span class="n">UNITY_INSTANCING_BUFFER_START</span><span class="p">(</span><span class="n">Props</span><span class="p">)</span>
<span class="n">UNITY_DEFINE_INSTANCED_PROP</span><span class="p">(</span><span class="n">float4</span><span class="p">,</span> <span class="n">_Color</span><span class="p">)</span>
<span class="n">UNITY_INSTANCING_BUFFER_END</span><span class="p">(</span><span class="n">Props</span><span class="p">)</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//setup instance id</span>
<span class="n">UNITY_SETUP_INSTANCE_ID</span><span class="p">(</span><span class="n">v</span><span class="p">);</span>
<span class="n">UNITY_TRANSFER_INSTANCE_ID</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">o</span><span class="p">);</span>
<span class="c1">//calculate the position in clip space to render the object</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//setup instance id</span>
<span class="n">UNITY_SETUP_INSTANCE_ID</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
<span class="c1">//get _Color Property from buffer</span>
<span class="n">fixed4</span> <span class="n">color</span> <span class="o">=</span> <span class="n">UNITY_ACCESS_INSTANCED_PROP</span><span class="p">(</span><span class="n">Props</span><span class="p">,</span> <span class="n">_Color</span><span class="p">);</span>
<span class="c1">//Return the color the Object is rendered in</span>
<span class="k">return</span> <span class="n">color</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>Ronja BöhringerCurrent StateInverse Lerp and Remap2020-01-08T00:00:00+01:002020-01-08T00:00:00+01:00https://www.ronja-tutorials.com/2020/01/08/invlerp_remap<p>In a <a href="/2018/05/03/interpolating-colors.html">previous tutorial</a> I explained how the builtin <code class="language-plaintext highlighter-rouge">lerp</code> function works. Now I want to add the inverse lerp as well as the remap functions to this. They’re not builtin functions so we’ll have to write our own implementations. While this is a tutorial that focuses on explaining mathematical concepts, they resolve into basic addition and multiplication pretty quickly so I hope it isn’t too hard.</p>
<p><img src="/assets/images/posts/047/result.png" alt="" /></p>
<h2 id="example-shader">Example Shader</h2>
<p>The base shader is pretty barebones, a little more complex than <a href="/basics.html">a completely unlit one</a>. I decided to write the custom functions in a separate include file which I named Interpolation.cginc, but you can just as well copy-paste the functions into your main shader file. As the “blending variable” I used the y component of the UV coordinates so it’s immediately visible what the function does over a gradient from 0 to 1.</p>
<p>A shader version for a regular linear interpolation looks like this:</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/047_InvLerp_Remap/Lerp"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_FromColor</span> <span class="p">(</span><span class="s">"From Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="c1">//the base color</span>
<span class="n">_ToColor</span> <span class="p">(</span><span class="s">"To Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span> <span class="c1">//the color to blend to</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span><span class="p">}</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//include useful shader functions</span>
<span class="cp">#include "UnityCG.cginc"
</span> <span class="cp">#include "Interpolation.cginc"
</span>
<span class="c1">//define vertex and fragment shader</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//the colors to blend between</span>
<span class="n">fixed4</span> <span class="n">_FromColor</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_ToColor</span><span class="p">;</span>
<span class="c1">//the object data that's put into the vertex shader</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the data that's used to generate fragments and can be read by the fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the vertex shader</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//convert the vertex positions from object space to clip space so they can be rendered</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">uv</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">uv</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="kt">float</span> <span class="n">blend</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">.</span><span class="n">y</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">_FromColor</span><span class="p">,</span> <span class="n">_ToColor</span><span class="p">,</span> <span class="n">blend</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And the “barebones” include file looks like this, the <code class="language-plaintext highlighter-rouge">ifdef</code> and <code class="language-plaintext highlighter-rouge">define</code> statements are there to allow including the file multiple times over multiple files without leading to errors.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//avoid multiple imports</span>
<span class="cp">#ifndef INTERPOLATION
#define INTERPOLATION
</span>
<span class="cm">/* //hlsl supports linear interpolation intrinsically so this isn't needed
float lerp(float from, float to, float rel){
return ((1 - rel) * from) + (rel * to);
}
*/</span>
<span class="cp">#endif
</span></code></pre></div></div>
<h2 id="inverse-lerp">Inverse Lerp</h2>
<p>Lerp does a interpolation that returns values between the <code class="language-plaintext highlighter-rouge">from</code> and <code class="language-plaintext highlighter-rouge">to</code> input values for interpolation values between 0 and 1. The inverse of that is a function which we can hand a third value and it’ll return how close that value is to the first or second value.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="n">inverseLerped</span> <span class="o">=</span> <span class="n">invLerp</span><span class="p">(</span><span class="n">from</span><span class="p">,</span> <span class="n">to</span><span class="p">,</span> <span class="n">value</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">result</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">from</span><span class="p">,</span> <span class="n">to</span><span class="p">,</span> <span class="n">inverseLerped</span><span class="p">);</span>
</code></pre></div></div>
<p>In this example the <code class="language-plaintext highlighter-rouge">result</code> should always be the same value as the input <code class="language-plaintext highlighter-rouge">value</code>. Similarly first doing a lerp and then an inverse lerp with the arguments chained like this shouldn’t change anything.</p>
<p>I like the straightforward way we can deduce the function. We start by making sure that if the value is the same as the lower bound, the function returns a <code class="language-plaintext highlighter-rouge">0</code>, we do this by subtracting the <code class="language-plaintext highlighter-rouge">from</code> variable from the value.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="nf">invLerp</span><span class="p">(</span><span class="kt">float</span> <span class="n">from</span><span class="p">,</span> <span class="kt">float</span> <span class="n">to</span><span class="p">,</span> <span class="kt">float</span> <span class="n">value</span><span class="p">){</span>
<span class="k">return</span> <span class="n">value</span> <span class="o">-</span> <span class="n">from</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>With this setup the function returns 0 when the input value is equal to the from variable. Next let’s ensure that a input value equal to the to variable results in a output of <code class="language-plaintext highlighter-rouge">1</code>. So far the output would be <code class="language-plaintext highlighter-rouge">from - to</code>, so lets divide the whole thing we wrote so far by <code class="language-plaintext highlighter-rouge">from - to</code>. With this the function is already done.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="nf">invLerp</span><span class="p">(</span><span class="kt">float</span> <span class="n">from</span><span class="p">,</span> <span class="kt">float</span> <span class="n">to</span><span class="p">,</span> <span class="kt">float</span> <span class="n">value</span><span class="p">){</span>
<span class="k">return</span> <span class="p">(</span><span class="n">value</span> <span class="o">-</span> <span class="n">from</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">to</span> <span class="o">-</span> <span class="n">from</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>With this you can can get gradients in a 0 to 1 range from any other gradient. You could replace all of the arguments with multidimensional vectors (<code class="language-plaintext highlighter-rouge">float2</code>, <code class="language-plaintext highlighter-rouge">float3</code>, <code class="language-plaintext highlighter-rouge">float4</code>), but it’s far less useful than with <code class="language-plaintext highlighter-rouge">lerp</code> unless you want a component-wise inverse lerp.</p>
<p><img src="/assets/images/posts/047/InvLerp.gif" alt="" /></p>
<p>The <code class="language-plaintext highlighter-rouge">smoothstep</code> function that’s built into hlsl does almost the same as our inverse lerp function, but it also applies cubic smoothing, so it’s marginally more expensive to calculate and only works between 0 and 1 while our function can also extrapolate. It’s best to try around with both to get a feel for which to use for which occasion. (I admit I use smoothstep a lot when invLerp would be better, just because I don’t have to add the function to the project…)</p>
<h2 id="remap">Remap</h2>
<p>I mentioned earlier that chaining inverse lerp and lerp with the same arguments results in no change. While this is still true, we can chain them with different arguments for the lower and upper bounds. The custom remap function I wrote takes 5 arguments, the source bounds as well as the target bounds and the original value. The “remap” action then remaps those values so the a original value of the source <code class="language-plaintext highlighter-rouge">from</code> value will become a target <code class="language-plaintext highlighter-rouge">from</code> value. Similarly the <code class="language-plaintext highlighter-rouge">to</code> values and those inbetween. This allows you to remap linear gradients however you want.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="nf">remap</span><span class="p">(</span><span class="kt">float</span> <span class="n">origFrom</span><span class="p">,</span> <span class="kt">float</span> <span class="n">origTo</span><span class="p">,</span> <span class="kt">float</span> <span class="n">targetFrom</span><span class="p">,</span> <span class="kt">float</span> <span class="n">targetTo</span><span class="p">,</span> <span class="kt">float</span> <span class="n">value</span><span class="p">){</span>
<span class="kt">float</span> <span class="n">rel</span> <span class="o">=</span> <span class="n">invLerp</span><span class="p">(</span><span class="n">origFrom</span><span class="p">,</span> <span class="n">origTo</span><span class="p">,</span> <span class="n">value</span><span class="p">);</span>
<span class="k">return</span> <span class="n">lerp</span><span class="p">(</span><span class="n">targetFrom</span><span class="p">,</span> <span class="n">targetTo</span><span class="p">,</span> <span class="n">rel</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This also has more of a use with vectors because it can be used to set the whitelevel of a color output. After also creating the same vector version for the invLerp function you can create a multidimensional version by replacing all <code class="language-plaintext highlighter-rouge">float</code> with the fitting vector version.</p>
<p><img src="/assets/images/posts/047/Remap.png" alt="" /></p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/047_InverseInterpolationAndRemap/Interpolation.cginc">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/047_InverseInterpolationAndRemap/Interpolation.cginc</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//avoid multiple imports</span>
<span class="cp">#ifndef INTERPOLATION
#define INTERPOLATION
</span>
<span class="cm">/* //hlsl supports linear interpolation intrinsically so this isn't needed
float lerp(float from, float to, float rel){
return ((1 - rel) * from) + (rel * to);
}
*/</span>
<span class="kt">float</span> <span class="nf">invLerp</span><span class="p">(</span><span class="kt">float</span> <span class="n">from</span><span class="p">,</span> <span class="kt">float</span> <span class="n">to</span><span class="p">,</span> <span class="kt">float</span> <span class="n">value</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span><span class="n">value</span> <span class="o">-</span> <span class="n">from</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">to</span> <span class="o">-</span> <span class="n">from</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">float4</span> <span class="nf">invLerp</span><span class="p">(</span><span class="n">float4</span> <span class="n">from</span><span class="p">,</span> <span class="n">float4</span> <span class="n">to</span><span class="p">,</span> <span class="n">float4</span> <span class="n">value</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span><span class="n">value</span> <span class="o">-</span> <span class="n">from</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">to</span> <span class="o">-</span> <span class="n">from</span><span class="p">);</span>
<span class="p">}</span>
<span class="kt">float</span> <span class="nf">remap</span><span class="p">(</span><span class="kt">float</span> <span class="n">origFrom</span><span class="p">,</span> <span class="kt">float</span> <span class="n">origTo</span><span class="p">,</span> <span class="kt">float</span> <span class="n">targetFrom</span><span class="p">,</span> <span class="kt">float</span> <span class="n">targetTo</span><span class="p">,</span> <span class="kt">float</span> <span class="n">value</span><span class="p">){</span>
<span class="kt">float</span> <span class="n">rel</span> <span class="o">=</span> <span class="n">invLerp</span><span class="p">(</span><span class="n">origFrom</span><span class="p">,</span> <span class="n">origTo</span><span class="p">,</span> <span class="n">value</span><span class="p">);</span>
<span class="k">return</span> <span class="n">lerp</span><span class="p">(</span><span class="n">targetFrom</span><span class="p">,</span> <span class="n">targetTo</span><span class="p">,</span> <span class="n">rel</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">float4</span> <span class="nf">remap</span><span class="p">(</span><span class="n">float4</span> <span class="n">origFrom</span><span class="p">,</span> <span class="n">float4</span> <span class="n">origTo</span><span class="p">,</span> <span class="n">float4</span> <span class="n">targetFrom</span><span class="p">,</span> <span class="n">float4</span> <span class="n">targetTo</span><span class="p">,</span> <span class="n">float4</span> <span class="n">value</span><span class="p">){</span>
<span class="n">float4</span> <span class="n">rel</span> <span class="o">=</span> <span class="n">invLerp</span><span class="p">(</span><span class="n">origFrom</span><span class="p">,</span> <span class="n">origTo</span><span class="p">,</span> <span class="n">value</span><span class="p">);</span>
<span class="k">return</span> <span class="n">lerp</span><span class="p">(</span><span class="n">targetFrom</span><span class="p">,</span> <span class="n">targetTo</span><span class="p">,</span> <span class="n">rel</span><span class="p">);</span>
<span class="p">}</span>
<span class="cp">#endif
</span></code></pre></div></div>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/047_InverseInterpolationAndRemap/InvLerp.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/047_InverseInterpolationAndRemap/InvLerp.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/047_InvLerp_Remap/InvLerp"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_SrcZeroValue</span> <span class="p">(</span><span class="s">"Src 0 Value"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span> <span class="c1">//source min value</span>
<span class="n">_SrcOneValue</span> <span class="p">(</span><span class="s">"Src 1 Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span> <span class="c1">//source max value</span>
<span class="n">_TargetZeroValue</span> <span class="p">(</span><span class="s">"Target 0 Value"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span> <span class="c1">//target min value</span>
<span class="n">_TargetOneValue</span> <span class="p">(</span><span class="s">"Target 1 Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span> <span class="c1">//target max value</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span><span class="p">}</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//include useful shader functions</span>
<span class="cp">#include "UnityCG.cginc"
</span> <span class="cp">#include "Interpolation.cginc"
</span>
<span class="c1">//define vertex and fragment shader</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//the colors to blend between</span>
<span class="n">fixed4</span> <span class="n">_SrcZeroValue</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_SrcOneValue</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_TargetZeroValue</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_TargetOneValue</span><span class="p">;</span>
<span class="c1">//the object data that's put into the vertex shader</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the data that's used to generate fragments and can be read by the fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the vertex shader</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//convert the vertex positions from object space to clip space so they can be rendered</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">uv</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">uv</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="kt">float</span> <span class="n">blend</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">.</span><span class="n">y</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">remap</span><span class="p">(</span><span class="n">_SrcZeroValue</span><span class="p">,</span> <span class="n">_SrcOneValue</span><span class="p">,</span> <span class="n">_TargetZeroValue</span><span class="p">,</span> <span class="n">_TargetOneValue</span><span class="p">,</span> <span class="n">blend</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/047_InverseInterpolationAndRemap/Remap.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/047_InverseInterpolationAndRemap/Remap.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/047_InvLerp_Remap/Remap"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_SrcZeroValue</span> <span class="p">(</span><span class="s">"Src 0 Value"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span> <span class="c1">//source min value</span>
<span class="n">_SrcOneValue</span> <span class="p">(</span><span class="s">"Src 1 Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span> <span class="c1">//source max value</span>
<span class="n">_TargetZeroValue</span> <span class="p">(</span><span class="s">"Target 0 Value"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span> <span class="c1">//target min value</span>
<span class="n">_TargetOneValue</span> <span class="p">(</span><span class="s">"Target 1 Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span> <span class="c1">//target max value</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span><span class="p">}</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//include useful shader functions</span>
<span class="cp">#include "UnityCG.cginc"
</span> <span class="cp">#include "Interpolation.cginc"
</span>
<span class="c1">//define vertex and fragment shader</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//the colors to blend between</span>
<span class="n">fixed4</span> <span class="n">_SrcZeroValue</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_SrcOneValue</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_TargetZeroValue</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_TargetOneValue</span><span class="p">;</span>
<span class="c1">//the object data that's put into the vertex shader</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the data that's used to generate fragments and can be read by the fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the vertex shader</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//convert the vertex positions from object space to clip space so they can be rendered</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">uv</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">uv</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="kt">float</span> <span class="n">blend</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">.</span><span class="n">y</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">remap</span><span class="p">(</span><span class="n">_SrcZeroValue</span><span class="p">,</span> <span class="n">_SrcOneValue</span><span class="p">,</span> <span class="n">_TargetZeroValue</span><span class="p">,</span> <span class="n">_TargetOneValue</span><span class="p">,</span> <span class="n">blend</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>Ronja BöhringerIn a previous tutorial I explained how the builtin lerp function works. Now I want to add the inverse lerp as well as the remap functions to this. They’re not builtin functions so we’ll have to write our own implementations. While this is a tutorial that focuses on explaining mathematical concepts, they resolve into basic addition and multiplication pretty quickly so I hope it isn’t too hard.Partial Derivatives (fwidth)2019-11-29T00:00:00+01:002019-11-29T00:00:00+01:00https://www.ronja-tutorials.com/2019/11/29/fwidth<p>The partial derivative functions ddx, ddy and fwidth are some of the least used hlsl functions and they look quite confusing at first, but I like them a lot and I think they have some straightforward useful use cases so I hope I can explain them to you. Since I’m explaining straightforward functions you don’t have to know a lot of shader programming for this, but you should have a rough overview over how to render simple things with shaders in unity. If don’t know the basics yet, I have a couple of <a href="/basics.html">tutorials on them here</a>.</p>
<p><img src="/assets/images/posts/046/fire.gif" alt="" /></p>
<h2 id="ddx-and-ddy">DDX and DDY</h2>
<p>“Derivative” is a fancy word which means “change of a function” at a point. In this case we can use any value and get the change between the neighboring speenspace pixels. <code class="language-plaintext highlighter-rouge">ddx</code> and <code class="language-plaintext highlighter-rouge">ddy</code> are the simpler 2 of the 3 functions, they compare values of two pixels next to each other vertically or horizontally. This isn’t something that’s possible with any other functions and relies on a special architetecture detail of the GPU you might not expect. Instead of calculating every pixel completely on their own, pixels are grouped in little 2x2 fields that are calculated in parallel and in those units any information can be compared. The ddx function returns the value of the subtraction of the left pixel of a horizontal pixel pair from the right pixel. The ddy pixel works similarly for the vertical axis. This means that two pixels in such a pixel pair always return the same value for ddx or ddy.</p>
<p><img src="/assets/images/posts/046/ddx_ddy.png" alt="" /></p>
<p>For a test I used a simple shader with UV coordinates and returned the derivative of the first component of the coordinate multiplied by an adjustable factor. The <code class="language-plaintext highlighter-rouge">.xxx</code> I used converts the 1d scalar value to a 3d value with the same value for all 3 components.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//calculate the change of the uv coordinate to the next pixel</span>
<span class="kt">float</span> <span class="n">derivative</span> <span class="o">=</span> <span class="n">ddx</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">*</span> <span class="n">_Factor</span><span class="p">;</span>
<span class="c1">//transform derivative to greyscale color</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">float4</span><span class="p">(</span><span class="n">derivative</span><span class="p">.</span><span class="n">xxx</span> <span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="n">col</span> <span class="o">*=</span> <span class="n">_Color</span><span class="p">;</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>When we now play around with this shader we can see that it changes color depending on how much the x value of the UVs changes in relation to the screen x pixels. If we zoom closer to the surface or scale it up theres less change per pixel and the surface gets darker. If we rotate the surface the change in uv.x is in the screen y axis instead of the x axis and the surface becomes again darker.</p>
<p><img src="/assets/images/posts/046/ddx_transform_change.gif" alt="" /></p>
<p>This alone can be very powerful. For example it’s possible to very quickly calculate low-quality normalmaps from depth maps from this and the tex2D function uses this internally to choose between mipmap levels. But the most frequent use I have needs the overall change of a value, not the directional one, this is what fwidth gives us.</p>
<h2 id="fwidth">fwidth</h2>
<p>If we want to combine ddx and ddy the most straightforward way to do that is to get their absolute values and then add them, so a custom implementation would look like this (you don’t have to add this code to your shader since the internal definition of fwidth already does this):</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="nf">fwidth</span><span class="p">(</span><span class="kt">float</span> <span class="n">value</span><span class="p">){</span>
<span class="k">return</span> <span class="n">abs</span><span class="p">(</span><span class="n">ddx</span><span class="p">(</span><span class="n">value</span><span class="p">))</span> <span class="o">+</span> <span class="n">abs</span><span class="p">(</span><span class="n">ddy</span><span class="p">(</span><span class="n">value</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If we replace the ddx in our test shader from earlier with an fwidth we can see that zooming or scaling still has the same effect, but rotating now only changes the brightness slightly, having the same grey at 90° angles, but being a bit brighter inbetween. We could eliminate the color change by writing our own fwidth function with a little bit of fancy trigonometry (we’d square the results of ddx and ddy and take the square root of the sum), but in most cases the higher quality of the math here isn’t worth the performance hit of the more complicated math.</p>
<p><img src="/assets/images/posts/046/fwidth_transform_change.gif" alt="" /></p>
<h2 id="non-aliased-step">Non-aliased step</h2>
<p>The #1 usecase for fwidth (at least for me) is to cut off gradients at a specific value into distinct fields without getting aliasing artefacts, this is used in many variations for effects like fire, water, toon lighting and many more. The most straightforward way to cut off a gradient this way is to take the step of the gradient value and the cutoff value and then do a linear interpolation with that step result from the color of one side to the color of the other side. This step introduces aliasing though, jaggy edges we usually want to avoid. The way to avoid this is to do a inverse lerp based on how much the value changes over a single pixel.</p>
<p>We start doing the non aliased step by first calculating the fwidth value of our gradient (I’ll use the UV x component here again, but anything works, try around what you can get away with!). Since the next step is to do the inverse lerp from half a pixel before the cutoff value to half a pixel after the cutoff value to get a whole pixel gradient we also divide the change by 2 here.</p>
<p>After successfully calculating half of the change, we can do the inverse lerp. Instead of that I also often use the smoothstep function since it’s a built-in function, but that one also does some smoothing we don’t need here, so it’s less effective overall. The inverse of a interpolation means that the calculation returns 0 if the input is equal to the first value or 1 if it’s equal to the second value and it returns the inbetween values as expected. To get it we subtract the lower edge of the range from the value, this moves the 0 intersection to the correct value. Then we divide by the difference between the lower to the upper edge of the function. Because this process allows input values outside of the specified edges we end the calculation by clamping the result between 0 and 1 with the saturate function.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//you can use almost any value as a gradient</span>
<span class="kt">float</span> <span class="n">gradient</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">.</span><span class="n">x</span><span class="p">;</span>
<span class="c1">//calculate the change</span>
<span class="kt">float</span> <span class="n">halfChange</span> <span class="o">=</span> <span class="n">fwidth</span><span class="p">(</span><span class="n">gradient</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
<span class="c1">//base the range of the inverse lerp on the change over one pixel</span>
<span class="kt">float</span> <span class="n">lowerEdge</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span> <span class="o">-</span> <span class="n">halfChange</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">upperEdge</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span> <span class="o">+</span> <span class="n">halfChange</span><span class="p">;</span>
<span class="c1">//do the inverse interpolation</span>
<span class="kt">float</span> <span class="n">stepped</span> <span class="o">=</span> <span class="p">(</span><span class="n">gradient</span> <span class="o">-</span> <span class="n">lowerEdge</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">upperEdge</span> <span class="o">-</span> <span class="n">lowerEdge</span><span class="p">);</span>
<span class="n">stepped</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">stepped</span><span class="p">);</span>
<span class="c1">//convert to greyscale color for output</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">float4</span><span class="p">(</span><span class="n">stepped</span><span class="p">.</span><span class="n">xxx</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here I compare the regular step function to the non aliased step we just wrote as well as the one that uses the <code class="language-plaintext highlighter-rouge">smoothstep</code> function. On the left surface you can see the aliasing jaggyness of the step function while the other two functions provide a smoother transition. I also can’t make out a definitive difference between the smoothstep version and the cheaper inverse lerp so I recommend you to stick with that instead of the builtin function.</p>
<p><img src="/assets/images/posts/046/stepcompare.png" alt="" /></p>
<h2 id="a-better-step">A better step?</h2>
<p>So far we can see better results with the new technique, but it’s also kind of bothersome to write and a bit slower. We can’t change the performance demands of the functions but I’d also argue that in 99.9% of all cases your performance bottleneck won’t be here, as mentioned previously every tex2d call also accesses those functions and thats by far not expensive part of a texture sample. What we can do is to write a custom function that’s easy to use as <code class="language-plaintext highlighter-rouge">step</code> and can work as a drag and drop replacement.</p>
<p>Step returns 1 if the first argument is smaller than the second and 0 otherwise. We’ll translate those arguments into the comparison value as the first argument and the gradient value as the second one and then we translate the code of the previous implementation into a function that only depends on those two arguments.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//smooth version of step</span>
<span class="kt">float</span> <span class="nf">aaStep</span><span class="p">(</span><span class="kt">float</span> <span class="n">compValue</span><span class="p">,</span> <span class="kt">float</span> <span class="n">gradient</span><span class="p">){</span>
<span class="kt">float</span> <span class="n">halfChange</span> <span class="o">=</span> <span class="n">fwidth</span><span class="p">(</span><span class="n">gradient</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
<span class="c1">//base the range of the inverse lerp on the change over one pixel</span>
<span class="kt">float</span> <span class="n">lowerEdge</span> <span class="o">=</span> <span class="n">compValue</span> <span class="o">-</span> <span class="n">halfChange</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">upperEdge</span> <span class="o">=</span> <span class="n">compValue</span> <span class="o">+</span> <span class="n">halfChange</span><span class="p">;</span>
<span class="c1">//do the inverse interpolation</span>
<span class="kt">float</span> <span class="n">stepped</span> <span class="o">=</span> <span class="p">(</span><span class="n">gradient</span> <span class="o">-</span> <span class="n">lowerEdge</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">upperEdge</span> <span class="o">-</span> <span class="n">lowerEdge</span><span class="p">);</span>
<span class="n">stepped</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">stepped</span><span class="p">);</span>
<span class="k">return</span> <span class="n">stepped</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="nf">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="kt">float</span> <span class="n">stepped</span> <span class="o">=</span> <span class="n">aaStep</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">.</span><span class="n">x</span><span class="p">);</span>
<span class="c1">//value to greyscale color with full alpha</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">float4</span><span class="p">(</span><span class="n">stepped</span><span class="p">.</span><span class="n">xxx</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="err">}</span>
</code></pre></div></div>
<h2 id="an-example">An example</h2>
<p>One nice use for step is to make procedural fire. I based this example loosely on <a href="https://twitter.com/febucci">Febucci’s</a> <a href="https://www.febucci.com/2019/05/fire-shader/">fire shader</a>.</p>
<p>We shift the texture UVs based on the time, and read from a noise texture, as the gradient how “intense” a fire is at any position, I’ll use a square of the inverse uv y component as that gets us a good amount fire with my noise texture (I used layered perlin noise, generated via <a href="/2018/10/13/baking_shaders.html">the texture baking tool I made a tutorial about</a>). Then I generated the cutoff values for the texture, for the shape I used the step between the noise texture and the gradient and for the edges between the colors I did the same but with some offset based on adjustable properties. To combine those colors we can start by making everything the “outer” color and then interpolating to the “inner” colors wherever they are visible.</p>
<p>I also modified the aaStep here to interpolate over 2 pixels instead of one by not dividing the result of the fwidth function by 2, this is something you can play around with and see what feels best for your use case.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//smooth version of step</span>
<span class="kt">float</span> <span class="nf">aaStep</span><span class="p">(</span><span class="kt">float</span> <span class="n">compValue</span><span class="p">,</span> <span class="kt">float</span> <span class="n">gradient</span><span class="p">){</span>
<span class="kt">float</span> <span class="n">change</span> <span class="o">=</span> <span class="n">fwidth</span><span class="p">(</span><span class="n">gradient</span><span class="p">);</span>
<span class="c1">//base the range of the inverse lerp on the change over two pixels</span>
<span class="kt">float</span> <span class="n">lowerEdge</span> <span class="o">=</span> <span class="n">compValue</span> <span class="o">-</span> <span class="n">change</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">upperEdge</span> <span class="o">=</span> <span class="n">compValue</span> <span class="o">+</span> <span class="n">change</span><span class="p">;</span>
<span class="c1">//do the inverse interpolation</span>
<span class="kt">float</span> <span class="n">stepped</span> <span class="o">=</span> <span class="p">(</span><span class="n">gradient</span> <span class="o">-</span> <span class="n">lowerEdge</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">upperEdge</span> <span class="o">-</span> <span class="n">lowerEdge</span><span class="p">);</span>
<span class="n">stepped</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">stepped</span><span class="p">);</span>
<span class="c1">//smoothstep version here would be `smoothstep(lowerEdge, upperEdge, gradient)`</span>
<span class="k">return</span> <span class="n">stepped</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//I square this here to make the fire look a bit more "full"</span>
<span class="kt">float</span> <span class="n">fireGradient</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">.</span><span class="n">y</span><span class="p">;</span>
<span class="n">fireGradient</span> <span class="o">=</span> <span class="n">fireGradient</span> <span class="o">*</span> <span class="n">fireGradient</span><span class="p">;</span>
<span class="c1">//calculate fire UVs and animate them</span>
<span class="n">float2</span> <span class="n">fireUV</span> <span class="o">=</span> <span class="n">TRANSFORM_TEX</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">,</span> <span class="n">_MainTex</span><span class="p">);</span>
<span class="n">fireUV</span><span class="p">.</span><span class="n">y</span> <span class="o">-=</span> <span class="n">_Time</span><span class="p">.</span><span class="n">y</span> <span class="o">*</span> <span class="n">_ScrollSpeed</span><span class="p">;</span>
<span class="c1">//get the noise texture</span>
<span class="kt">float</span> <span class="n">fireNoise</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">fireUV</span><span class="p">).</span><span class="n">x</span><span class="p">;</span>
<span class="c1">//calculate whether fire is visibe at all and which colors should be shown</span>
<span class="kt">float</span> <span class="n">outline</span> <span class="o">=</span> <span class="n">aaStep</span><span class="p">(</span><span class="n">fireNoise</span><span class="p">,</span> <span class="n">fireGradient</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">edge1</span> <span class="o">=</span> <span class="n">aaStep</span><span class="p">(</span><span class="n">fireNoise</span><span class="p">,</span> <span class="n">fireGradient</span> <span class="o">-</span> <span class="n">_Edge1</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">edge2</span> <span class="o">=</span> <span class="n">aaStep</span><span class="p">(</span><span class="n">fireNoise</span><span class="p">,</span> <span class="n">fireGradient</span> <span class="o">-</span> <span class="n">_Edge2</span><span class="p">);</span>
<span class="c1">//define shape of fire</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">_Color1</span> <span class="o">*</span> <span class="n">outline</span><span class="p">;</span>
<span class="c1">//add other colors</span>
<span class="n">col</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">col</span><span class="p">,</span> <span class="n">_Color2</span><span class="p">,</span> <span class="n">edge1</span><span class="p">);</span>
<span class="n">col</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">col</span><span class="p">,</span> <span class="n">_Color3</span><span class="p">,</span> <span class="n">edge2</span><span class="p">);</span>
<span class="c1">//uv to color</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/046/fire.gif" alt="" /></p>
<p>Here is a comparison of step vs. the new non aliased step. It’s not huge and if you have a pixely aesthetic it makes your game look worse, but I think it’s a good step to making your game look a little better, especially when you have a soft aesthetic and want the game to also look smooth at low-ish resolutions (along the lines of 720p, not pixel art).</p>
<p><img src="/assets/images/posts/046/FireComparison.png" alt="" /></p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/046_Partial_Derivatives/testing.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/046_Partial_Derivatives/testing.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/046_Partial_Derivatives/testing"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_Factor</span><span class="p">(</span><span class="s">"Factor"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">100</span><span class="p">))</span> <span class="o">=</span> <span class="mi">1</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span><span class="p">}</span>
<span class="n">Cull</span> <span class="n">Off</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//include useful shader functions</span>
<span class="cp">#include "UnityCG.cginc"
</span>
<span class="c1">//define vertex and fragment shader</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="kt">float</span> <span class="n">_Factor</span><span class="p">;</span>
<span class="c1">//the object data that's put into the vertex shader</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the data that's used to generate fragments and can be read by the fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the vertex shader</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//convert the vertex positions from object space to clip space so they can be rendered</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">uv</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">uv</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//calculate the change of the uv coordinate to the next pixel</span>
<span class="kt">float</span> <span class="n">derivative</span> <span class="o">=</span> <span class="n">fwidth</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">*</span> <span class="n">_Factor</span><span class="p">;</span>
<span class="c1">//transform derivative to greyscale color</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">float4</span><span class="p">(</span><span class="n">derivative</span><span class="p">.</span><span class="n">xxx</span> <span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/046_Partial_Derivatives/aa_step.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/046_Partial_Derivatives/aa_step.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/046_Partial_Derivatives/aaStep"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span><span class="p">}</span>
<span class="n">Cull</span> <span class="n">Off</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//include useful shader functions</span>
<span class="cp">#include "UnityCG.cginc"
</span>
<span class="c1">//define vertex and fragment shader</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//the object data that's put into the vertex shader</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the data that's used to generate fragments and can be read by the fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the vertex shader</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//convert the vertex positions from object space to clip space so they can be rendered</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">uv</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">uv</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//smooth version of step</span>
<span class="kt">float</span> <span class="n">aaStep</span><span class="p">(</span><span class="kt">float</span> <span class="n">compValue</span><span class="p">,</span> <span class="kt">float</span> <span class="n">gradient</span><span class="p">){</span>
<span class="kt">float</span> <span class="n">halfChange</span> <span class="o">=</span> <span class="n">fwidth</span><span class="p">(</span><span class="n">gradient</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
<span class="c1">//base the range of the inverse lerp on the change over one pixel</span>
<span class="kt">float</span> <span class="n">lowerEdge</span> <span class="o">=</span> <span class="n">compValue</span> <span class="o">-</span> <span class="n">halfChange</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">upperEdge</span> <span class="o">=</span> <span class="n">compValue</span> <span class="o">+</span> <span class="n">halfChange</span><span class="p">;</span>
<span class="c1">//do the inverse interpolation</span>
<span class="kt">float</span> <span class="n">stepped</span> <span class="o">=</span> <span class="p">(</span><span class="n">gradient</span> <span class="o">-</span> <span class="n">lowerEdge</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">upperEdge</span> <span class="o">-</span> <span class="n">lowerEdge</span><span class="p">);</span>
<span class="n">stepped</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">stepped</span><span class="p">);</span>
<span class="c1">//smoothstep version here would be `smoothstep(lowerEdge, upperEdge, gradient)`</span>
<span class="k">return</span> <span class="n">stepped</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="kt">float</span> <span class="n">stepped</span> <span class="o">=</span> <span class="n">aaStep</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">.</span><span class="n">x</span><span class="p">);</span>
<span class="c1">//value to greyscale color with full alpha</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">float4</span><span class="p">(</span><span class="n">stepped</span><span class="p">.</span><span class="n">xxx</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/046_Partial_Derivatives/Fire.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/046_Partial_Derivatives/Fire.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/046_Partial_Derivatives/fire"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_MainTex</span> <span class="p">(</span><span class="s">"Fire Noise"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
<span class="n">_ScrollSpeed</span><span class="p">(</span><span class="s">"Animation Speed"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">_Color1</span> <span class="p">(</span><span class="s">"Color 1"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">_Color2</span> <span class="p">(</span><span class="s">"Color 2"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">_Color3</span> <span class="p">(</span><span class="s">"Color 3"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">_Edge1</span> <span class="p">(</span><span class="s">"Edge 1-2"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">25</span>
<span class="n">_Edge2</span> <span class="p">(</span><span class="s">"Edge 2-3"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"transparent"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"transparent"</span><span class="p">}</span>
<span class="n">Cull</span> <span class="n">Off</span>
<span class="n">Blend</span> <span class="n">SrcAlpha</span> <span class="n">OneMinusSrcAlpha</span>
<span class="n">ZWrite</span> <span class="n">Off</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//include useful shader functions</span>
<span class="cp">#include "UnityCG.cginc"
</span>
<span class="c1">//define vertex and fragment shader</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//tint of the texture</span>
<span class="n">fixed4</span> <span class="n">_Color1</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_Color2</span><span class="p">;</span>
<span class="n">fixed4</span> <span class="n">_Color3</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">_Edge1</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">_Edge2</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">_ScrollSpeed</span><span class="p">;</span>
<span class="kt">sampler2D</span> <span class="n">_MainTex</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">_MainTex_ST</span><span class="p">;</span>
<span class="c1">//the object data that's put into the vertex shader</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the data that's used to generate fragments and can be read by the fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the vertex shader</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//convert the vertex positions from object space to clip space so they can be rendered</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">uv</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">uv</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//smooth version of step</span>
<span class="kt">float</span> <span class="n">aaStep</span><span class="p">(</span><span class="kt">float</span> <span class="n">compValue</span><span class="p">,</span> <span class="kt">float</span> <span class="n">gradient</span><span class="p">){</span>
<span class="kt">float</span> <span class="n">change</span> <span class="o">=</span> <span class="n">fwidth</span><span class="p">(</span><span class="n">gradient</span><span class="p">);</span>
<span class="c1">//base the range of the inverse lerp on the change over two pixels</span>
<span class="kt">float</span> <span class="n">lowerEdge</span> <span class="o">=</span> <span class="n">compValue</span> <span class="o">-</span> <span class="n">change</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">upperEdge</span> <span class="o">=</span> <span class="n">compValue</span> <span class="o">+</span> <span class="n">change</span><span class="p">;</span>
<span class="c1">//do the inverse interpolation</span>
<span class="kt">float</span> <span class="n">stepped</span> <span class="o">=</span> <span class="p">(</span><span class="n">gradient</span> <span class="o">-</span> <span class="n">lowerEdge</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">upperEdge</span> <span class="o">-</span> <span class="n">lowerEdge</span><span class="p">);</span>
<span class="n">stepped</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">stepped</span><span class="p">);</span>
<span class="c1">//smoothstep version here would be `smoothstep(lowerEdge, upperEdge, gradient)`</span>
<span class="k">return</span> <span class="n">stepped</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//I square this here to make the fire look a bit more "full"</span>
<span class="kt">float</span> <span class="n">fireGradient</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">.</span><span class="n">y</span><span class="p">;</span>
<span class="n">fireGradient</span> <span class="o">=</span> <span class="n">fireGradient</span> <span class="o">*</span> <span class="n">fireGradient</span><span class="p">;</span>
<span class="c1">//calculate fire UVs and animate them</span>
<span class="n">float2</span> <span class="n">fireUV</span> <span class="o">=</span> <span class="n">TRANSFORM_TEX</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">,</span> <span class="n">_MainTex</span><span class="p">);</span>
<span class="n">fireUV</span><span class="p">.</span><span class="n">y</span> <span class="o">-=</span> <span class="n">_Time</span><span class="p">.</span><span class="n">y</span> <span class="o">*</span> <span class="n">_ScrollSpeed</span><span class="p">;</span>
<span class="c1">//get the noise texture</span>
<span class="kt">float</span> <span class="n">fireNoise</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">fireUV</span><span class="p">).</span><span class="n">x</span><span class="p">;</span>
<span class="c1">//calculate whether fire is visibe at all and which colors should be shown</span>
<span class="kt">float</span> <span class="n">outline</span> <span class="o">=</span> <span class="n">aaStep</span><span class="p">(</span><span class="n">fireNoise</span><span class="p">,</span> <span class="n">fireGradient</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">edge1</span> <span class="o">=</span> <span class="n">aaStep</span><span class="p">(</span><span class="n">fireNoise</span><span class="p">,</span> <span class="n">fireGradient</span> <span class="o">-</span> <span class="n">_Edge1</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">edge2</span> <span class="o">=</span> <span class="n">aaStep</span><span class="p">(</span><span class="n">fireNoise</span><span class="p">,</span> <span class="n">fireGradient</span> <span class="o">-</span> <span class="n">_Edge2</span><span class="p">);</span>
<span class="c1">//define shape of fire</span>
<span class="n">fixed4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">_Color1</span> <span class="o">*</span> <span class="n">outline</span><span class="p">;</span>
<span class="c1">//add other colors</span>
<span class="n">col</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">col</span><span class="p">,</span> <span class="n">_Color2</span><span class="p">,</span> <span class="n">edge1</span><span class="p">);</span>
<span class="n">col</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">col</span><span class="p">,</span> <span class="n">_Color3</span><span class="p">,</span> <span class="n">edge2</span><span class="p">);</span>
<span class="c1">//uv to color</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>Ronja BöhringerThe partial derivative functions ddx, ddy and fwidth are some of the least used hlsl functions and they look quite confusing at first, but I like them a lot and I think they have some straightforward useful use cases so I hope I can explain them to you. Since I’m explaining straightforward functions you don’t have to know a lot of shader programming for this, but you should have a rough overview over how to render simple things with shaders in unity. If don’t know the basics yet, I have a couple of tutorials on them here.Handling Depth for Spheretracing2019-10-14T00:00:00+02:002019-10-14T00:00:00+02:00https://www.ronja-tutorials.com/2019/10/14/spheretracing-depth<p>In the last 2 tutorials of the volumetric rendering series I showed how
to trace 3d signed distance fields and how to shade the result. In my
opinion the biggest drawback of the state of the shader so far is the
way that independent objects interact with each other and with regular
meshes. They either don’t write to the depth buffer at all, or with the
shape of the mesh that’s used for them and the depth check is similarly
lacking. In this tutorial I want to show you how to make volumetric
rendering work with the depth buffer just as you expect it to.</p>
<p>This tutorial starts with the code of the <a href="/2019/08/15/spheretracing-shading.html">shaded spheretracing
tutorial</a> and you should at least understand
<a href="/2019/06/21/spheretracing-basics.html">spheretracing basics</a> before you try to understand it.</p>
<p><img src="/assets/images/posts/045/Result.png" alt="" /></p>
<h2 id="minor-adjustments">Minor adjustments</h2>
<p>If you followed my previous tutorials on volumetric rendering tutorials
I want to change a few small things for this one. Since we’re fixing the
depth, we can now enable the depth writing for the shader by adding
<code class="language-plaintext highlighter-rouge">ZWrite On</code> to the SubShader or Pass section. I also set the Render Type
to Opaque as well as define the render queue to render the object just
after regular geometry in the Tags of the SubShader.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//the material is completely non-transparent and is rendered just after opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry+1"</span> <span class="s">"DisableBatching"</span><span class="o">=</span><span class="s">"True"</span> <span class="s">"IgnoreProjector"</span><span class="o">=</span><span class="s">"True"</span><span class="p">}</span>
</code></pre></div></div>
<p>Additionally I want to change the setup of the tracing loop since the
color of the material is decided in a function call inside a
conditional(if) statement in the loop. I’m not 100% sure what operations
affect performance in which ways, but my gut feeling tells me that it’s
more efficient to do the heavy operations outside of the loop. And keep
calculations in conditional blocks as lightweight as possible. I
archived this by defining a <code class="language-plaintext highlighter-rouge">hitsurface</code> variable before the loop starts
and set it to false and if we actually hit a surface, it’s set to true
and the loop is aborted. This way we can then discard all pixels which
rays didn’t result in a surface hit after the loop and then return the
color. Another small change was that I renamed the function which
calculates the color to <code class="language-plaintext highlighter-rouge">renderSurface</code> and defined the samplePoint
variable before the loop so we still have access to it after it
finished. With this the new fragment function should look like this:</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fixed4</span> <span class="nf">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">){</span>
<span class="c1">//ray information</span>
<span class="n">float3</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">localPosition</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">viewDirection</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">progress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">samplePoint</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">bool</span> <span class="n">hitsurface</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="c1">//tracing loop</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">uint</span> <span class="n">iter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">iter</span> <span class="o"><</span> <span class="n">MAX_STEPS</span><span class="p">;</span> <span class="n">iter</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//get current location on ray</span>
<span class="n">samplePoint</span> <span class="o">=</span> <span class="n">pos</span> <span class="o">+</span> <span class="n">dir</span> <span class="o">*</span> <span class="n">progress</span><span class="p">;</span>
<span class="c1">//get distance to closest shape</span>
<span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="c1">//return color if inside shape</span>
<span class="k">if</span><span class="p">(</span><span class="n">distance</span> <span class="o"><</span> <span class="n">THICKNESS</span><span class="p">){</span>
<span class="n">hitsurface</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="k">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//go forwards</span>
<span class="n">progress</span> <span class="o">=</span> <span class="n">progress</span> <span class="o">+</span> <span class="n">distance</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//discard pixel if no shape was hit</span>
<span class="n">clip</span><span class="p">(</span><span class="n">hitsurface</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="c1">//return surface color</span>
<span class="k">return</span> <span class="n">renderSurface</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/045/TracedBeforeDepth.png" alt="" /></p>
<h2 id="output-custom-depth">Output Custom Depth</h2>
<p>If we don’t worry about the depth of our surface, the shader pipeline
automatically uses the depth of the triangles of the mesh. But we also
have the possibility on many platforms to write and compare whatever
depth value we want to. To write custom depth values we can either
return a struct from the fragment function with variables for both color
and depth or what I opted to do, remove the output type of the function
by changing it to void and instead define output variables for the color
and depth values.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">,</span> <span class="k">out</span> <span class="n">fixed4</span> <span class="n">color</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">,</span> <span class="k">out</span> <span class="kt">float</span> <span class="n">depth</span> <span class="o">:</span> <span class="n">SV_Depth</span><span class="p">){</span>
</code></pre></div></div>
<p>The color variable we can set to the value we returned previously. For
the depth value we have to calculate the distance to the camera, the
most forward way to do that is to get it from the clip space position.
We calculate the clip space position in a similar way we transform
vertices into clip space in the vertex shader, via the
<code class="language-plaintext highlighter-rouge">UnityObjectToClipPos</code> macro. The <code class="language-plaintext highlighter-rouge">float4</code> result of this also has the
<code class="language-plaintext highlighter-rouge">w</code> component which we have to divide by to get regular 3d values. Since
we only care about the depth, we only divide the <code class="language-plaintext highlighter-rouge">z</code> component by the
<code class="language-plaintext highlighter-rouge">w</code> component and assign the result to the depth output value. Because
we changed the function type to void we don’t have to return anything.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//calculate surface color</span>
<span class="n">color</span> <span class="o">=</span> <span class="n">renderSurface</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="c1">//calculate surface depth</span>
<span class="n">float4</span> <span class="n">tracedClipPos</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">float4</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">));</span>
<span class="n">depth</span> <span class="o">=</span> <span class="n">tracedClipPos</span><span class="p">.</span><span class="n">z</span> <span class="o">/</span> <span class="n">tracedClipPos</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/045/Result.png" alt="" /></p>
<h2 id="source">Source</h2>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/045_SphereTracingDepth/SphereTracingDepth.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/045_SphereTracingDepth/SphereTracingDepth.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/045_SphereTracingDepth"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_Color</span> <span class="p">(</span><span class="s">"Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered just after opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry+1"</span> <span class="s">"DisableBatching"</span><span class="o">=</span><span class="s">"True"</span> <span class="s">"IgnoreProjector"</span><span class="o">=</span><span class="s">"True"</span><span class="p">}</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">ZWrite</span> <span class="n">On</span>
<span class="n">CGPROGRAM</span>
<span class="cp">#include "UnityCG.cginc"
</span> <span class="cp">#include "Lighting.cginc"
</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//surface color</span>
<span class="n">fixed4</span> <span class="n">_Color</span><span class="p">;</span>
<span class="c1">//maximum amount of steps</span>
<span class="cp">#define MAX_STEPS 32
</span> <span class="c1">//furthest distance that's accepted as inside surface</span>
<span class="cp">#define THICKNESS 0.001
</span> <span class="c1">//distance from rendered point to sample SDF for normal calculation</span>
<span class="cp">#define NORMAL_EPSILON 0.01
</span>
<span class="c1">//input data</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//data that goes from vertex to fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span> <span class="c1">//position in clip space</span>
<span class="n">float4</span> <span class="n">localPosition</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span> <span class="c1">//position in local space</span>
<span class="n">float4</span> <span class="n">viewDirection</span> <span class="o">:</span> <span class="n">TEXCOORD1</span><span class="p">;</span> <span class="c1">//view direction in local space (not normalized!)</span>
<span class="p">};</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//position for rendering</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="c1">//save local position for origin</span>
<span class="n">o</span><span class="p">.</span><span class="n">localPosition</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">;</span>
<span class="c1">//get camera position in local space</span>
<span class="n">float4</span> <span class="n">objectSpaceCameraPos</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_WorldToObject</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">_WorldSpaceCameraPos</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
<span class="c1">//get local view vector</span>
<span class="n">o</span><span class="p">.</span><span class="n">viewDirection</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span> <span class="o">-</span> <span class="n">objectSpaceCameraPos</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">float</span> <span class="n">scene</span><span class="p">(</span><span class="n">float3</span> <span class="n">pos</span><span class="p">){</span>
<span class="k">return</span> <span class="n">length</span><span class="p">(</span><span class="n">pos</span><span class="p">)</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">float3</span> <span class="n">normal</span><span class="p">(</span><span class="n">float3</span> <span class="n">pos</span><span class="p">){</span>
<span class="c1">//determine change in signed distance</span>
<span class="kt">float</span> <span class="n">changeX</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">changeY</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">changeZ</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">));</span>
<span class="c1">//construct normal vector</span>
<span class="n">float3</span> <span class="n">surfaceNormal</span> <span class="o">=</span> <span class="n">float3</span><span class="p">(</span><span class="n">changeX</span><span class="p">,</span> <span class="n">changeY</span><span class="p">,</span> <span class="n">changeZ</span><span class="p">);</span>
<span class="c1">//convert normal vector into worldspace and make it uniform length</span>
<span class="n">surfaceNormal</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_ObjectToWorld</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">surfaceNormal</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="k">return</span> <span class="n">normalize</span><span class="p">(</span><span class="n">surfaceNormal</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">float4</span> <span class="n">lightColor</span><span class="p">(</span><span class="n">float3</span> <span class="n">position</span><span class="p">){</span>
<span class="c1">//calculate needed surface and light data</span>
<span class="n">float3</span> <span class="n">surfaceNormal</span> <span class="o">=</span> <span class="n">normal</span><span class="p">(</span><span class="n">position</span><span class="p">);</span>
<span class="n">float3</span> <span class="n">lightDirection</span> <span class="o">=</span> <span class="n">_WorldSpaceLightPos0</span><span class="p">.</span><span class="n">xyz</span><span class="p">;</span>
<span class="c1">//calculate simple shading</span>
<span class="kt">float</span> <span class="n">lightAngle</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">dot</span><span class="p">(</span><span class="n">surfaceNormal</span><span class="p">,</span> <span class="n">lightDirection</span><span class="p">));</span>
<span class="k">return</span> <span class="n">lightAngle</span> <span class="o">*</span> <span class="n">_LightColor0</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">float4</span> <span class="n">renderSurface</span><span class="p">(</span><span class="n">float3</span> <span class="n">position</span><span class="p">){</span>
<span class="c1">//get light color</span>
<span class="n">float4</span> <span class="n">light</span> <span class="o">=</span> <span class="n">lightColor</span><span class="p">(</span><span class="n">position</span><span class="p">);</span>
<span class="c1">//combine base color and light color</span>
<span class="n">float4</span> <span class="n">color</span> <span class="o">=</span> <span class="n">_Color</span> <span class="o">*</span> <span class="n">light</span><span class="p">;</span>
<span class="k">return</span> <span class="n">color</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">,</span> <span class="k">out</span> <span class="n">fixed4</span> <span class="n">color</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">,</span> <span class="k">out</span> <span class="kt">float</span> <span class="n">depth</span> <span class="o">:</span> <span class="n">SV_Depth</span><span class="p">){</span>
<span class="c1">//ray information</span>
<span class="n">float3</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">localPosition</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">viewDirection</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">progress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">samplePoint</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">bool</span> <span class="n">hitsurface</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="c1">//tracing loop</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">uint</span> <span class="n">iter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">iter</span> <span class="o"><</span> <span class="n">MAX_STEPS</span><span class="p">;</span> <span class="n">iter</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//get current location on ray</span>
<span class="n">samplePoint</span> <span class="o">=</span> <span class="n">pos</span> <span class="o">+</span> <span class="n">dir</span> <span class="o">*</span> <span class="n">progress</span><span class="p">;</span>
<span class="c1">//get distance to closest shape</span>
<span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="c1">//return color if inside shape</span>
<span class="k">if</span><span class="p">(</span><span class="n">distance</span> <span class="o"><</span> <span class="n">THICKNESS</span><span class="p">){</span>
<span class="n">hitsurface</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="k">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//go forwards</span>
<span class="n">progress</span> <span class="o">=</span> <span class="n">progress</span> <span class="o">+</span> <span class="n">distance</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//discard pixel if no shape was hit</span>
<span class="n">clip</span><span class="p">(</span><span class="n">hitsurface</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="c1">//calculate surface color</span>
<span class="n">color</span> <span class="o">=</span> <span class="n">renderSurface</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="c1">//calculate surface depth</span>
<span class="n">float4</span> <span class="n">tracedClipPos</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">float4</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">));</span>
<span class="n">depth</span> <span class="o">=</span> <span class="n">tracedClipPos</span><span class="p">.</span><span class="n">z</span> <span class="o">/</span> <span class="n">tracedClipPos</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>Ronja BöhringerIn the last 2 tutorials of the volumetric rendering series I showed how to trace 3d signed distance fields and how to shade the result. In my opinion the biggest drawback of the state of the shader so far is the way that independent objects interact with each other and with regular meshes. They either don’t write to the depth buffer at all, or with the shape of the mesh that’s used for them and the depth check is similarly lacking. In this tutorial I want to show you how to make volumetric rendering work with the depth buffer just as you expect it to.Spheretracing with Shading2019-08-15T00:00:00+02:002019-08-15T00:00:00+02:00https://www.ronja-tutorials.com/2019/08/15/spheretracing-shading<p>In a <a href="/2019/06/21/spheretracing-basics.html">previous tutorial</a> I showed how to trace signed distance functions to reveal their silouette. In this one I will show you how to expand that shader to add simple lighting and make the objects look more tangible.</p>
<h2 id="architecture-changes">Architecture Changes</h2>
<p>In the previous shader we returned a solid color after finding a surface the ray collides with. To add lighting or other effects we have to expand this part. To keep the shader as readable as possible we’ll do a function call in this place and return the result of the function. This material function will calculate the light and combine the lighting with the surface color. To make this function as modular as possible I decided to put the light calculations in another function and the calculations for the normal of the surface in yet another one. If fewer more monolithic functions are more readable to you, feel free to structure your shader like that.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">float3</span> <span class="nf">normal</span><span class="p">(</span><span class="n">float3</span> <span class="n">pos</span><span class="p">){</span>
<span class="c1">//calculate surface normal</span>
<span class="p">}</span>
<span class="n">float3</span> <span class="nf">lightColor</span><span class="p">(</span><span class="n">float3</span> <span class="n">pos</span><span class="p">){</span>
<span class="c1">//calculate light color</span>
<span class="p">}</span>
<span class="n">float4</span> <span class="nf">material</span><span class="p">(</span><span class="n">float3</span> <span class="n">pos</span><span class="p">){</span>
<span class="c1">//return final surface color</span>
<span class="p">}</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//ray information</span>
<span class="n">float3</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">localPosition</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">viewDirection</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">progress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="c1">//tracing loop</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">uint</span> <span class="n">iter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">iter</span> <span class="o"><</span> <span class="n">MAX_STEPS</span><span class="p">;</span> <span class="n">iter</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//get current location on ray</span>
<span class="n">float3</span> <span class="n">samplePoint</span> <span class="o">=</span> <span class="n">pos</span> <span class="o">+</span> <span class="n">dir</span> <span class="o">*</span> <span class="n">progress</span><span class="p">;</span>
<span class="c1">//get distance to closest shape</span>
<span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="c1">//return color if inside shape</span>
<span class="k">if</span><span class="p">(</span><span class="n">distance</span> <span class="o"><</span> <span class="n">THICKNESS</span><span class="p">){</span>
<span class="k">return</span> <span class="n">material</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">//go forwards</span>
<span class="n">progress</span> <span class="o">=</span> <span class="n">progress</span> <span class="o">+</span> <span class="n">distance</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//discard pixel if no shape was hit</span>
<span class="n">clip</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="normal">Normal</h2>
<p>One of the most important variables when calculating light in a shader is the surface normal. Unlike a mesh where the normal is embeded into the data, we have to get the normal from the scene function ourselves in this case. We use the fact that we can see a signed distance field as a function to calculate the normal. The normal is the direction in which the value of the SDF grows. Even though a 3d function looks way more complex than a 1d function we can still use similar techniques to find the direction of it.</p>
<p>This “direction” of a function is also called its “derivative” and a common and simple way to find it is to look at two close points of the function and compare the change in value. With 1d functions we pass the function 2 different X values and divide the change in resulting Y values by the change in the X axis to get the rate at which the result of the function changes.</p>
<p><img src="/assets/images/posts/044/1d_derivative.png" alt="" /></p>
<p>When working with our 3d signed distance function we can do the same separately in 3 axis and since we only care about the direction and use the same change in position in all axis we don’t even have to care about the length of the resulting vector.</p>
<p>We call the change in position to get the normal “epsilon”. A epsilon that’s too big leads to surfaces that look too smooth and inaccurate, but a epsilon that’s too small can lead to calculation imprecisions, so it’s worth playing around with that value for your use case. I chose 0.01 as a starting value. The function to calculate the change in signed distance value is scenevalue at the position plus a little distance in a axis subtracted by the scene value at the position minus a tiny number on that axis. In code this looks like this:</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//determine change in signed distance</span>
<span class="kt">float</span> <span class="n">changeX</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">changeY</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">changeZ</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">));</span>
</code></pre></div></div>
<p>You can see how to get the derivative on the X axis we move the position a bit to the right and left on that axis.</p>
<p>After getting all those values we can combine them to one normal vector. Since we used such a tiny epsilon, the normal vector will also be very small, so to fix that we normalize it wich results in a normal vector with a length of 1. Since we decided the object is traced in object space, but a normal in worldspace is more useful we also do a matrix multiplication to convert it into worldspace before normalizing it. For the multiplication we add a 4th component which we set to 0, this makes the instruction only change the rotation and scale of the vector, but doesn’t move it since the normal is position independent.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//distance from rendered point to sample SDF for normal calculation</span>
<span class="cp">#define NORMAL_EPSILON 0.01
</span>
<span class="n">float3</span> <span class="nf">normal</span><span class="p">(</span><span class="n">float3</span> <span class="n">pos</span><span class="p">){</span>
<span class="c1">//determine change in signed distance</span>
<span class="kt">float</span> <span class="n">changeX</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">changeY</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">changeZ</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">));</span>
<span class="c1">//construct normal vector</span>
<span class="n">float3</span> <span class="n">surfaceNormal</span> <span class="o">=</span> <span class="n">float3</span><span class="p">(</span><span class="n">changeX</span><span class="p">,</span> <span class="n">changeY</span><span class="p">,</span> <span class="n">changeZ</span><span class="p">);</span>
<span class="c1">//convert normal vector into worldspace and make it uniform length</span>
<span class="n">surfaceNormal</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_ObjectToWorld</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">surfaceNormal</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="k">return</span> <span class="n">normalize</span><span class="p">(</span><span class="n">surfaceNormal</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If we now return the result of the normal function from the shader, we can see the worldspace normals which lead to a red/green/blue surface in the same direction as the direction gizmo in the corner.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//return color if inside shape</span>
<span class="k">if</span><span class="p">(</span><span class="n">distance</span> <span class="o"><</span> <span class="n">THICKNESS</span><span class="p">){</span>
<span class="k">return</span> <span class="n">float4</span><span class="p">(</span><span class="n">normal</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">),</span> <span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/044/normals.png" alt="" /></p>
<h2 id="lighting">Lighting</h2>
<p>The lighting function works the same as in any other context. The main difference to <a href="/2018/06/02/custom-lighting.html">the lighting implementations I’ve explained previously</a> we can’t use surface shaders so this implementation is more limited since that would need multiple shader passes. I’m not going to implement more than 1 light and won’t show how to make point lights work here, so we’re stuck with a single directional light.</p>
<p>We begin my retrieving the surface normal with the previously written function. Then we get the direction the light is coming from. In the case of directional lights this is always saved in <code class="language-plaintext highlighter-rouge">_WorldSpaceLightPos0.xyz</code>.</p>
<p>With this information we can do a simple lighting calculation. We get the dot product between direction and normal and then we use the saturate function to ensure the result is never negative. The result of the function is this falloff multiplied by the color of the light which is stored in <code class="language-plaintext highlighter-rouge">_LightColor0</code> to use the color of the light as a tint. It’s important that the <code class="language-plaintext highlighter-rouge">_LightColor0</code> variable is only available when we imclude the <code class="language-plaintext highlighter-rouge">Lighting.cginc</code> include file in our shader, so we also add that.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include "Lighting.cginc"
</span>
<span class="n">float4</span> <span class="nf">lightColor</span><span class="p">(</span><span class="n">float3</span> <span class="n">position</span><span class="p">){</span>
<span class="c1">//calculate needed surface and light data</span>
<span class="n">float3</span> <span class="n">surfaceNormal</span> <span class="o">=</span> <span class="n">normal</span><span class="p">(</span><span class="n">position</span><span class="p">);</span>
<span class="n">float3</span> <span class="n">lightDirection</span> <span class="o">=</span> <span class="n">_WorldSpaceLightPos0</span><span class="p">.</span><span class="n">xyz</span><span class="p">;</span>
<span class="c1">//calculate simple shading</span>
<span class="kt">float</span> <span class="n">lightAngle</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">dot</span><span class="p">(</span><span class="n">surfaceNormal</span><span class="p">,</span> <span class="n">lightDirection</span><span class="p">));</span>
<span class="k">return</span> <span class="n">lightAngle</span> <span class="o">*</span> <span class="n">_LightColor0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Printing out the result of the lighting function already looks like a plain white surface.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//return color if inside shape</span>
<span class="k">if</span><span class="p">(</span><span class="n">distance</span> <span class="o"><</span> <span class="n">THICKNESS</span><span class="p">){</span>
<span class="k">return</span> <span class="n">lightColor</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/044/light.png" alt="" /></p>
<h2 id="final-steps">Final Steps</h2>
<p>The last step for this shader I want to show is how to include the surface color into the shader again. For this we prepared the material function earlier which combines everything into the final result. In this implementation we just multiply the light we calculated with the color property which we added in the previous tutorial to get the final color.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">float4</span> <span class="nf">material</span><span class="p">(</span><span class="n">float3</span> <span class="n">position</span><span class="p">){</span>
<span class="c1">//get light color</span>
<span class="n">float4</span> <span class="n">light</span> <span class="o">=</span> <span class="n">lightColor</span><span class="p">(</span><span class="n">position</span><span class="p">);</span>
<span class="c1">//combine base color and light color</span>
<span class="n">float4</span> <span class="n">color</span> <span class="o">=</span> <span class="n">_Color</span> <span class="o">*</span> <span class="n">light</span><span class="p">;</span>
<span class="k">return</span> <span class="n">color</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/044/Result.png" alt="" /></p>
<p>You can expand this by reading a texture or generating a pattern in the material function or by using more complex or interresting lighting functions in the lightColor function and of course by using a more complex signed distance field, but I hope this tutorial gave you some insight into the basics and how to get to more complex implementations.</p>
<h2 id="source">Source</h2>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/044_SphereTracingShading/SphereTracingShading.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/044_SphereTracingShading/SphereTracingShading.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/044_SphereTracingShading"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_Color</span> <span class="p">(</span><span class="s">"Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span> <span class="s">"DisableBatching"</span><span class="o">=</span><span class="s">"True"</span><span class="p">}</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">ZWrite</span> <span class="n">Off</span>
<span class="n">CGPROGRAM</span>
<span class="cp">#include "UnityCG.cginc"
</span> <span class="cp">#include "Lighting.cginc"
</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//surface color</span>
<span class="n">fixed4</span> <span class="n">_Color</span><span class="p">;</span>
<span class="c1">//maximum amount of steps</span>
<span class="cp">#define MAX_STEPS 10
</span> <span class="c1">//furthest distance that's accepted as inside surface</span>
<span class="cp">#define THICKNESS 0.01
</span> <span class="c1">//distance from rendered point to sample SDF for normal calculation</span>
<span class="cp">#define NORMAL_EPSILON 0.01
</span>
<span class="c1">//input data</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//data that goes from vertex to fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span> <span class="c1">//position in clip space</span>
<span class="n">float4</span> <span class="n">localPosition</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span> <span class="c1">//position in local space</span>
<span class="n">float4</span> <span class="n">viewDirection</span> <span class="o">:</span> <span class="n">TEXCOORD1</span><span class="p">;</span> <span class="c1">//view direction in local space (not normalized!)</span>
<span class="p">};</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//position for rendering</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="c1">//save local position for origin</span>
<span class="n">o</span><span class="p">.</span><span class="n">localPosition</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">;</span>
<span class="c1">//get camera position in local space</span>
<span class="n">float4</span> <span class="n">objectSpaceCameraPos</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_WorldToObject</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">_WorldSpaceCameraPos</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
<span class="c1">//get local view vector</span>
<span class="n">o</span><span class="p">.</span><span class="n">viewDirection</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span> <span class="o">-</span> <span class="n">objectSpaceCameraPos</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">float</span> <span class="n">scene</span><span class="p">(</span><span class="n">float3</span> <span class="n">pos</span><span class="p">){</span>
<span class="k">return</span> <span class="n">length</span><span class="p">(</span><span class="n">pos</span><span class="p">)</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">float3</span> <span class="n">normal</span><span class="p">(</span><span class="n">float3</span> <span class="n">pos</span><span class="p">){</span>
<span class="c1">//determine change in signed distance</span>
<span class="kt">float</span> <span class="n">changeX</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">changeY</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="kt">float</span> <span class="n">changeZ</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">))</span> <span class="o">-</span> <span class="n">scene</span><span class="p">(</span><span class="n">pos</span> <span class="o">-</span> <span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">NORMAL_EPSILON</span><span class="p">));</span>
<span class="c1">//construct normal vector</span>
<span class="n">float3</span> <span class="n">surfaceNormal</span> <span class="o">=</span> <span class="n">float3</span><span class="p">(</span><span class="n">changeX</span><span class="p">,</span> <span class="n">changeY</span><span class="p">,</span> <span class="n">changeZ</span><span class="p">);</span>
<span class="c1">//convert normal vector into worldspace and make it uniform length</span>
<span class="n">surfaceNormal</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_ObjectToWorld</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">surfaceNormal</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="k">return</span> <span class="n">normalize</span><span class="p">(</span><span class="n">surfaceNormal</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">float4</span> <span class="n">lightColor</span><span class="p">(</span><span class="n">float3</span> <span class="n">position</span><span class="p">){</span>
<span class="c1">//calculate needed surface and light data</span>
<span class="n">float3</span> <span class="n">surfaceNormal</span> <span class="o">=</span> <span class="n">normal</span><span class="p">(</span><span class="n">position</span><span class="p">);</span>
<span class="n">float3</span> <span class="n">lightDirection</span> <span class="o">=</span> <span class="n">_WorldSpaceLightPos0</span><span class="p">.</span><span class="n">xyz</span><span class="p">;</span>
<span class="c1">//calculate simple shading</span>
<span class="kt">float</span> <span class="n">lightAngle</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">dot</span><span class="p">(</span><span class="n">surfaceNormal</span><span class="p">,</span> <span class="n">lightDirection</span><span class="p">));</span>
<span class="k">return</span> <span class="n">lightAngle</span> <span class="o">*</span> <span class="n">_LightColor0</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">float4</span> <span class="n">material</span><span class="p">(</span><span class="n">float3</span> <span class="n">position</span><span class="p">){</span>
<span class="c1">//get light color</span>
<span class="n">float4</span> <span class="n">light</span> <span class="o">=</span> <span class="n">lightColor</span><span class="p">(</span><span class="n">position</span><span class="p">);</span>
<span class="c1">//combine base color and light color</span>
<span class="n">float4</span> <span class="n">color</span> <span class="o">=</span> <span class="n">_Color</span> <span class="o">*</span> <span class="n">light</span><span class="p">;</span>
<span class="k">return</span> <span class="n">color</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//ray information</span>
<span class="n">float3</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">localPosition</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">viewDirection</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">progress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="c1">//tracing loop</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">uint</span> <span class="n">iter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">iter</span> <span class="o"><</span> <span class="n">MAX_STEPS</span><span class="p">;</span> <span class="n">iter</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//get current location on ray</span>
<span class="n">float3</span> <span class="n">samplePoint</span> <span class="o">=</span> <span class="n">pos</span> <span class="o">+</span> <span class="n">dir</span> <span class="o">*</span> <span class="n">progress</span><span class="p">;</span>
<span class="c1">//get distance to closest shape</span>
<span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="c1">//return color if inside shape</span>
<span class="k">if</span><span class="p">(</span><span class="n">distance</span> <span class="o"><</span> <span class="n">THICKNESS</span><span class="p">){</span>
<span class="k">return</span> <span class="n">material</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">//go forwards</span>
<span class="n">progress</span> <span class="o">=</span> <span class="n">progress</span> <span class="o">+</span> <span class="n">distance</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//discard pixel if no shape was hit</span>
<span class="n">clip</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>Ronja BöhringerIn a previous tutorial I showed how to trace signed distance functions to reveal their silouette. In this one I will show you how to expand that shader to add simple lighting and make the objects look more tangible.Spheretracing Basics2019-06-21T00:00:00+02:002019-06-21T00:00:00+02:00https://www.ronja-tutorials.com/2019/06/21/spheretracing-basics<p>Raytracing is a huge topic and one that seems scary and unapproachable for many. One specific kind of raytracing we can do with signed distance fields which I have explored in the 2d space in previous tutorials is called spheretracing. In this first tutorial we’ll just trace the silouette of a sphere, but in future tutorials I’ll give examples how to make more complex shapes and do lighting.</p>
<p>As the base of the shader we’ll use a <a href="/basics.html">basic unlit shader</a>, so you can do this tutorial when you’re fairly new to shaders. If you do struggle with some of the concepts of signed distance fields though, have a look into <a href="/2018/11/10/2d-sdf-basics.html">my tutorial about 2d signed distance fields</a>.</p>
<h2 id="the-theory">The theory</h2>
<p>The central concept of raytracing is the ray. To construct a ray we need a origin and a direction. If we only do the raytracing inside of a mesh we can use the surface point of that mesh as the origin of the ray. The direction of the view ray is the vector from the camera to that surface point.</p>
<p>With this data we can take steps through our SDF scene. We’ll advance the distance of our distance field in the direction of our ray. We can do that because the definition of a distance field is that the closest surface is as far away as the return value of the distance function. As soon as we are close enough to a surface that we consider it a hit we know that the ray does hit the silouette. If the ray travelled too far or a maximum number of steps was reached that can be interpeted as a fail state and we can assume the ray never hits a scene object.</p>
<h2 id="preparing-the-data">Preparing the data</h2>
<p>As mentioned previously what we need to define a ray for each pixel is the origin and the direction of the ray. We can do the raytracing in any “space” we want to. If we do it in world space we can move around the object and it moves like a window into the traced world. If we do it in object space the raytraced objects will be moved, scaled and rotated with the object that’s moved. For this tutorial I’ll do the spheretracing in object space because it’s more intuitive and it’s a bit harder to do so you might be able to figure out how to do it in worldspace yourself if you want that.</p>
<p>As the origin of the ray we’ll use the local coordinates which is the data that’s given to the shader via the appdata struct. The object space view direction is a bit trickier - we get it by transforming the camera world position into object space and then subtracting it from the local position. To transform the camera position into local space we have to multiply the world to object matrix with it, but before this multiplication we have to transform it from a float3 into a float4 with a “1” as the w component. If we don’t do that the w component would be filled with a 0 and the movement would be ignored, only rotation and scale would be applied.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//input data</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//data that goes from vertex to fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span> <span class="c1">//position in clip space</span>
<span class="n">float4</span> <span class="n">localPosition</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span> <span class="c1">//position in local space</span>
<span class="n">float4</span> <span class="n">viewDirection</span> <span class="o">:</span> <span class="n">TEXCOORD1</span><span class="p">;</span> <span class="c1">//view direction in local space (not normalized!)</span>
<span class="p">};</span>
<span class="n">v2f</span> <span class="nf">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//position for rendering</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="c1">//save local position for origin</span>
<span class="n">o</span><span class="p">.</span><span class="n">localPosition</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">;</span>
<span class="c1">//get camera position in local space</span>
<span class="n">float4</span> <span class="n">objectSpaceCameraPos</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_WorldToObject</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">_WorldSpaceCameraPos</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
<span class="c1">//get local view vector</span>
<span class="n">o</span><span class="p">.</span><span class="n">viewDirection</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span> <span class="o">-</span> <span class="n">objectSpaceCameraPos</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="3d-signed-distance-functions">3d signed distance functions</h2>
<p>Signed distance functions work similarly in 3d as they do in 2d. In this tutorial I’ll only use a sphere, but if you’re curious about other shapes and how to combine them you can use those two sites: <a href="https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm">https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm</a>, <a href="http://mercury.sexy/hg_sdf/">http://mercury.sexy/hg_sdf/</a></p>
<p>The sphere is very similar to the circle in 2d. We first subtract the center of the sphere from the position we want to sample it at, then we calculate the length of the resulting vector and subtract the radius of the sphere to increase it’s size. Because this is a very simple example I’m going to place the sphere at the origin of the scene which means I don’t have to do the subtraction of the sphere center and give it a hardcoded size of <code class="language-plaintext highlighter-rouge">0.5</code>.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">float</span> <span class="nf">scene</span><span class="p">(</span><span class="n">float3</span> <span class="n">pos</span><span class="p">){</span>
<span class="k">return</span> <span class="n">length</span><span class="p">(</span><span class="n">pos</span><span class="p">)</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="fixed-step-ray-marching">Fixed step ray marching</h2>
<p>Before we take full advantage of our signed distance field, I want to show a more simplistic way of raytracing which just going fixed steps forwards through the space until it hits something. The advantage of this is that we can use it with any function that tells us wether a given point is inside or outside of a shape.</p>
<p>To do the raytracing we first have to set up three variables. The point where the ray starts, the direction of the ray and the progress we’ve already made on our ray. The starting point is the local position in our case which we passed via the v2f struct. The direction was also already calculated in the vertex shader, but we have to normalize it so it’s easier to work with before using it. We normalize this vector in the fragment and not the vertex shader because it would loose it wouldn’t have a length of 1 anymore after being interpolated between vertices. This is especially visible when the camera is close to low poly objects. Third we define the progress variable which starts at <code class="language-plaintext highlighter-rouge">0</code>.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//ray information</span>
<span class="n">float3</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">localPosition</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">viewDirection</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">progress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</code></pre></div></div>
<p>For the tracing itself we also have to decide on two more factors, how many steps we iterate through at maximum and how big the steps we will do will be. Because those are fixed, I’m going to use define statements, but if you’re more comfortable with variables or just writing in the numbers that’s also fine. Because we know the size and complexity of our shape fairly well we can make a pretty good guess what would be appropriate values. I decided to define 10 steps with a distance of 0.1 each. Note that you can use the define statements anywhere, but I decided that they’re best with the global variables that can also be manipulated by properties.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//how big steps to take when usign fixed steps</span>
<span class="cp">#define STEP_SIZE 0.1
</span><span class="c1">//maximum amount of steps</span>
<span class="cp">#define MAX_STEPS 10
</span></code></pre></div></div>
<p>And with all of this set up we can then finally write the loop that does the actual work. I used a for loop with a iterator that counts up and aborts when the iterator reaches the maximum amount of steps we defined. Inside the loop we first calculate the current point on the ray we’re on. We get this by solving the line equation of <code class="language-plaintext highlighter-rouge">point = origin + direction * progress</code>. Putting this result into the scene function then gives us the distance to the closest shape. Right now we’re only interrested in whether our current location is inside the shape or not so we check whether the distance smaller than <code class="language-plaintext highlighter-rouge">0</code> which would mean that it’s inside of a shape. If that check is successful we directly return the color we set via our property. If it isn’t inside the shape we increase our progress by the step size and the code goes into the next iteration of the loop. If the loop terminates without ever hitting a shape we assume it missed completely and return <code class="language-plaintext highlighter-rouge">0</code> for a completely black pixel.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//ray information</span>
<span class="n">float3</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">localPosition</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">viewDirection</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">progress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="c1">//tracing loop</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">uint</span> <span class="n">iter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">iter</span> <span class="o"><</span> <span class="n">MAX_STEPS</span><span class="p">;</span> <span class="n">iter</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//get current location on ray</span>
<span class="n">float3</span> <span class="n">samplePoint</span> <span class="o">=</span> <span class="n">pos</span> <span class="o">+</span> <span class="n">dir</span> <span class="o">*</span> <span class="n">progress</span><span class="p">;</span>
<span class="c1">//get distance to closest shape</span>
<span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="c1">//return color if inside shape</span>
<span class="k">if</span><span class="p">(</span><span class="n">distance</span> <span class="o"><</span> <span class="mi">0</span><span class="p">){</span>
<span class="k">return</span> <span class="n">_Color</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//go forwards</span>
<span class="n">progress</span> <span class="o">=</span> <span class="n">progress</span> <span class="o">+</span> <span class="n">STEP_SIZE</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//return black pixel if no shape was hit</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/043/TracedSphere.png" alt="" /></p>
<p>The main disadvantage of fixed step raymarching is that it’s often hard to choose a step size. With a step size that’s too short you do a lot of samples in areas where theres no shape anywhere close and loose a lot of calculation time doing that. If you choose a step size that’s too big it’s possible to jump through walls and shapes that should be visible are simply missing because they are between two samples.</p>
<h2 id="spheretracing">Spheretracing</h2>
<p>With signed distance fields we have more information than just is it inside a shape or not. We can also determine how close the closest shape is. If we go the distance to the closest shape forwards we cannot skip any shapes. So instead of using a fixed step spheretracing walks forward the current distance of the SDF.</p>
<p>The changes we make to our existing code are just that we completely get rid of the step size and instead add the distance we have anyways to the progress. Because we only go the distance to the closest shape further it’s impossible now to follow the ray inside of the surface. Instead we define a small thickness and accept it as a hit if the distance is smaller than that.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//maximum amount of steps</span>
<span class="cp">#define MAX_STEPS 10
</span><span class="c1">//furthest distance that's accepted as inside surface</span>
<span class="cp">#define THICKNESS 0.01
</span></code></pre></div></div>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//ray information</span>
<span class="n">float3</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">localPosition</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">viewDirection</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">progress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="c1">//tracing loop</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">uint</span> <span class="n">iter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">iter</span> <span class="o"><</span> <span class="n">MAX_STEPS</span><span class="p">;</span> <span class="n">iter</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//get current location on ray</span>
<span class="n">float3</span> <span class="n">samplePoint</span> <span class="o">=</span> <span class="n">pos</span> <span class="o">+</span> <span class="n">dir</span> <span class="o">*</span> <span class="n">progress</span><span class="p">;</span>
<span class="c1">//get distance to closest shape</span>
<span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="c1">//return color if inside shape</span>
<span class="k">if</span><span class="p">(</span><span class="n">distance</span> <span class="o"><</span> <span class="n">THICKNESS</span><span class="p">){</span>
<span class="k">return</span> <span class="n">_Color</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//go forwards</span>
<span class="n">progress</span> <span class="o">=</span> <span class="n">progress</span> <span class="o">+</span> <span class="n">distance</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//return black pixel if no shape was hit</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Your result shouldn’t look too different from the previous iteration, but I promise you it works way better with huge spaces as well as more delicate shapes.</p>
<p>One minor tweak I’ll also mention in this tutorial is how to make the object have the silouette of the traced shape instead of the mesh. For that you can discard the pixels with missed rays before returning black by calling the clip function with a negative argument.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//discard pixel if no shape was hit</span>
<span class="n">clip</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/043/ClippedSphere.png" alt="" /></p>
<h2 id="source">Source</h2>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/043_SphereTracingBasics/SphereTracingBasics.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/043_SphereTracingBasics/SphereTracingBasics.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/042_SphereTracingBasics"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_Color</span> <span class="p">(</span><span class="s">"Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="c1">//also disable batching so local coordinates are always valid</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span> <span class="s">"DisableBatching"</span><span class="o">=</span><span class="s">"True"</span><span class="p">}</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">ZWrite</span> <span class="n">Off</span>
<span class="n">CGPROGRAM</span>
<span class="cp">#include "UnityCG.cginc"
</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//silouette color</span>
<span class="n">fixed4</span> <span class="n">_Color</span><span class="p">;</span>
<span class="c1">//maximum amount of steps</span>
<span class="cp">#define MAX_STEPS 10
</span> <span class="c1">//furthest distance that's accepted as inside surface</span>
<span class="cp">#define THICKNESS 0.01
</span>
<span class="c1">//input data</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//data that goes from vertex to fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span> <span class="c1">//position in clip space</span>
<span class="n">float4</span> <span class="n">localPosition</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span> <span class="c1">//position in local space</span>
<span class="n">float4</span> <span class="n">viewDirection</span> <span class="o">:</span> <span class="n">TEXCOORD1</span><span class="p">;</span> <span class="c1">//view direction in local space (not normalized!)</span>
<span class="p">};</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//position for rendering</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="c1">//save local position for origin</span>
<span class="n">o</span><span class="p">.</span><span class="n">localPosition</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">;</span>
<span class="c1">//get camera position in local space</span>
<span class="n">float4</span> <span class="n">objectSpaceCameraPos</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_WorldToObject</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">_WorldSpaceCameraPos</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
<span class="c1">//get local view vector</span>
<span class="n">o</span><span class="p">.</span><span class="n">viewDirection</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span> <span class="o">-</span> <span class="n">objectSpaceCameraPos</span><span class="p">;</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">float</span> <span class="n">scene</span><span class="p">(</span><span class="n">float3</span> <span class="n">pos</span><span class="p">){</span>
<span class="k">return</span> <span class="n">length</span><span class="p">(</span><span class="n">pos</span><span class="p">)</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//ray information</span>
<span class="n">float3</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">localPosition</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">viewDirection</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">progress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="c1">//tracing loop</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">uint</span> <span class="n">iter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">iter</span> <span class="o"><</span> <span class="n">MAX_STEPS</span><span class="p">;</span> <span class="n">iter</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//get current location on ray</span>
<span class="n">float3</span> <span class="n">samplePoint</span> <span class="o">=</span> <span class="n">pos</span> <span class="o">+</span> <span class="n">dir</span> <span class="o">*</span> <span class="n">progress</span><span class="p">;</span>
<span class="c1">//get distance to closest shape</span>
<span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">scene</span><span class="p">(</span><span class="n">samplePoint</span><span class="p">);</span>
<span class="c1">//return color if inside shape</span>
<span class="k">if</span><span class="p">(</span><span class="n">distance</span> <span class="o"><</span> <span class="n">THICKNESS</span><span class="p">){</span>
<span class="k">return</span> <span class="n">_Color</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//go forwards</span>
<span class="n">progress</span> <span class="o">=</span> <span class="n">progress</span> <span class="o">+</span> <span class="n">distance</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//discard pixel if no shape was hit</span>
<span class="n">clip</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>Ronja BöhringerRaytracing is a huge topic and one that seems scary and unapproachable for many. One specific kind of raytracing we can do with signed distance fields which I have explored in the 2d space in previous tutorials is called spheretracing. In this first tutorial we’ll just trace the silouette of a sphere, but in future tutorials I’ll give examples how to make more complex shapes and do lighting.Dithering2019-05-11T00:00:00+02:002019-05-11T00:00:00+02:00https://www.ronja-tutorials.com/2019/05/11/dithering<p>We often use gradients of some kind in shaders, but there are cases where we’re limited to less shades of colors than we want to express. One common technique to fake having many different colors with only a few is dithering. In this tutorial I explain how to dither between two colors based on a given ratio, but it’s also possible to use dithering for more shades of color with more complex algorithms.</p>
<p><img src="/assets/images/posts/042/Result.gif" alt="" /></p>
<h2 id="simple-dithering">Simple Dithering</h2>
<p>For this first version we’re taking the red channel of the input texture as the ratio between the two colors. For the pattern how to combine them we use a “bayer dithering” pattern, it’s optimized to have as much difference between the neighboring pixels in the pattern. As the base for this shader I used the result of <a href="/2018/03/23/basic.html">the unlit shader with texture access</a>.</p>
<p>Getting access to the base color we want to dither is already done with with this texture sample, but we don’t know how to read from the dither pattern texture. Unless you use fancy mapping techniques like Return of Obra Dinn did, the most straightforward approach here is to use screenspace UV coordinates. I explain how to get the basic screenspace coordinates in <a href="/2019/01/20/screenspace-texture.html">this tutorial</a>. One thing that’s pretty special about dithering is that we don’t care about how big the dither texture is or how often it repeats on the screen. The only thing we care about is that one texture pixel maps to one screen pixel to use it exactly as intended. To archieve that we first multiply the sceenspace UVs by the screen size itself, creating a UV set that increases by 1 for every pixel. Then we divide that UV by the amount of pixels of the dither texture, creating a texture that goes from 0 to 1 every “dither texture size” pixels, always sampling the middle of the pixels.</p>
<p>When doing those calculations we can easily get the screen size from the x and y components of the builtin <code class="language-plaintext highlighter-rouge">_ScreenParams</code> variable. To get the size of the dither pattern we add a new variable to the shader that has the same name as the texture we want to know the size of, but with <code class="language-plaintext highlighter-rouge">_TexelSize</code> to the end of it’s name. Then instead of dividing by the size of the texture (the z and w components of this vector) we can also multiply with one divided by the size, this value is already saved in the x and y components of this vector. We do this because a multiplication is usually faster than a division.</p>
<p>Here are the 4x4 and 8x8 versions of the dither texture I used:</p>
<p><img src="/assets/images/posts/042/BayerDither4x4.png" alt="" class="pixelated" />
<img src="/assets/images/posts/042/BayerDither8x8.png" alt="" class="pixelated" /></p>
<p>It’s important to disable compression completely in unity, otherwise it will mess with your textures and it will look bad (the textures are so tiny that compression wouldn’t make much of a difference anyways). Which texture you use doesn’t matter that much, the 8x8 texture gives you similar results in small areas and leads to less banding with slowly changing values, so if you’re not sure use the bigger one.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//Shader Property</span>
<span class="n">_DitherPattern</span> <span class="p">(</span><span class="s">"Dithering Pattern"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
</code></pre></div></div>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//Shader Variables</span>
<span class="c1">//The dithering pattern</span>
<span class="kt">sampler2D</span> <span class="n">_DitherPattern</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">;</span>
</code></pre></div></div>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//the data that's used to generate fragments and can be read by the fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">screenPosition</span> <span class="o">:</span> <span class="n">TEXCOORD1</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//test fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="n">float2</span> <span class="n">screenPos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPosition</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPosition</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">ditherCoordinate</span> <span class="o">=</span> <span class="n">screenPos</span> <span class="o">*</span> <span class="n">_ScreenParams</span><span class="p">.</span><span class="n">xy</span> <span class="o">*</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">.</span><span class="n">xy</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">ditherValue</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_DitherPattern</span><span class="p">,</span> <span class="n">ditherCoordinate</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="k">return</span> <span class="n">ditherValue</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/042/DitherPattern.png" alt="" /></p>
<p>With this value in hand we can already compare it to the density of the dithering and render the result. For this case the <code class="language-plaintext highlighter-rouge">step</code> function is ideal, we can pipe in the dither value and the value of our texture to get a 0 or 1 binary result that’ll represent the value of the texture value by regulating the density of the pixels.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//texture value the dithering is based on</span>
<span class="kt">float</span> <span class="n">texColor</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//value from the dither pattern</span>
<span class="n">float2</span> <span class="n">screenPos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPosition</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPosition</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">ditherCoordinate</span> <span class="o">=</span> <span class="n">screenPos</span> <span class="o">*</span> <span class="n">_ScreenParams</span><span class="p">.</span><span class="n">xy</span> <span class="o">*</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">.</span><span class="n">xy</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">ditherValue</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_DitherPattern</span><span class="p">,</span> <span class="n">ditherCoordinate</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//combine dither pattern with texture value to get final result</span>
<span class="kt">float</span> <span class="n">col</span> <span class="o">=</span> <span class="n">step</span><span class="p">(</span><span class="n">ditherValue</span><span class="p">,</span> <span class="n">texColor</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/042/DitherGradient.png" alt="" /></p>
<p>If you want to make the dither colors anything but black/white you can use a linear interpolation with the value we just used as a color as the interpolation parameter.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//Shader Properties</span>
<span class="n">_Color1</span> <span class="p">(</span><span class="s">"Dither Color 1"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">_Color2</span> <span class="p">(</span><span class="s">"Dither Color 2"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//Shader variables</span>
<span class="n">float4</span> <span class="n">_Color1</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">_Color2</span><span class="p">;</span>
</code></pre></div></div>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//texture value the dithering is based on</span>
<span class="kt">float</span> <span class="n">texColor</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//value from the dither pattern</span>
<span class="n">float2</span> <span class="n">screenPos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPosition</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPosition</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">ditherCoordinate</span> <span class="o">=</span> <span class="n">screenPos</span> <span class="o">*</span> <span class="n">_ScreenParams</span><span class="p">.</span><span class="n">xy</span> <span class="o">*</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">.</span><span class="n">xy</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">ditherValue</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_DitherPattern</span><span class="p">,</span> <span class="n">ditherCoordinate</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//combine dither pattern with texture value to get final result</span>
<span class="kt">float</span> <span class="n">ditheredValue</span> <span class="o">=</span> <span class="n">step</span><span class="p">(</span><span class="n">ditherValue</span><span class="p">,</span> <span class="n">texColor</span><span class="p">);</span>
<span class="n">float4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">_Color1</span><span class="p">,</span> <span class="n">_Color2</span><span class="p">,</span> <span class="n">ditheredValue</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/042/DyedDither.png" alt="" /></p>
<h2 id="surface-shader">Surface Shader</h2>
<p>When we want to do the same in a surface shader the steps we have to take are a bit easier since we don’t have to write our own vetex shader to get access to the screenspace coordinates. Instead we just just have to add a variable called <code class="language-plaintext highlighter-rouge">ScreenPos</code> to the input struct. If you want to do this but don’t know yet how surface shaders work like, read <a href="/2018/03/30/simple-surface.html">the tutorial about them here</a>.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//input struct which is automatically filled by unity</span>
<span class="k">struct</span> <span class="n">Input</span> <span class="p">{</span>
<span class="n">float2</span> <span class="n">uv_MainTex</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">screenPos</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the surface shader function which sets parameters the lighting function then uses</span>
<span class="kt">void</span> <span class="nf">surf</span> <span class="p">(</span><span class="n">Input</span> <span class="n">i</span><span class="p">,</span> <span class="k">inout</span> <span class="n">SurfaceOutputStandard</span> <span class="n">o</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//texture value the dithering is based on</span>
<span class="kt">float</span> <span class="n">texColor</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv_MainTex</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//value from the dither pattern</span>
<span class="n">float2</span> <span class="n">screenPos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPos</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPos</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">ditherCoordinate</span> <span class="o">=</span> <span class="n">screenPos</span> <span class="o">*</span> <span class="n">_ScreenParams</span><span class="p">.</span><span class="n">xy</span> <span class="o">*</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">.</span><span class="n">xy</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">ditherValue</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_DitherPattern</span><span class="p">,</span> <span class="n">ditherCoordinate</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//combine dither pattern with texture value to get final result</span>
<span class="kt">float</span> <span class="n">ditheredValue</span> <span class="o">=</span> <span class="n">step</span><span class="p">(</span><span class="n">ditherValue</span><span class="p">,</span> <span class="n">texColor</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">Albedo</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">_Color1</span><span class="p">,</span> <span class="n">_Color2</span><span class="p">,</span> <span class="n">ditheredValue</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If you do this you might notice that the end result on your screen has a gradient based on the lighting. If you don’t want that you can also <a href="/2018/06/02/custom-lighting.html">write your own lighting function</a> and implement the dithering after the lighting was calculated, but I’m not going into that in this tutorial.</p>
<h2 id="fade-dither-transparency">Fade Dither Transparency</h2>
<p>I explained how to make your materials look transparent in <a href="/2018/04/06/simple-transparency.html">an earlier tutorial</a>, but when we use this our material becomes much more expensive to render because it becomes more important in which order objects are rendered since we can’t use the Z-buffer anymore (it’s not critical to understand what this means) and all objects at a position have to be rendered because the colors might mix. A solution to this problem is that we can discard pixels completely with the <code class="language-plaintext highlighter-rouge">clip</code> function. The main draw of this function is that it can only completely draw or discard a pixel, no inbetween values, this is where dithering comes in, but by drawing some of the pixels and discarding the rest with a dither pattern the result looks again like a semitransparent surface. In this example I’m going to show how to convert the surface shader variant into a shader that fades out when it comes close to the camera, but it works just as well for the unlit variant.</p>
<p>The clip function discards a pixel when it’s passed a value that’s lower than 0 and does nothing if the argument is 0 or higher. We can use this to do dithered transparency without even using the step function. By subtracting the dither pattern from the value we want to encode as a dithered pattern we get values that are lower than 0 in the correct relation to values that are not.</p>
<p>If you use the alpha channel instead of the red channel for discarding pixels here you can do normal transparency like this without having to pay the full rendering cost that comes with it. (There are other disadvantages, but it’s worth a shot if that’s whats killing your performance. Especially in particles where you can get a lot of overdraw otherwise this can be useful)</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//the surface shader function which sets parameters the lighting function then uses</span>
<span class="kt">void</span> <span class="nf">surf</span> <span class="p">(</span><span class="n">Input</span> <span class="n">i</span><span class="p">,</span> <span class="k">inout</span> <span class="n">SurfaceOutputStandard</span> <span class="n">o</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//read texture and write it to diffuse color</span>
<span class="n">float4</span> <span class="n">texColor</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv_MainTex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">Albedo</span> <span class="o">=</span> <span class="n">texColor</span><span class="p">.</span><span class="n">rgb</span><span class="p">;</span>
<span class="c1">//value from the dither pattern</span>
<span class="n">float2</span> <span class="n">screenPos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPos</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPos</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">ditherCoordinate</span> <span class="o">=</span> <span class="n">screenPos</span> <span class="o">*</span> <span class="n">_ScreenParams</span><span class="p">.</span><span class="n">xy</span> <span class="o">*</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">.</span><span class="n">xy</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">ditherValue</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_DitherPattern</span><span class="p">,</span> <span class="n">ditherCoordinate</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//discard pixels accordingly</span>
<span class="n">clip</span><span class="p">(</span><span class="n">texColor</span><span class="p">.</span><span class="n">r</span> <span class="o">-</span> <span class="n">ditherValue</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/042/DitherTransGradient.png" alt="" /></p>
<p>We can already get the approximate distance to the camera by taking the 4th component of the screen position variable. After we have that we can then remap it to be 0 at the closest distance where surface is completely hidden and 1 at the furthest distance where the surface is completely visible. This operation is like the opposite of a linear interpolation and we can do it by first ensuring the zero is correct by subtracting the minimum fade distance from the distance and then dividing the result by the different between the maximum and minimum fade distance to move the point that was equal to the maximum before to 1.</p>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//the surface shader function which sets parameters the lighting function then uses</span>
<span class="kt">void</span> <span class="nf">surf</span> <span class="p">(</span><span class="n">Input</span> <span class="n">i</span><span class="p">,</span> <span class="k">inout</span> <span class="n">SurfaceOutputStandard</span> <span class="n">o</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//read texture and write it to diffuse color</span>
<span class="n">float3</span> <span class="n">texColor</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv_MainTex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">Albedo</span> <span class="o">=</span> <span class="n">texColor</span><span class="p">.</span><span class="n">rgb</span><span class="p">;</span>
<span class="c1">//value from the dither pattern</span>
<span class="n">float2</span> <span class="n">screenPos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPos</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPos</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">ditherCoordinate</span> <span class="o">=</span> <span class="n">screenPos</span> <span class="o">*</span> <span class="n">_ScreenParams</span><span class="p">.</span><span class="n">xy</span> <span class="o">*</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">.</span><span class="n">xy</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">ditherValue</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_DitherPattern</span><span class="p">,</span> <span class="n">ditherCoordinate</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//get relative distance from the camera</span>
<span class="kt">float</span> <span class="n">relDistance</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPos</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="n">relDistance</span> <span class="o">=</span> <span class="n">relDistance</span> <span class="o">-</span> <span class="n">_MinDistance</span><span class="p">;</span>
<span class="n">relDistance</span> <span class="o">=</span> <span class="n">relDistance</span> <span class="o">/</span> <span class="p">(</span><span class="n">_MaxDistance</span> <span class="o">-</span> <span class="n">_MinDistance</span><span class="p">);</span>
<span class="c1">//discard pixels accordingly</span>
<span class="n">clip</span><span class="p">(</span><span class="n">relDistance</span> <span class="o">-</span> <span class="n">ditherValue</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/042/DitherPoles.png" alt="" /></p>
<h2 id="source">Source</h2>
<h3 id="unlit-binary-dither">Unlit Binary Dither</h3>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/042_Dithering/BW_Dithering.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/042_Dithering/BW_Dithering.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/042_Dithering/Basic"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_MainTex</span> <span class="p">(</span><span class="s">"Texture"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
<span class="n">_DitherPattern</span> <span class="p">(</span><span class="s">"Dithering Pattern"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
<span class="n">_Color1</span> <span class="p">(</span><span class="s">"Dither Color 1"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">_Color2</span> <span class="p">(</span><span class="s">"Dither Color 2"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">SubShader</span><span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span><span class="p">}</span>
<span class="n">Pass</span><span class="p">{</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//include useful shader functions</span>
<span class="cp">#include "UnityCG.cginc"
</span>
<span class="c1">//define vertex and fragment shader</span>
<span class="cp">#pragma vertex vert
</span> <span class="cp">#pragma fragment frag
</span>
<span class="c1">//texture and transforms of the texture</span>
<span class="kt">sampler2D</span> <span class="n">_MainTex</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">_MainTex_ST</span><span class="p">;</span>
<span class="c1">//The dithering pattern</span>
<span class="kt">sampler2D</span> <span class="n">_DitherPattern</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">;</span>
<span class="c1">//Dither colors</span>
<span class="n">float4</span> <span class="n">_Color1</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">_Color2</span><span class="p">;</span>
<span class="c1">//the object data that's put into the vertex shader</span>
<span class="k">struct</span> <span class="n">appdata</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">vertex</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the data that's used to generate fragments and can be read by the fragment shader</span>
<span class="k">struct</span> <span class="n">v2f</span><span class="p">{</span>
<span class="n">float4</span> <span class="n">position</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">screenPosition</span> <span class="o">:</span> <span class="n">TEXCOORD1</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the vertex shader</span>
<span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">){</span>
<span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
<span class="c1">//convert the vertex positions from object space to clip space so they can be rendered</span>
<span class="n">o</span><span class="p">.</span><span class="n">position</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">uv</span> <span class="o">=</span> <span class="n">TRANSFORM_TEX</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">uv</span><span class="p">,</span> <span class="n">_MainTex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">screenPosition</span> <span class="o">=</span> <span class="n">ComputeScreenPos</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">position</span><span class="p">);</span>
<span class="k">return</span> <span class="n">o</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//the fragment shader</span>
<span class="n">fixed4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_TARGET</span><span class="p">{</span>
<span class="c1">//texture value the dithering is based on</span>
<span class="kt">float</span> <span class="n">texColor</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//value from the dither pattern</span>
<span class="n">float2</span> <span class="n">screenPos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPosition</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPosition</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">ditherCoordinate</span> <span class="o">=</span> <span class="n">screenPos</span> <span class="o">*</span> <span class="n">_ScreenParams</span><span class="p">.</span><span class="n">xy</span> <span class="o">*</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">.</span><span class="n">xy</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">ditherValue</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_DitherPattern</span><span class="p">,</span> <span class="n">ditherCoordinate</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//combine dither pattern with texture value to get final result</span>
<span class="kt">float</span> <span class="n">ditheredValue</span> <span class="o">=</span> <span class="n">step</span><span class="p">(</span><span class="n">ditherValue</span><span class="p">,</span> <span class="n">texColor</span><span class="p">);</span>
<span class="n">float4</span> <span class="n">col</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">_Color1</span><span class="p">,</span> <span class="n">_Color2</span><span class="p">,</span> <span class="n">ditheredValue</span><span class="p">);</span>
<span class="k">return</span> <span class="n">col</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">Fallback</span> <span class="s">"Standard"</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="surface-camera-fade">Surface Camera Fade</h3>
<ul>
<li><a href="https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/042_Dithering/DistanceFade.shader">https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/042_Dithering/DistanceFade.shader</a></li>
</ul>
<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Shader</span> <span class="s">"Tutorial/042_Dithering/DistanceFade"</span><span class="p">{</span>
<span class="c1">//show values to edit in inspector</span>
<span class="n">Properties</span><span class="p">{</span>
<span class="n">_MainTex</span> <span class="p">(</span><span class="s">"Texture"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
<span class="n">_DitherPattern</span> <span class="p">(</span><span class="s">"Dithering Pattern"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
<span class="n">_MinDistance</span> <span class="p">(</span><span class="s">"Minimum Fade Distance"</span><span class="p">,</span> <span class="n">Float</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">_MaxDistance</span> <span class="p">(</span><span class="s">"Maximum Fade Distance"</span><span class="p">,</span> <span class="n">Float</span><span class="p">)</span> <span class="o">=</span> <span class="mi">1</span>
<span class="p">}</span>
<span class="n">SubShader</span> <span class="p">{</span>
<span class="c1">//the material is completely non-transparent and is rendered at the same time as the other opaque geometry</span>
<span class="n">Tags</span><span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span><span class="p">}</span>
<span class="n">CGPROGRAM</span>
<span class="c1">//the shader is a surface shader, meaning that it will be extended by unity in the background to have fancy lighting and other features</span>
<span class="c1">//our surface shader function is called surf and we use the default PBR lighting model</span>
<span class="cp">#pragma surface surf Standard
</span> <span class="cp">#pragma target 3.0
</span>
<span class="c1">//texture and transforms of the texture</span>
<span class="kt">sampler2D</span> <span class="n">_MainTex</span><span class="p">;</span>
<span class="c1">//The dithering pattern</span>
<span class="kt">sampler2D</span> <span class="n">_DitherPattern</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">;</span>
<span class="c1">//remapping of distance</span>
<span class="kt">float</span> <span class="n">_MinDistance</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">_MaxDistance</span><span class="p">;</span>
<span class="c1">//input struct which is automatically filled by unity</span>
<span class="k">struct</span> <span class="n">Input</span> <span class="p">{</span>
<span class="n">float2</span> <span class="n">uv_MainTex</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">screenPos</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">//the surface shader function which sets parameters the lighting function then uses</span>
<span class="kt">void</span> <span class="n">surf</span> <span class="p">(</span><span class="n">Input</span> <span class="n">i</span><span class="p">,</span> <span class="k">inout</span> <span class="n">SurfaceOutputStandard</span> <span class="n">o</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//read texture and write it to diffuse color</span>
<span class="n">float3</span> <span class="n">texColor</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">uv_MainTex</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">Albedo</span> <span class="o">=</span> <span class="n">texColor</span><span class="p">.</span><span class="n">rgb</span><span class="p">;</span>
<span class="c1">//value from the dither pattern</span>
<span class="n">float2</span> <span class="n">screenPos</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPos</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPos</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">ditherCoordinate</span> <span class="o">=</span> <span class="n">screenPos</span> <span class="o">*</span> <span class="n">_ScreenParams</span><span class="p">.</span><span class="n">xy</span> <span class="o">*</span> <span class="n">_DitherPattern_TexelSize</span><span class="p">.</span><span class="n">xy</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">ditherValue</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_DitherPattern</span><span class="p">,</span> <span class="n">ditherCoordinate</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="c1">//get relative distance from the camera</span>
<span class="kt">float</span> <span class="n">relDistance</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">screenPos</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="n">relDistance</span> <span class="o">=</span> <span class="n">relDistance</span> <span class="o">-</span> <span class="n">_MinDistance</span><span class="p">;</span>
<span class="n">relDistance</span> <span class="o">=</span> <span class="n">relDistance</span> <span class="o">/</span> <span class="p">(</span><span class="n">_MaxDistance</span> <span class="o">-</span> <span class="n">_MinDistance</span><span class="p">);</span>
<span class="c1">//discard pixels accordingly</span>
<span class="n">clip</span><span class="p">(</span><span class="n">relDistance</span> <span class="o">-</span> <span class="n">ditherValue</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">ENDCG</span>
<span class="p">}</span>
<span class="n">FallBack</span> <span class="s">"Standard"</span>
<span class="p">}</span>
</code></pre></div></div>Ronja BöhringerWe often use gradients of some kind in shaders, but there are cases where we’re limited to less shades of colors than we want to express. One common technique to fake having many different colors with only a few is dithering. In this tutorial I explain how to dither between two colors based on a given ratio, but it’s also possible to use dithering for more shades of color with more complex algorithms.