<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>FukaFuka Seika</title><description>フカフカ製菓</description><link>https://fukafukaseika.moe/</link><language>en</language><item><title>Obscure Unity Knowledge</title><link>https://fukafukaseika.moe/posts/obscure-unity-knowledge/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/obscure-unity-knowledge/</guid><description>Small Unity quirks worth remembering — GraphicsBuffer remapping, GPU flush-to-zero, and more.</description><pubDate>Wed, 21 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently I&apos;ve come across some weird things about Unity that you might never ever need to know. But I think I&apos;d still put them here in case one day I might need to recall them. These knowledges are small enough to not be worth separate posts, so, well, let them grow in here then.&lt;/p&gt;
&lt;h2&gt;1. Graphics Buffers are not mapped to persistent physical memory&lt;/h2&gt;
&lt;p&gt;Whenever you create a &lt;code&gt;ComputeBuffer&lt;/code&gt; or &lt;code&gt;GraphicsBuffer&lt;/code&gt;, what you have in hand is not always the same chunk of memory. Unity may reallocate the underlying physical memory, but keeps the managed buffer reference intact.&lt;/p&gt;
&lt;p&gt;In other words, the managed reference of &lt;code&gt;ComputeBuffer&lt;/code&gt; or &lt;code&gt;GraphicsBuffer&lt;/code&gt; is always the same, but the underlying physical memory address may change without Unity telling you this. Calling &lt;code&gt;GetNativePtr&lt;/code&gt; might give you different results even on the same &lt;code&gt;ComputeBuffer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I discovered this when I tried to develop a tool that tracks buffer allocation and deallocation. Back then, I thought the physical memory address is persistent (via &lt;code&gt;GetNativePtr&lt;/code&gt;), but then I noticed a significant amount of non-released buffers in my tool, despite walking through the debugger it shows that all these buffers&apos; &lt;code&gt;Dispose&lt;/code&gt; methods are properly called. Well, when I match a dispose call with a creation call, I checked two things - managed reference address and the native pointer. And this is the moment I found that despite the managed reference stays the same, the native pointer had changed.&lt;/p&gt;
&lt;p&gt;Well, what does it mean? It means &lt;strong&gt;you should never cache the native pointer&lt;/strong&gt;. In day-to-day Unity development, this is almost irrelevant. But this is crucial when it comes to Native Render Plugins, where you pass this pointer to your plugin. You must always call &lt;code&gt;GetNativePtr&lt;/code&gt; just before calling your plugin code.&lt;/p&gt;
&lt;p&gt;... Which means there might be another artifact - two different managed references pointing to the same piece of physical memory. This is fairly common in render graph implementations - if two logical resources have compatible descriptors and their lifespans do not overlap, then we could alias them to the same physical memory. But this is just my educated guess - I haven&apos;t encountered a case where this knowledge becomes significant when using &lt;code&gt;ComputeBuffer&lt;/code&gt; or &lt;code&gt;GraphicsBuffer&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;2. GPU Flush to Zero&lt;/h2&gt;
&lt;p&gt;When a floating point number is too small, it gets flushed to 0. This is a logical way to combat precision issues related to floats, but it would also induce subtle bugs that could be hard to track if you are not aware of it.&lt;/p&gt;
&lt;p&gt;I noticed this behavior when trying to pack data on the CPU side and send the data to the GPU. Essentially, I have 2 halves, and I pack them to float. When the first half is 0, obviously, the packed float becomes extraordinarily small. While this number is preserved when passed to the GPU, the GPU simply thinks it is noise and flushes the entire float to 0, making the second half erroneously 0 as well. In RenderDoc, the symptom would be that the value becomes 0 after stepping through any arithmetic on the said value.&lt;/p&gt;
&lt;p&gt;If you want finer technical details, &lt;a href=&quot;https://developer.nvidia.com/blog/cuda-pro-tip-flush-denormals-confidence/&quot;&gt;here&apos;s the NVIDIA article on denormal flushing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Simply put, do not use floats to pack values; always use unsigned integers.&lt;/p&gt;
</content:encoded></item><item><title>Grass Interaction - Part 4</title><link>https://fukafukaseika.moe/posts/grass-interaction-4/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/grass-interaction-4/</guid><description>Learn how to create physically based interactive grass in Unity. In this fourth part of the series, we will add vertex displacement to the grass blade by reading the influence texture, finalizing our implementation of sway and trample.</description><pubDate>Wed, 22 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this fourth part, we will finally add vertex displacement to the grass&apos;s vertex shader. We will use &lt;em&gt;shader graph&lt;/em&gt; - which should make things a touch easier. But most important logic will be put inside a &lt;em&gt;custom function&lt;/em&gt; node which should make it easier to include for readers that prefer a pure code approach.&lt;/p&gt;
&lt;h2&gt;Pendulum Library&lt;/h2&gt;
&lt;p&gt;Before diving into the vertex shader straight away, let&apos;s create a pendulum library. Recall what we have established in part one:&lt;/p&gt;
&lt;p&gt;The 3 cases for grass motion $\theta(t)$  are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Underdamped&lt;/strong&gt; : $\theta(t)=\theta_0*e^{-\gamma t}cos(\omega t)$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Overdamped&lt;/strong&gt;: $\theta(t)= e^{-\gamma t}(A_1e^{\omega t}+A_2e^{-\omega t})$ where $A_1 = \frac{\theta_0}{2} + \frac{\gamma\theta_0}{2\omega}$ and $A_2 = \frac{\theta_0}{2} - \frac{\gamma\theta_0}{2\omega}$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Critically damped&lt;/strong&gt;: $\theta(t) = \theta_0(1+\gamma t)e^{-\gamma t}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\gamma=\frac{b}{2*l_0}$&lt;/li&gt;
&lt;li&gt;$\omega_0 = \sqrt{\frac{g-k}{l_0}}$&lt;/li&gt;
&lt;li&gt;$\omega = \sqrt{abs(\gamma^2-\omega_0^2)}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The implementation is straightforward, with some cached computations here and there. We are using half precision because we don&apos;t really need single precision for this computation (I&apos;ve tested both; they aren&apos;t noticeably different).&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;Pendulum.hlsl&lt;/code&gt; file and type/copy-paste the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#pragma once  
  
/* Physical simulation of damped pendulums.  
 * * All formulae use &quot;small angle approximation&quot; ( sin(x) = x ) even for big angles.  
 * Which means it is not strictly correct but should be visually correct. */  
#define PENDULUM_GRAVITY 9.8  
  
half pendulum_omega_zero_sqr(half pendulumLen, half elasticity)  
{  
    return (elasticity + PENDULUM_GRAVITY)/pendulumLen;  
}  
  
half pendulum_gamma(half dampingCoef, half pendulumLen)  
{  
    return 0.5*dampingCoef/pendulumLen;  
}  
  
half pendulum_omega(half elasticity, half pendulumLen, half gamma)  
{  
    return sqrt(max(0,pendulum_omega_zero_sqr(pendulumLen, elasticity) - gamma*gamma));  
}  
  
/* In all cases, we assume initial conditions for f(t) -&amp;gt; θ:  
 *  -  f(0) = θ0 (initial angle) *  -  f&apos;(0) = 0 (no initial angular velocity) */  
half pendulum_underdamped(half initialAngle, half t, half elasticity, half pendulumLen, half dampingCoef)  
{  
    half gamma = pendulum_gamma(dampingCoef, pendulumLen);  
    return initialAngle*exp(-t*gamma)*cos(pendulum_omega(elasticity,pendulumLen,gamma)*t);  
}  
  
  
half pendulum_critically_damped(half initialAngle, half t, half elasticity, half pendulumLen, half dampingCoef)  
{  
    half gamma = pendulum_gamma(dampingCoef, pendulumLen);  
    return initialAngle * (1 + gamma * t) * exp(-t*gamma);  
}  
  
  
half pendulum_overdamped(half initialAngle, half t, half elasticity, half pendulumLen, half dampingCoef)  
{  
    half gamma = pendulum_gamma(dampingCoef, pendulumLen);  
    half omega0 = pendulum_omega_zero_sqr(pendulumLen, elasticity);  
    half r = sqrt( abs(gamma * gamma - omega0 * omega0) ) ;  
    half r1 = - gamma + r;  
    half r2 = - gamma - r;  
    half rdiff = r1 - r2;  
    half a = -initialAngle * r2 / rdiff;  
    half b = initialAngle * (1 + r1 / rdiff);  
    return a*exp(r1*t) + b*exp(r2*t);  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Current Angle and Displacement&lt;/h2&gt;
&lt;p&gt;We will now sample the texture we filled in the last part. The texture provides &lt;strong&gt;initial angle&lt;/strong&gt; and &lt;strong&gt;elapsed time&lt;/strong&gt; for our motion equation. We still need &lt;strong&gt;elasticity&lt;/strong&gt;, &lt;strong&gt;pendulum length&lt;/strong&gt;, and &lt;strong&gt;damping factor&lt;/strong&gt;. These will be passed into our vertex displace function and are exposed in the inspector.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;PlantVertexDisplace.hlsl&lt;/code&gt; and start with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;Pendulum.hlsl&quot;  
  
/* osParams layout  
 * x: min os y pos (anchor) * y: max os y pos * z: * w:  
 */
void PlantVertexDisplace_float(  
    in float3 osPos,  
    in float3 wsPos,  
    in float4 osParams,  
    in float dampingCoef,  
    in float elasticity,  
    in float4 influenceTex,  
    in float trample,  
    out float3 osDisplacement,  
    out float trampleStr  
)  
{  
    half angle = 0;  
    half initialAngle = length(influenceTex.xy);  
    half t = influenceTex.w;  
    half pendulumLen = osParams.y - osParams.x;  
    #if _PENDULUM_CRITICALLY_DAMPED  
    angle = pendulum_critically_damped(initialAngle, t, elasticity, pendulumLen, dampingCoef);    
    #elif _PENDULUM_OVERDAMPED    
    angle = pendulum_overdamped(initialAngle, t, elasticity, pendulumLen, dampingCoef);    
    #elif _PENDULUM_UNDERDAMPED    
    angle = pendulum_underdamped(initialAngle, t, elasticity, pendulumLen, dampingCoef);  
    #endif
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some explanations of the parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;osPos&lt;/code&gt; is the vertex&apos;s &lt;em&gt;Object Space&lt;/em&gt; position.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wsPos&lt;/code&gt; is the &lt;strong&gt;object&apos;s&lt;/strong&gt; &lt;em&gt;World Space&lt;/em&gt; position - not the world space position of the vertex. I have tried the latter, and it looked horrible. (You could try it yourself)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;osParam&lt;/code&gt;, the &lt;code&gt;x&lt;/code&gt; component is the object&apos;s root in object space, and &lt;code&gt;y&lt;/code&gt; is the object&apos;s highest point in object space. (float4 is actually an overshot, you could just use float2)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;influenceTex&lt;/code&gt; is the sampled influence texture&apos;s color value.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trample&lt;/code&gt; is the trample strength multiplier. We will discuss this later.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;osDisplacement&lt;/code&gt; is the object space displacement vector.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trampleStr&lt;/code&gt; is the resulting trample strength, again, discussed later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Physically speaking, the equation we should use is determined by the constants, but here we leave the choice to the artist instead - as they probably just want the grass to behave in a particular way by clicking a button, without thinking about how the constants should be adjusted to achieve that look. And to be honest, most of the time you would just want the &lt;em&gt;under-damped&lt;/em&gt; case. If that&apos;s true, then you could remove the two other cases.&lt;/p&gt;
&lt;p&gt;Optionally, you could make &lt;strong&gt;pendulum length&lt;/strong&gt; a standalone exposed constant for even more artistic control (while being less physically correct).&lt;/p&gt;
&lt;p&gt;We have the &lt;em&gt;current angle&lt;/em&gt; now. The next thing is to turn it into a displacement vector. To do this, we need to rotate the grass blade by the current angle. The axis of this rotation is perpendicular to the plane containing the grass blade and the &lt;strong&gt;direction vector&lt;/strong&gt; (the normalized &lt;strong&gt;rg&lt;/strong&gt; channel of the influence texture).&lt;/p&gt;
&lt;p&gt;Here&apos;s a diagram illustrating this rotation:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/vertex-displacement.webp&quot; alt=&quot;alt text{caption=grass shader vertex displacement diagram}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Note that we are not rotating &lt;code&gt;osPos&lt;/code&gt; directly, as the root of the object may not be where the object space position is zero. Therefore, we account for the &lt;em&gt;plannar displacement&lt;/em&gt; of the grass blade (displacement in xz plane relative to object space origin), and the height of the root configured via &lt;code&gt;osParams.x&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let&apos;s prepare for the rotation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float3 directionVect = normalize(float3(dir.x, 0.001, dir.y));  
float3 rotationAxis = cross(directionVect, float3(0,1,0));  
rotationAxis.y = max(rotationAxis.y, 0.001);  
rotationAxis = normalize(rotationAxis);  
float3 planarDisplacement = float3(osPos.x, 0, osPos.z);  
float3 grassBlade = osPos - planarDisplacement;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are plenty of small values here, because we don&apos;t want normalization to end up with a zero division. And when they do, the grass simply won&apos;t render.&lt;/p&gt;
&lt;p&gt;For the rotation itself, we will use &lt;strong&gt;Rodrigue&apos;s Rotation Formula&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;The formula is given by:&lt;/p&gt;
&lt;p&gt;$$\mathbf{v}_{\text{rot}} = \mathbf{v} \cos \theta + (\mathbf{k} \times \mathbf{v}) \sin \theta + \mathbf{k} (\mathbf{k} \cdot \mathbf{v}) (1 - \cos \theta)$$&lt;/p&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\mathbf{v}$ is the original vector to be rotated.&lt;/li&gt;
&lt;li&gt;$\mathbf{k}$ is the unit vector along the axis of rotation.&lt;/li&gt;
&lt;li&gt;$\theta$ is the angle of rotation.&lt;/li&gt;
&lt;li&gt;$\mathbf{v}_{\text{rot}}$ is the resulting rotated vector.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// Rodrigue rotation  
float3 afterRot = grassBlade * cos(angle) + cross(rotationAxis, grassBlade)* sin(angle) + rotationAxis*dot(rotationAxis,grassBlade)*(1-cos(angle));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;osDisplacement&lt;/code&gt; is simply:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;osDisplacement = grassBlade - afterRot;

// for now, set trample strength to 0
trampleStr = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: In &quot;standard&quot; implementation of interactive grass, you would typically find multiplying the displacement by either &lt;code&gt;uv.y&lt;/code&gt; or normalized height of the vertex. This is unnecessary in this implementation, as the rotation itself already accounts for the &quot;closer to root, less displacement&quot; effect.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This should be enough for setting up our interactive grass. Let&apos;s create a grass shader to see it in action!&lt;/p&gt;
&lt;h2&gt;The Grass Shader&lt;/h2&gt;
&lt;h3&gt;Properties&lt;/h3&gt;
&lt;p&gt;Create a &lt;strong&gt;Lit Shader Graph&lt;/strong&gt;, and add these properties to the blackboard:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/grass-interaction-properties.webp&quot; alt=&quot;alt text{caption=grass shader properties}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Scope&lt;/code&gt; of &lt;code&gt;_InfluenceTexture&lt;/code&gt; must be set to &lt;code&gt;Global&lt;/code&gt; so that it correctly references the global texture we set in EIR custom pass.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;_Pendulum&lt;/code&gt; enum is set as follows:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/pendulum-enum.webp&quot; alt=&quot;alt text{caption=grass shader pendulum enum}&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: Some values are not yet exposed since this is a test shader - particularly &lt;code&gt;osParams&lt;/code&gt;. You should definitely expose it later.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Plant Vertex Displace custom function&lt;/h3&gt;
&lt;p&gt;To use the function we created earlier, we will need to use a &lt;strong&gt;Custom Function&lt;/strong&gt; node. But it is usually better to create a &lt;strong&gt;Sub Shader Graph&lt;/strong&gt; for it so that you could drop it in another shader with no need to create all the ins and outs. (You could always copy-paste it, though, but I find creating sub shader graphs easier to manage.)&lt;/p&gt;
&lt;p&gt;The process of creating this sub shader graph is tedious, but here it is...&lt;/p&gt;
&lt;p&gt;It&apos;s literally just a wrapper around a custom function node.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/vertex-displace-subsg.webp&quot; alt=&quot;alt text{caption=grass shader vertex displacement sub graph}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There&apos;s a warning about not having a half precision method. You could ignore it since we will use a single precision shader graph, anyway.&lt;/p&gt;
&lt;h3&gt;Sample Influence Texture&lt;/h3&gt;
&lt;p&gt;Back to the lit shader graph. The first thing we need to do is to sample the influence texture. Here we need to convert a world position to uv, and the correct formula for it is:&lt;/p&gt;
&lt;p&gt;$$uv = (worldPos.xz - worldOrigin.xz)/worldSize$$&lt;/p&gt;
&lt;p&gt;&lt;code&gt;worldSize&lt;/code&gt; and &lt;code&gt;worldOrigin&lt;/code&gt; should actually be read from global floats. I&apos;m cutting some corners here by hard-coding the world size into the shader graph. If you have multiple maps that vary in size, then don&apos;t copy me!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/sample-influence-texture.webp&quot; alt=&quot;alt text{caption=sample influence texture}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;Position&lt;/em&gt; node here is a legacy of trial-and-error. I used to take vertex position as &lt;code&gt;wsPos&lt;/code&gt; but it turned out horribly by having crazy deformations, therefore I changed it to an &lt;em&gt;Object&lt;/em&gt; node so that the whole object samples with the same uv, giving a more consistent result.&lt;/p&gt;
&lt;h2&gt;Wire Up Plant Vertex Displace&lt;/h2&gt;
&lt;p&gt;There&apos;s not much to explain here, except for the hard-coded &lt;code&gt;osParams&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/vertex-displace-wire-up.webp&quot; alt=&quot;alt text{caption=grass shader vertex displacement wire up}&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: Well, I notice I named a parameter &lt;code&gt;InfluenceTexA&lt;/code&gt; here. This is again a legacy naming. I used to have 2 influence textures before I figured out how to pack everything into one texture.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Final Vertex Position&lt;/h2&gt;
&lt;p&gt;Finally, we could compute the vertex position (object space). This should simply be vertex&apos;s object space position plus the displacement. But here, we limit the height of the resulting position so that it never exceeds the original height (more of a precaution).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/final-vertex-pos.webp&quot; alt=&quot;alt text{caption=final vertex position wire up}&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Testing&lt;/h3&gt;
&lt;p&gt;Alright, now you could create a material and put it on your grass. If you don&apos;t have any... you could create a &lt;em&gt;Tree&lt;/em&gt; with &lt;em&gt;3D Object / Tree&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Next, create an object with &lt;code&gt;EntityInfluenceProvide&lt;/code&gt;, set its radius and height accordingly. The following example shows an object with radius 25 and height 5.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/grass-interaction-test-1.webp&quot; alt=&quot;alt text{caption=grass interaction test}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Drag around the object and play with the constants to see the interaction in action. You might need to enter play mode to see it smoothly.&lt;/p&gt;
&lt;h2&gt;Entity Elevation&lt;/h2&gt;
&lt;p&gt;The previous test shows we are ignoring the elevation of the entity:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/entity-elevation-problem.webp&quot; alt=&quot;alt text{caption=entity elevation problem}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And indeed... we haven&apos;t used the elevation we calculated in EIR custom pass yet. Let&apos;s fix it now.&lt;/p&gt;
&lt;p&gt;The idea is to convert the entity&apos;s elevation to something comparable to the grass blade&apos;s height. The following lines show an normalization approach:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float entityElevation = influenceTex.z;  
// determine press strength (reduce strength when the object is above the grass)  
float rawStr = 1 - (entityElevation - wsPos.y)/max(0.1,osParams.y - osParams.x);  
float str = smoothstep(0.5, 1, rawStr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then, after calculating the rotation, we multiply the result by &lt;code&gt;str&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;osDisplacement *= str;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/entity-elevation-resolved.webp&quot; alt=&quot;alt text{caption=entity elevation problem resolved} &quot; /&gt;&lt;/p&gt;
&lt;p&gt;This solution is still simplistic and doesn&apos;t account for other cases, for example, when the entity is below the grass. But this is only relevant if you have underground sections.&lt;/p&gt;
&lt;h2&gt;Trample&lt;/h2&gt;
&lt;p&gt;Trample effect primarily considers the distance between the grass blade and the entity - the closer the entity is, the stronger our trample effect. However, we do not have this distance yet, and we do not have extra channels to store the distance in our texture. We could, of course, add another texture, but performance-wise that would be terrible. So instead, we could guess this distance from &lt;code&gt;initialAngle&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We know &lt;code&gt;initialAngle&lt;/code&gt; is clamped between 0 and 90 degrees, and when it is 0, the grass is upright, unaffected by any entity. Also, the function that gives &lt;code&gt;initialAngle&lt;/code&gt; is a strictly decreasing function between 0 and entity radius, and for a given entity, it only depends on the distance between the entity and the pixel, therefore we could safely use it to estimate the distance:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float dist = initialAngle/HALF_PI;  
trampleStr = saturate(saturate(str) *  saturate(1 - dist) * trample * (1 - saturate(t)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OK there&apos;s actually a lot more going on here.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;str&lt;/code&gt;: this is the same as entity elevation&apos;s impact. If the entity is high-up then we do not apply any trample.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 - dist&lt;/code&gt;: distance between the entity and the grass blade.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trample&lt;/code&gt;: exposed constant for fine tuning.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 - saturate(t)&lt;/code&gt;: reduce trample over time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The added lines produce a result like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/grass-trample.webp&quot; alt=&quot;alt text{caption=trample effect}&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Rotation and Scale&lt;/h2&gt;
&lt;p&gt;One final problem we need to solve is that we have ignored the grass blade&apos;s transform rotation. For example, this is when the grass has 180 degrees rotation around the y axis:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/grass-rotation.webp&quot; alt=&quot;alt text{caption=rotation and scale problem}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It is defying the rules of physics by leaning &lt;em&gt;towards&lt;/em&gt; the entity.&lt;/p&gt;
&lt;p&gt;The solution is simple yet slightly weird in shader graph: multiplying the final vertex position by a rotation matrix. This rotation matrix is part of the &lt;em&gt;model matrix&lt;/em&gt; of the grass blade, and we will take the upper-left 3x3 of it, which actually gives rotation and scale transformation. After that, we divide the result by the object&apos;s scale or else the scale is applied twice:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-4/grass-rotation-sg.webp&quot; alt=&quot;alt text{caption=rotation and scale fix wire up}&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Recap&lt;/h2&gt;
&lt;p&gt;This should provide a good enough base for everything. Feel free to adjust any of the calculations.&lt;/p&gt;
&lt;p&gt;Here&apos;s the vertex displacement function so far:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;Pendulum.hlsl&quot;  
#define HALF_PI 1.5707963  
  
void PlantVertexDisplace_float(  
    in float3 osPos,  
    in float3 wsPos,  
    in float4 osParams,  
    in float dampingCoef,  
    in float elasticity,  
    in float4 influenceTex,  
    in float trample,  
    out float3 osDisplacement,  
    out float trampleStr  
)  
{  
    half angle = 0;  
    half initialAngle = length(influenceTex.xy);  
    half t = influenceTex.w;  
    half pendulumLen = osParams.y - osParams.x;  
    #if _PENDULUM_CRITICALLY_DAMPED  
    angle = pendulum_critically_damped(initialAngle, t, elasticity, pendulumLen, dampingCoef);    
    #elif _PENDULUM_OVERDAMPED    
    angle = pendulum_overdamped(initialAngle, t, elasticity, pendulumLen, dampingCoef);    
    #elif _PENDULUM_UNDERDAMPED    
    angle = pendulum_underdamped(initialAngle, t, elasticity, pendulumLen, dampingCoef);  
    #endif   
    osDisplacement = 0;  
    trampleStr = 0;  
    osPos.y -= osParams.x;  
    half2 dir = initialAngle &amp;gt; 0.0001 ? influenceTex.xy/initialAngle : half2(0,0);  
    float entityElevation = influenceTex.z;  
    // determine press strength (reduce strength when the object is above the grass)  
    float rawStr = 1 - (entityElevation - wsPos.y)/max(0.1,osParams.y - osParams.x);  
    float str = smoothstep(0.5, 1, rawStr);  
      
    float3 directionVect = normalize(float3(dir.x, 0.001, dir.y));  
    float3 rotationAxis = cross(directionVect, float3(0,1,0));  
    rotationAxis.y = max(rotationAxis.y, 0.001);  
    rotationAxis = normalize(rotationAxis);  
    float3 planarDisplacement = float3(osPos.x, 0, osPos.z);  
    float3 grassBlade = osPos - planarDisplacement;  
    /* Instead of lateral displacement, we use rotation.  
     * Rotation can better preserve the shape of the grass.     */    // Rodrigue rotation    float3 afterRot = grassBlade * cos(angle) + cross(rotationAxis, grassBlade)* sin(angle) + rotationAxis*dot(rotationAxis,grassBlade)*(1-cos(angle));  
  
    osDisplacement = (grassBlade - afterRot);  
    osDisplacement *= str;  
      
    float dist = initialAngle/HALF_PI;  
    trampleStr = saturate(saturate(str) *  saturate(1 - dist) * trample * (1 - saturate(t)));  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the next and final part we will address some other issue and performance concerns.&lt;/p&gt;
</content:encoded></item><item><title>Grass Interaction - Part 3</title><link>https://fukafukaseika.moe/posts/grass-interaction-3/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/grass-interaction-3/</guid><description>Learn how to create physically based interactive grass in Unity. In this third part of the series, we will setup the Custom Pass that drives the Entity Influence system, sending non-grass specific data to the GPU and see the influence texture update in action.</description><pubDate>Thu, 16 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this third part, we will set up the &lt;em&gt;Entity Influence&lt;/em&gt; system, calculating non-grass-specific data required by the motion equation.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: All &lt;code&gt;using&lt;/code&gt; declarations are hidden for clarity. You should be able to auto-complete them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Entities and Entity Influence Registry&lt;/h2&gt;
&lt;p&gt;We start by defining the interface of our entities:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface IEntityInfluenceInfo{
	public Vector3 WorldPos {get;}
	public float Radius {get;}
	public Vector3 WorldVel {get;}
	public float Height {get;}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C# doesn&apos;t have multiple inheritance, thus using interface is the most flexible solution. The interface matches closely what we have defined in the compute shader &lt;code&gt;InfluenceInfo&lt;/code&gt;. However, since we need to push the data into a buffer, and an interface is &lt;em&gt;not blittable&lt;/em&gt;, we need a &lt;code&gt;struct&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public struct EntityInfluenceInfo  
{  
    public Vector3 worldPos;  
    public float radius;  
    public Vector3 worldVel;  
    public float height;  
    public void CopyFrom(IEntityInfluenceInfo info)  
    {  
        worldPos = info.WorldPos;  
        worldVel = info.WorldVel;  
        radius = info.Radius;  
        height = info.Height;  
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: The order of the fields in &lt;code&gt;EntityInfluenceInfo&lt;/code&gt; must match &lt;code&gt;InfluenceInfo&lt;/code&gt; declared in HLSL. When transmitting data from CPU to GPU, there&apos;s no way to remap fields as the data is essentially &lt;em&gt;raw&lt;/em&gt; (&lt;code&gt;void*&lt;/code&gt;). Furthermore, the computer does not give you any error message when you get the order wrong except when the ordering changed the size of the struct.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This would seem redundant, having both an interface and a struct, why not just having a struct and store it as a member field? The reason is we want our system to take care of data updating, instead of letting the containing class to call update per-frame (if we only retain &lt;code&gt;EntityInfluenceInfo&lt;/code&gt; struct, the containing class will also need to set its values per-frame). This would make the API more friendly.&lt;/p&gt;
&lt;p&gt;Next, we will create a class that gathers all the info. For simplicity, we will make it a &lt;em&gt;singleton&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class EntityInfluenceRegistry  
{  
	private EntityInfluenceRegistry(){}
	private static EntityInfluenceRegistry _instance;  
	public static EntityInfluenceRegistry Instance  
	{  
		get  
		{  
			if (_instance == null)  
		    {  
		        _instance = new EntityInfluenceRegistry();  
			}  
			return _instance;  
		}  
	}
	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need a way to let other components to register entity influence info, as well as removal. Therefore, let&apos;s first add an &lt;code&gt;Add&lt;/code&gt; and a &lt;code&gt;Remove&lt;/code&gt; method. We are also tracking &quot;large entities&quot; so that we can determine the transcription method.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private List&amp;lt;IEntityInfluenceInfo&amp;gt; _influenceInfos = new();
private const float PerfThreshold = 2;  
private int _largeEntityCount = 0;

public int Size =&amp;gt; _influenceInfos.Count;

public void Add(IEntityInfluenceInfo info)  
{  
	_influenceInfos.Add(info);  
	if(info.Radius &amp;gt; PerfThreshold) _largeEntityCount++;  
}  
  
public void Remove(IEntityInfluenceInfo info)  
{  
	_influenceInfos.Remove(info);  
	if(info.Radius &amp;gt; PerfThreshold) _largeEntityCount--;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: For simplicity, the &lt;code&gt;PerfThreshold&lt;/code&gt; is a constant field, but you could definitely expose it as an adjustable field in the inspector.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Since we are here, let&apos;s solve the transcription method first. Create an enum &lt;code&gt;EIRTranscribeMethod&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public enum EIRTranscribeMethod  
{  
    /// small entities, large quantity  
    Method1,  
    /// large entities, small quantity  
    Method2  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: &quot;EIR&quot; is short for &quot;Entity Influence Recording&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then, if we have any entity that is considered &quot;large&quot;, we will use the second method. However, it is totally possible to have one large entity and hundreds of small entities, then both methods would fall short. A possible way to solve it is to split large and small entities to their dedicated buffer, and use the appropriate methods to transcribe them in separated compute passes. The implementation of such approach is left to you. For now, we just naively check if there&apos;s any large entity:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public EIRTranscribeMethod Method =&amp;gt; _largeEntityCount &amp;gt; 0 ? EIRTranscribeMethod.Method2 : EIRTranscribeMethod.Method1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We now need a &lt;code&gt;GraphicsBuffer&lt;/code&gt; to transfer the data collected. Unlike a C# &lt;code&gt;List&lt;/code&gt;, &lt;code&gt;GraphicsBuffer&lt;/code&gt; must have a predefined size. We will use 512 elements for now, but if you do actually have more than 512 entities, you would want to increase that.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private GraphicsBuffer _influenceBuffer;  
private EntityInfluenceInfo[] _stagingBuffer = new EntityInfluenceInfo[512];
private int InfluenceInfoSize =&amp;gt; sizeof(float) * 8;

public GraphicsBuffer InfluenceInfo  
{  
	get  
	{  
		if (_influenceBuffer == null || !_influenceBuffer.IsValid())  
		{  
			_influenceBuffer?.Dispose();  
		    _influenceBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, GraphicsBuffer.UsageFlags.None, _infos.Length, InfluenceInfoSize);  
		}  
		return _influenceBuffer;  
	}  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: You may notice there are 3 copies of the same data: one in &lt;code&gt;List&lt;/code&gt;, one in &lt;code&gt;EntityInfluenceInfo[]&lt;/code&gt;, and one in &lt;code&gt;GraphicsBuffer&lt;/code&gt;. The &lt;code&gt;GraphicsBuffer&lt;/code&gt; resides on the GPU hence is different from the other two that are on the CPU side. The &lt;code&gt;EntityInfluenceInfo[]&lt;/code&gt; is like a &lt;em&gt;staging buffer&lt;/em&gt;, which contains raw data (values are kept), and the &lt;code&gt;List&lt;/code&gt; contains the &lt;em&gt;providers&lt;/em&gt; of the raw data (therefore only references are kept).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Finally, we add an &lt;code&gt;Update&lt;/code&gt; method to copy data from the list to the staging buffer, and bind it to the graphics buffer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void Update()  
{  
	for (int i = 0; i &amp;lt; _influenceInfos.Count; ++i)  
	{  
		_stagingBuffer[i].CopyFrom(_influenceInfos[i]);  
	}  
	InfluenceBuffer.SetData(_stagingBuffer);  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We will add a test script called &lt;code&gt;EntityInfluenceProvider&lt;/code&gt;. This script has a velocity field that moves the object to the right, and whenever it reaches 4096 it resets its horizontal position to 0.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ExecuteAlways]  
public class EntityInfluenceProvider : MonoBehaviour, IEntityInfluenceInfo  
{  
    public float radius = 5;  
    public float height = 1;  
    public float vel = 10;  
    private Vector3 _lastPos = Vector3.zero;  
    private Vector3 _velocity = Vector3.zero;  
    private void OnEnable()  
    {  
        EntityInfluenceRegistry.Instance.Add(this);  
    }  
  
    private void OnDisable()  
    {  
        EntityInfluenceRegistry.Instance.Remove(this);  
    }  
  
    private void Update()  
    {  
        _velocity = (transform.position - _lastPos) / Time.deltaTime;  
        _lastPos = transform.position;  
        transform.position += Vector3.right * (Time.deltaTime * vel);  
        if (transform.position.x &amp;gt; 4096)  
        {  
            var vector3 = transform.position;  
            vector3.x = 0;  
            transform.position = vector3;  
        }  
    }  
    public Vector3 WorldPos =&amp;gt; transform.position;  
    public Vector3 WorldVel =&amp;gt; _velocity;  
    public float Radius =&amp;gt; radius;  
    public float Height =&amp;gt; height;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Custom Pass&lt;/h2&gt;
&lt;p&gt;This is the part where HDRP and URP part ways - they offer different API for doing this. Though in a nutshell, what we will be doing is setup some compute dispatch calls, and it should be fairly easy to do in either pipelines.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class EntityInfluenceRecordingPass : CustomPass  
{  
    // Shader parameters 
    private static readonly int InfluenceTexture = Shader.PropertyToID(&quot;_InfluenceTexture&quot;);  
    private static readonly int InfluenceTextureParams = Shader.PropertyToID(&quot;_InfluenceTextureParams&quot;);  
    private static readonly int InfluenceInfo = Shader.PropertyToID(&quot;_InfluenceInfo&quot;);  
    private static readonly int InfluenceInfoCount = Shader.PropertyToID(&quot;_InfluenceInfoCount&quot;);  
    private static readonly int Time1 = Shader.PropertyToID(&quot;_TimeParams&quot;);  
    
    // Serialized properties
    public ComputeShader scriptoriumShader;  
    public float coverageSize;  
    public Vector2 offset;  
    public int textureSize;  
      
    // Under the hood stuff
    private RenderTexture _influenceTexture;  
    private Vector4 _influenceTextureParams;  
    private bool _initSucceeded = false;
    // Kernels  
    private int _updateTimeKernel;  
    private int _transcribeKernel1;  
    private int _transcribeKernel2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we will initialize all the required resources in the &lt;code&gt;Setup&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        protected override void Setup(ScriptableRenderContext renderContext, CommandBuffer cmd)  
        {  
            // sanity check
            Assert.IsNotNull(scriptoriumShader, &quot;EIR: ScriptoriumShader is null&quot;);  
            Assert.IsTrue(coverageSize != 0, &quot;EIR: CoverageSize is 0&quot;);  
            Assert.IsTrue(textureSize != 0, &quot;EIR: TextureSize is 0&quot;);  
            base.Setup(renderContext, cmd);  
            // texture creation
            var descriptor = new RenderTextureDescriptor(textureSize, textureSize)  
            {  
                graphicsFormat = GraphicsFormat.R16G16B16A16_SFloat,  
                depthBufferBits = 0,  
                depthStencilFormat = GraphicsFormat.None,  
                enableRandomWrite = true,  
            };  
  
            _influenceTexture = new RenderTexture(descriptor);  
            _influenceTexture.Create();
            // finding kernels
            _updateTimeKernel = scriptoriumShader.FindKernel(&quot;UpdateTime&quot;);  
            _transcribeKernel1 = scriptoriumShader.FindKernel(&quot;TranscribeMethod1&quot;);  
            _transcribeKernel2 = scriptoriumShader.FindKernel(&quot;TranscribeMethod2&quot;);  
  
            // set params only once in build to reduce overhead  
#if !UNITY_EDITOR  
            SetParams(cmd);  
#endif  
            _initSucceeded = true;  
        }  
  
        private void SetKernelParams(CommandBuffer cmd, int kernelID)  
        {  
            cmd.SetComputeBufferParam(scriptoriumShader,kernelID, InfluenceInfo, EntityInfluenceRegistry.Instance.InfluenceBuffer);  
            cmd.SetComputeTextureParam(scriptoriumShader, kernelID, InfluenceTexture, _influenceTexture);  
        }  
  
        private void SetParams(CommandBuffer cmd)  
        {  
            _influenceTextureParams = new Vector4(coverageSize / textureSize, textureSize, offset.x, offset.y);  
            cmd.SetGlobalTexture(InfluenceTexture, _influenceTexture);  
            SetKernelParams(cmd, _transcribeKernel1);  
            SetKernelParams(cmd, _transcribeKernel2);  
            SetKernelParams(cmd, _updateTimeKernel);  
            cmd.SetComputeFloatParam(scriptoriumShader, LerpFactor, lerpFactor);  
            cmd.SetGlobalVector(InfluenceTextureParams, _influenceTextureParams);  
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: We use &lt;code&gt;CommandBuffer.Set...&lt;/code&gt; methods because they generally have less overhead compared to &lt;code&gt;Shader.Set...&lt;/code&gt; methods. The exact implementation is hidden from us, but it is possible that &lt;code&gt;Shader.Set...&lt;/code&gt; methods need to create onetime-submit command buffers. Benchmarking showed &lt;code&gt;CommandBuffer.Set...&lt;/code&gt; methods are 1.5x-2x faster than equivalent &lt;code&gt;Shader.Set...&lt;/code&gt; methods.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: Resource bindings only need to be set once. This is why I put &lt;code&gt;SetParams&lt;/code&gt; call in &lt;code&gt;Setup&lt;/code&gt; rather than &lt;code&gt;Execute&lt;/code&gt;, since they won&apos;t change in the built game. In Editor it is called per-frame since we might want to adjust constants in real-time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And now in &lt;code&gt;Execute&lt;/code&gt;, we trigger the update on &lt;code&gt;EntityInfluenceRegistry&lt;/code&gt; and dispatch the two computes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        protected override void Execute(CustomPassContext ctx)  
        {  
            if (!_initSucceeded) return;  
            base.Execute(ctx);  

			// trigger registry update
            EntityInfluenceRegistry.Instance.Update();  
            // set influence info count (array bound)
            scriptoriumShader.SetInt(InfluenceInfoCount, EntityInfluenceRegistry.Instance.Size);  
            // update time
            scriptoriumShader.SetVector(Time1, new Vector4(Time.deltaTime, 0, 0, 0));  
            var cmd = ctx.cmd;  
            // In Editor mode, set params every frame to support param adjustments.  
#if UNITY_EDITOR  
            SetParams(cmd);  
#endif  
            cmd.BeginSample(&quot;EntityInfluence Time Update&quot;);  
            cmd.DispatchCompute(scriptoriumShader, _updateTimeKernel,  
                textureSize / 8, textureSize / 8, 1);  
            cmd.EndSample(&quot;EntityInfluence Time Update&quot;); 
             
            cmd.BeginSample(&quot;EntityInfluence Transcribe&quot;);  
            cmd.DispatchCompute(scriptoriumShader,  
                EntityInfluenceRegistry.Instance.Method == EIRTranscribeMethod.Method1  
                    ? _transcribeKernel1  
                    : _transcribeKernel2,  
                textureSize / 8, textureSize / 8, 1);  
            cmd.EndSample(&quot;EntityInfluence Transcribe&quot;);  
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally in &lt;code&gt;Cleanup&lt;/code&gt;, release the texture:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected override void Cleanup()  
{  
    _influenceTexture.Release();  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That concludes the custom pass. Create a Custom Pass Volume in the Editor and hook up the EIR pass:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-3/EIR-pass.webp&quot; alt=&quot;alt text{caption=EIR pass}&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Debugging&lt;/h2&gt;
&lt;p&gt;Let&apos;s setup a simple shader to visualize our influence texture:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-3/EIR-visualizer.webp&quot; alt=&quot;alt text{caption=EIR visualizer}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It samples the global texture &lt;code&gt;_InfluenceTexture&lt;/code&gt;, and that&apos;s it. Remember to set the scope of the influence texture parameter to &lt;code&gt;Global&lt;/code&gt; or else it won&apos;t work.&lt;/p&gt;
&lt;p&gt;Create a material from this shader, add an UI Image and assign the image with the material created.&lt;/p&gt;
&lt;p&gt;Now create an empty object and add an &lt;code&gt;EntityInfluenceProvider&lt;/code&gt; - the test script we created earlier. You should see a circle in the UI image:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-3/EIR-visualizer-result-1.webp&quot; alt=&quot;alt text{caption=EIR result 1}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Here I set the radius to 50 so that it could be seen clearly. If the velocity of the object is not zero then you should see it leaving a trail behind:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-3/EIR-visualizer-result-2.webp&quot; alt=&quot;alt text{caption=EIR result 2}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Even if you disable the object, the trail should stay. You could modify the visualizer so that we take time into account:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-3/EIR-visualizer-alpha-aware.webp&quot; alt=&quot;alt text{caption=EIR visualizer with alpha}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Play around with the setup and check if everything work as expected so far.&lt;/p&gt;
&lt;p&gt;In the next part we will finish the final piece of the puzzle - vertex displacement in shader.&lt;/p&gt;
</content:encoded></item><item><title>Grass Interaction - Part 2</title><link>https://fukafukaseika.moe/posts/grass-interaction-2/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/grass-interaction-2/</guid><description>Learn how to create physically based interactive grass in Unity. In this second part of the series, we will set up the shader part of the Entity Influence system, designing the data layout of the influence texture, and how this texture is updated.</description><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In the second part, we are going to set up the shader part of the &lt;em&gt;Entity Influence&lt;/em&gt; system. In this system, we will pre-compute some of the required parameters so that the vertex shader can later read them and use the appropriate motion equation to calculate the angle.&lt;/p&gt;
&lt;p&gt;Since we might have different grass that react differently based on grass-specific parameters, the design philosophy is to separate entity influence and grass - we do not want to see any grass-specific constants in the entity influence system.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: This part expects you to know how &lt;em&gt;Compute Shaders&lt;/em&gt; work, and some knowledge of the render pipeline you are working with. You should at least know how to create a &lt;code&gt;CustomPass&lt;/code&gt; or &lt;code&gt;ScriptableRenderPass&lt;/code&gt; in your render pipeline. We will work in &lt;em&gt;HDRP&lt;/em&gt; and use &lt;code&gt;CustomPass&lt;/code&gt; here. I will try my best to abstract away the render pipeline specific features (which also future-proofs API changes). Feel free to make necessary adjustments according to the API you are given.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Overview of the Entity Influence System&lt;/h2&gt;
&lt;p&gt;Each entity will have an &lt;code&gt;EntityInfoProvider&lt;/code&gt; attached to them. The script will inform &lt;code&gt;EntityInfoRegistry&lt;/code&gt; about the current position, radius, height, etc. of the entity.&lt;/p&gt;
&lt;p&gt;Each frame, &lt;code&gt;EntityInfoRegistry&lt;/code&gt; packs these infos into a &lt;code&gt;GraphicsBuffer&lt;/code&gt;. A pass will bind this buffer to a compute shader as well as other globally tweakable parameters, and dispatch the compute shader. The compute shader will calculate the influence of each entity, and write the results to &lt;em&gt;Entity Influence Texture(s)&lt;/em&gt;, which will later be used in vertex shaders.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-2/EIR-sequence-diagram.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We are using textures because we have multiple entities that may influence grass. Sampling texture is going to be better than iterating through each entity in vertex shader. If you only have a very limited amount of entities, then it is possible to pass the buffer directly to the vertex shader and do all the calculations there - at the cost of vertex shader performance.&lt;/p&gt;
&lt;p&gt;We defer part of the calculation to a compute shader because that provides us a way to harness the power of &lt;em&gt;Async Compute&lt;/em&gt;. If we were to do everything in grass&apos; vertex shader, then everything will be done on Graphics queue. Also, even if we don&apos;t use async compute, as the compute shader really doesn&apos;t depend on anything, the driver can schedule it more effectively. The drawback is that we need texture to store intermediate results, therefore it isn&apos;t an obvious gain, but a &lt;em&gt;memory-speed trade-off&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Inputs and Outputs&lt;/h2&gt;
&lt;p&gt;Let&apos;s review the theories, and determine what data do we need to establish the systems.&lt;/p&gt;
&lt;p&gt;$$\theta_0(d) = \frac{\pi}{2}-arctan(\frac{2h_0d}{R_0^2-d^2})$$&lt;/p&gt;
&lt;p&gt;For initial angle calculation, we need the constant $c$ and distance $d$. For $c$, we need the height $h_0$ and radius $R_0$ of the instigator, which are simply 2 tweakable floats.&lt;/p&gt;
&lt;p&gt;As for $d$, by definition, is the distance between the center of the instigator (therefore, instigator&apos;s transform position), and the root of the grass. Now, the root of the grass may seem like a grass-specific data, but we could simply convert pixel position to world position. Though if we do so, the resolution of the texture will determine the accuracy of our simulation.&lt;/p&gt;
&lt;p&gt;As for the motion equation, we take the &lt;em&gt;underdamped&lt;/em&gt; case as an example:&lt;/p&gt;
&lt;p&gt;$$\theta(t)=\theta_0*e^{-\gamma t}cos(\omega t)$$&lt;/p&gt;
&lt;p&gt;With:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\gamma=\frac{b}{2*l_0}$&lt;/li&gt;
&lt;li&gt;$\omega_0 = \sqrt{\frac{g-k}{l_0}}$&lt;/li&gt;
&lt;li&gt;$\omega = \sqrt{abs(\gamma^2-\omega_0^2)}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We need grass length $l_0$, elasticity constant $k$, damping coefficient $b$, and elapsed time $t$. The length, elasticity constant, and damping coefficient are all grass-specific, so we only need to store elapsed time. The definition of elapsed time, in our case, is the time when grass leaves the influence of an entity. There are 2 ways to store this elapsed time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store the timestamp &lt;code&gt;t&lt;/code&gt; when the grass is last in an entity&apos;s influence, then in the vertex shader, we can calculate the elapsed time with &lt;code&gt;_Time.y - t&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Store the elapsed time &lt;code&gt;dt&lt;/code&gt; directly. Each frame we increase the elapsed time &lt;code&gt;dt&lt;/code&gt; by &lt;code&gt;unity_DeltaTime.x&lt;/code&gt;, and set &lt;code&gt;dt = 0&lt;/code&gt; if the grass is in any entity&apos;s influence.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To avoid potential overflow, we will adopt the second approach.&lt;/p&gt;
&lt;p&gt;Finally, calculating the angle is not enough. We still need to know in which direction should the grass bend. That means we need a &lt;code&gt;Vector2&lt;/code&gt; to represent the direction. We also need the entity&apos;s bottom surface elevation, because if an entity is above the grass, then it should exert no influence on the grass. (remember the height of the grass is only available in vertex shader, so we can&apos;t pre-calculate it!)&lt;/p&gt;
&lt;p&gt;To summarize, this will be the info that we must produce in our Entity Influence System:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Direction of the influence (&lt;code&gt;Vector2&lt;/code&gt;, or 2 &lt;code&gt;float&lt;/code&gt;s)&lt;/li&gt;
&lt;li&gt;Elevation of the bottom surface of the entity (&lt;code&gt;float&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Initial angle (&lt;code&gt;float&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Elapsed time (&lt;code&gt;float&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;&quot;Blast! That&apos;s 5 floats in total! That means we need 2 textures to fit them all in!&quot;&lt;/em&gt; - Very perceptive! But here&apos;s a trick we can do... see, when we need the direction, we do not need the &lt;em&gt;magnitude&lt;/em&gt; of the &lt;code&gt;Vector2&lt;/code&gt;. Later in the vertex shader, we would still call &lt;code&gt;normalize&lt;/code&gt; on it. That means the magnitude of the vector can store another float... let it be the initial angle then!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: The choice of storing initial angle in magnitude is actually deliberate. The bottom surface elevation can be negative, so we can&apos;t store it in magnitude. Elapsed time is positive, but if we store it in the magnitude, whenever we update influence, since time is reset to 0, it will set the direction to 0 as well. The initial angle does not have this side effect. Therefore, the value that is the most suitable for storing in magnitude is the initial angle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Writing the Compute Shader&lt;/h2&gt;
&lt;p&gt;Let&apos;s start by declaring the inputs and outputs.&lt;/p&gt;
&lt;h3&gt;Influence Info&lt;/h3&gt;
&lt;p&gt;The buffer containing entity influence info will have 2 &lt;code&gt;float3&lt;/code&gt; and 2 &lt;code&gt;float&lt;/code&gt;. We pack them into a &lt;code&gt;InfluenceInfo&lt;/code&gt; struct and use a &lt;code&gt;StructuredBuffer&lt;/code&gt; to communicate between CPU and GPU. The &lt;code&gt;_InfluenceInfoCount&lt;/code&gt; tracks the effective length of the buffer, and is equal to the number of entities registered. We will be allocating a large enough buffer for &lt;code&gt;_InfluenceInfo&lt;/code&gt; (e.g. 256 elements), and use the count to delimit valid data range.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct InfluenceInfo  
{  
    float3 worldPosition;  
    float radius;  
    float3 worldVelocity;  
    float height;  
};  
  
// raw influence info data  
StructuredBuffer&amp;lt;InfluenceInfo&amp;gt; _InfluenceInfo;

// valid length of influence info  
int _InfluenceInfoCount;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: The order of the fields in &lt;code&gt;InfluenceInfo&lt;/code&gt; is important due to &lt;em&gt;alignment requirements&lt;/em&gt;. In GPU shaders, buffers must adhere to specific alignment rules, which means types like &lt;code&gt;float3&lt;/code&gt; may get padded to ensure they fit properly in memory. If you don&apos;t account for this, you may end up allocating more memory than necessary.&lt;/p&gt;
&lt;p&gt;The struct above, with consecutive &lt;code&gt;float3&lt;/code&gt; and &lt;code&gt;float&lt;/code&gt; packed into one 16-byte block, is equivalent to 32 bytes in total. However, if you order your struct like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct InfluenceInfo  
{  
  float3 worldPosition;  
  float3 worldVelocity;  
  float radius;  
  float height;  
}; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It will take up &lt;strong&gt;48 bytes&lt;/strong&gt; (3 &lt;code&gt;float4&lt;/code&gt;), because the first &lt;code&gt;float3&lt;/code&gt; cannot be packed with the second &lt;code&gt;float3&lt;/code&gt; due to alignment rules, and as a result, it gets its own 16-byte block, with 4 bytes unused:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first &lt;code&gt;float3&lt;/code&gt; occupies 16 bytes, but the remaining 4 bytes are unused (due to padding).&lt;/li&gt;
&lt;li&gt;The second &lt;code&gt;float3&lt;/code&gt; and &lt;code&gt;float&lt;/code&gt; fields fit into the second 16-byte block without wasting any space.&lt;/li&gt;
&lt;li&gt;The final &lt;code&gt;float&lt;/code&gt; gets its own 16-byte block, with 12 bytes wasted.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As a general rule, it&apos;s good practice to ensure that fields in your struct are ordered in such a way that they can fit neatly into 16-byte blocks without any fields &quot;sitting between two blocks.&quot; This will help minimize wasted memory and ensure more efficient data storage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Texture and Other Constants&lt;/h3&gt;
&lt;p&gt;We will be using a texture of format &lt;code&gt;R16G16B16A16_SFloat&lt;/code&gt; which means 16-bit (&lt;code&gt;half&lt;/code&gt;) per channel. Our calculation doesn&apos;t need to be extremely precise so &lt;code&gt;half&lt;/code&gt; is enough. You could eventually use integer formats but then you need to write conversion code. For simplicity, we will stick with the float format.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RWTexture2D&amp;lt;half4&amp;gt; _InfluenceTexture;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The layout of a pixel looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;R&lt;/strong&gt;: direction vector x component&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;G&lt;/strong&gt;: direction vector y component&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B&lt;/strong&gt;: the elevation of the bottom surface of the object&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A&lt;/strong&gt;: elapsed time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, don&apos;t forget that &lt;strong&gt;the magnitude of the direction vector is the initial angle&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;Since we will be doing some conversion between pixels and world space, we need to know how much space does the texture cover, its world space offset, as well as the texture&apos;s size. We will pack all these info into a &lt;code&gt;float4&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; /**    
 * x: 1 px = x units in world space (world size/texture size)  
 * y: texture px size (square texture) 
 * z: WS origin.x (origin is the lower left corner) 
 * w: WS origin.y 
 */
 float4 _InfluenceTextureParams;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally we add the &lt;code&gt;_TimeParams&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  
 /**  
 * x: Time.deltaTime 
 */
 float4 _TimeParams;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s all of the ins and outs of the compute shader. Your shader should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct InfluenceInfo  
{  
    float3 worldPosition;  
    float radius;  
    float3 worldVelocity;  
    float enttConstant;  
};  
  
// raw influence info data  
StructuredBuffer&amp;lt;InfluenceInfo&amp;gt; _InfluenceInfo;  
  
// valid length of influence info  
int _InfluenceInfoCount;  
  
/** Texture to transcribe influence info to.  
 * Data layout of the influence texture *  
 * R:  N.x  \  initial angle
 * G:  N.y  /   
 * B:  bottom surface elevation
 * A:  elapsed time since release 
 */
 RWTexture2D&amp;lt;half4&amp;gt; _InfluenceTexture;  
  
/**    
 * x: 1 px = x units in world space (world size/texture size)  
 * y: texture px size (square texture) 
 * z: WS origin.x (origin is the lower left corner) 
 * w: WS origin.y 
 */
float4 _InfluenceTextureParams;  
  
/**  
 * x: Time.deltaTime 
 */
 float4 _TimeParams;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Time Update&lt;/h3&gt;
&lt;p&gt;Let&apos;s start with the easy one. As said before, we need to update the elapsed time each frame. This is simply adding delta time to the time channel of the influence texture, nothing fancy.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#pragma kernel UpdateTime

[numthreads(8,8,1)]  
void UpdateTime(uint3 id: SV_DispatchThreadID)  
{  
    uint2 px = id.xy;  
    // ensure the pixel is within bounds
    if (px.x &amp;gt;= _InfluenceTextureParams.y || px.y &amp;gt;= _InfluenceTextureParams.y) return;  
    half4 pxData = _InfluenceTexture[px];  
    half t = pxData.w;  
    t += _TimeParams.x;  
    _InfluenceTexture[px] = half4(pxData.xyz, t);  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are essentially updating every pixel here, regardless of whether the pixel is in an entity&apos;s influence. This is because we will later transcribe entity influence and that will overwrite relevant pixel&apos;s time.&lt;/p&gt;
&lt;h3&gt;Entity Influence Transcription&lt;/h3&gt;
&lt;p&gt;Now we need to take care of the pixels that fall into an entity&apos;s influence.&lt;/p&gt;
&lt;p&gt;We will implement 2 different methods for Entity Influence Transcription, each with some performance concerns and we need to switch from one to the other depending on the entities.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Iterate Per Entity&lt;/strong&gt;: For each compute thread we deal with one entity, calculate the position for every pixel that fall under the influence of that entity, and update them. This method is better when &lt;em&gt;entities are small but numerous&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iterate Per Pixel&lt;/strong&gt;: For each compute thread we deal with one pixel, and we iterate through all the entities. If we find that the pixel falls in the influence of any of them, we update the pixel with corresponding calculations. This method is better when &lt;em&gt;entities are large but not numerous&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-2/transcribe-methods.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The size of the entity depends on its &lt;em&gt;pixel coverage&lt;/em&gt;. An entity having a radius 5 units on a texture with 1 px = 1 unit is equivalent to an entity having a radius 1 on a texture with 1 px = 0.2 unit.&lt;/p&gt;
&lt;h4&gt;Texture Update Logic&lt;/h4&gt;
&lt;p&gt;Let&apos;s tackle the part in common first: &lt;strong&gt;Given a pixel and an entity, does the pixel falls into the entity&apos;s influence?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The logic is simple, since pixel is in texture space, and entity position is in world space, we need to compare them in the same space.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If we compare in &lt;em&gt;pixel&lt;/em&gt;, we need to convert entity&apos;s position and its radius to pixel.&lt;/li&gt;
&lt;li&gt;If we compare in &lt;em&gt;world units&lt;/em&gt;, we need to convert pixel&apos;s position to world space.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The latter requires one less conversion so we will compare them in world units:&lt;/p&gt;
&lt;p&gt;$$PixelWorldPos = TextureOriginWS + PixelCoord*PixelToWorldRatio$$&lt;/p&gt;
&lt;p&gt;And if the pixel is in the entity&apos;s influence, then its distance to the entity must be smaller than the entity&apos;s radius:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;half2 PixelToWorldPos(in uint2 px){
	half ratio = _InfluenceTextureParams.x;
    half offset = _InfluenceTextureParams.zw;
	return offset + px * ratio;
}

bool IsPixelInInfluence(in half2 pxWorldPos, in InfluenceInfo entity){
	return distance(pxWorldPos , entity.worldPosition.xz) &amp;lt; entity.radius;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We separate the conversion and the check because later we will also need to use the pixel&apos;s world position. We could convert once and reuse the result in other function calls.&lt;/p&gt;
&lt;p&gt;The next question is, &lt;strong&gt;if we found a pixel in an entity&apos;s influence, how do we update its color values?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;A&lt;/strong&gt; channel, we need to reset the elapsed time to 0 as the entity&apos;s influence will cause the grass to be in a static state.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;RG&lt;/strong&gt; (&lt;strong&gt;normalized direction&lt;/strong&gt;), we calculate the displacement from the entity&apos;s center to the pixel, in world space.&lt;/li&gt;
&lt;li&gt;For the  &lt;strong&gt;B&lt;/strong&gt; channel, we calculate via: $$elevation = entityY-h_0+h(d)$$ for the bottom surface elevation.&lt;/li&gt;
&lt;li&gt;For the &lt;strong&gt;magnitude of RG&lt;/strong&gt; (initial angle), we will simply apply the formula we saw in part 1 of the series to calculate the initial angle.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#define HALF_PI 1.5707963
  
half EllipticElevation(half x2, half a2, half b)  
{  
    return b *( sqrt(max(0,1 - x2 / a2 )) + 1 );  
}  

void UpdateInfluence(in uint2 px, in half2 pxWorldPos, in InfluenceInfo entity){  
    
    half2 displacement = pxWorldPos - entity.worldPosition.xz;  
    half d = length(displacement);  
    half angle = length(originalData.xy);  
      
    displacement = SafeNormalize(originalData.xy);
      
    half R2 = (entity.radius * entity.radius);  
    half h = entity.height;  
    half d2 = d * d;  
    
    // calculate initial angle  
    half initialAngle = HALF_PI - atan( 2 * entity.height * d / max( R2 - d2, 0.00001);
                            
    // calculate elevation  
    half entityElevation = entity.worldPosition.y - EllipticElevation(d2, R2, h);   
    _InfluenceTexture[px] = half4(displacement * max(0.01,initialAngle), entityElevation, 0);  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You might notice that the velocity of the entity is not used. We will be using it in a later chapter to adjust the simulation for moving entities.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: Strictly speaking, setting time to 0 will actually cause grass blade to &quot;teleport&quot; to the target position. You could try to lerp the time to 0, essentially &quot;reverse&quot; the motion. But that would look particularly weird when the grass is &lt;em&gt;underdamped&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And that&apos;s it! We will now use them in the actual kernels.&lt;/p&gt;
&lt;h4&gt;Transcription Method 1: Iterate Per Entity&lt;/h4&gt;
&lt;p&gt;In this method, each thread will process an entity. We are using &lt;code&gt;SV_GroupIndex&lt;/code&gt; as it directly gives the index in &lt;code&gt;_InfluenceInfo&lt;/code&gt; buffer. Let&apos;s start by writing some boilerplate code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#pragma kernel TranscribeMethod1

[numthreads(8,8,1)]  
void TranscribeMethod1(uint id: SV_GroupIndex)  
{  
    if (id &amp;gt;= _InfluenceInfoCount) return;  
    InfluenceInfo info = _InfluenceInfo[id];  
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the next step, we need to determine which pixels are in the entity&apos;s influence. To do this, we will convert the entity&apos;s position and radius into pixels.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#pragma kernel TranscribeMethod1

[numthreads(8,8,1)]  
void TranscribeMethod1(uint id: SV_GroupIndex)  
{  
    if (id &amp;gt;= _InfluenceInfoCount) return;  
    InfluenceInfo info = _InfluenceInfo[id];  

	half radius = info.radius;  
	int pxRadius = radius / _InfluenceTextureParams.x;  
	uint2 infoPxPos = (info.worldPosition.xz - _InfluenceTextureParams.zw) / _InfluenceTextureParams.x;  
	
	for (int x = -pxRadius; x &amp;lt;= pxRadius; x++)  
	{  
	    for (int y = -pxRadius; y &amp;lt;= pxRadius; y++)  
	    {
			int2 px = infoPxPos + int2(x, y); 
		    //...
	    }
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two &lt;code&gt;for&lt;/code&gt; loops cover a square instead of a circle, so we still need to check if a pixel is in the circle by calling &lt;code&gt;IsPixelInInfluence&lt;/code&gt;. After that, if we find the pixel is indeed in influence, we call &lt;code&gt;UpdateInfluence&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-2/pixel-range.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#pragma kernel TranscribeMethod1

[numthreads(8,8,1)]  
void TranscribeMethod1(uint id: SV_GroupIndex)  
{  
    if (id &amp;gt;= _InfluenceInfoCount) return;  
    InfluenceInfo info = _InfluenceInfo[id];  
    half radius = info.radius;  
    int pxRadius = radius / _InfluenceTextureParams.x;  
    uint2 infoPxPos = (info.worldPosition.xz - _InfluenceTextureParams.zw) / _InfluenceTextureParams.x;  
    for (int x = -pxRadius; x &amp;lt;= pxRadius; x++)  
    {  
        for (int y = -pxRadius; y &amp;lt;= pxRadius; y++)  
        {  
            int2 px = infoPxPos + int2(x, y);  
            half2 thisPxWsPos = PixelToWorldPos(px);  
            if (IsPixelInInfluence(thisPxWsPos, info))  
            {  
                UpdateInfluence(px, thisPxWsPos, info);  
            }  
        }  
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Transcription Method 2: Iterate Per Pixel&lt;/h4&gt;
&lt;p&gt;The idea is similar, but this time we simply go through all the pixels in the texture and check if it is in &lt;em&gt;any&lt;/em&gt; of the entity&apos;s influence. We are using &lt;code&gt;SV_DispatchThreadID&lt;/code&gt; which gives us pixel coordinates.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#pragma kernel TranscribeMethod2

[numthreads(8,8,1)]  
void TranscribeMethod2(uint3 id: SV_DispatchThreadID)  
{  
    uint2 px = id.xy;  
    if (px.x &amp;gt;= _InfluenceTextureParams.y || px.y &amp;gt;= _InfluenceTextureParams.y) return;  
    // Iterate over each entity  
    for (int i = 0; i &amp;lt; _InfluenceInfoCount; i++)  
    {  
        InfluenceInfo info = _InfluenceInfo[i];  
        half2 thisPxWsPos = PixelToWorldPos(px);  
        if (IsPixelInInfluence(thisPxWsPos, info))  
        {  
            UpdateInfluence(px, thisPxWsPos, info);  
            return;  
        }  
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that concludes our compute shader (for now).&lt;/p&gt;
&lt;p&gt;In the next part, we will setup the CPU side of the entity influence system.&lt;/p&gt;
</content:encoded></item><item><title>Grass Interaction - Part 1</title><link>https://fukafukaseika.moe/posts/grass-interaction-1/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/grass-interaction-1/</guid><description>Learn how to create physically based interactive grass in Unity. In this first part of the series, we will discuss how grass and instigators are abstracted physically, and establish the theoretical foundations for the implementation.</description><pubDate>Sun, 12 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Grass Interaction&lt;/strong&gt;, a staple in any game that has grass, and an inevitable when making open-world games (unless you are making a scorched land). However, most of the tutorials out there seem to be a little limited when it comes to make natural-looking grass interaction. Most would stop at the &lt;em&gt;trample&lt;/em&gt; effect - which is ok for basic use cases. The problem becomes more complex when you add NPCs or making a &lt;em&gt;bouncing&lt;/em&gt; effect.&lt;/p&gt;
&lt;p&gt;To fill the gap, this series aim to provide a slightly overkill simulation of grass interaction - by overkill I mean it won&apos;t be frame-rate friendly since we will be doing lots of expensive calculations. You could simplify the calculations depending on your project need.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;We will achieve the following effects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Grass is trampled when very near an instigator.&lt;/li&gt;
&lt;li&gt;Grass curves outwards when near an instigator.&lt;/li&gt;
&lt;li&gt;Grass will gradually restore its position after leaving the influence of an instigator.&lt;/li&gt;
&lt;li&gt;Grass &lt;em&gt;may&lt;/em&gt; show some oscillation during the restoration process.&lt;/li&gt;
&lt;li&gt;Each grass can have its own elasticity constant and damping coefficient.&lt;/li&gt;
&lt;li&gt;Support multiple instigators.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-1/grass-interaction-showcase-lowres.webp&quot; alt=&quot;grass oscillation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We will improve grass interaction based on the most basic method - adding a displacement in the vertex shader. There are plenty of other methods that provide better simulations, for example, using bone animation, suitable for more detailed plants. The technique we are using assumes the following conditions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Newtonian physics applies&lt;/strong&gt; - we only deal with macro, low-velocity objects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grass width is significantly smaller than instigators&lt;/strong&gt; - like grass vs human in real life. If your game is about grass vs cricket, then this technique is suboptimal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No precise physical simulation needed&lt;/strong&gt; - we will make lots of simplifications and compromises to achieve a visually correct result, but these simplifications will make the simulation not strictly physically correct.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The series is divided into 5 parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Part 1 (this post): physical abstraction of grass, instigators, and relevant theories.&lt;/li&gt;
&lt;li&gt;Part 2: Calculating influence of instigators.&lt;/li&gt;
&lt;li&gt;Part 3: Send influence data to the GPU.&lt;/li&gt;
&lt;li&gt;Part 4: Applying vertex displacement.&lt;/li&gt;
&lt;li&gt;Part 5: Adjustments and Refinements.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: The theoretical part requires you to have some college level calculus and Newtonian physics knowledge to understand completely. In fact, it would be very similar to a first year physics course. But it is OK if you don&apos;t understand everything - it is not required to achieve the effects mentioned above.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now let&apos;s dive into the theory.&lt;/p&gt;
&lt;h2&gt;Physical Abstraction of Grass&lt;/h2&gt;
&lt;h3&gt;Formulation of the System&lt;/h3&gt;
&lt;p&gt;We want to reduce the grass interaction problem to a well-studied physical system. What could it be, then?&lt;/p&gt;
&lt;p&gt;Empirically, the movement of grass resembles &lt;strong&gt;a mass attached to an elastic rod&lt;/strong&gt;. One end of the elastic rod is attached to the ground and is immobile, the other end is attached to the mass. The elastic rod can be bent but cannot be compressed. For simplicity, the mass of the elastic rod is ignored.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-1/elastic-rod.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Though this simplification is still too complex for us, due to the rod being bendable and changing the straight distance between the mass and the root. What we can do is to assume the rod is non-bendable (rigid), but still providing elastic force $ F_s $. Imagine there is a hidden contraption at the root of the grass, driven by a spring, whenever the grass isn&apos;t upright, the contraption provides the elastic force to restore the upright position.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-1/rigid-rod.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you now flip your monitor upside down, you might recognize this diagram in other literature: &lt;strong&gt;This is very similar to a damped/driven pendulum system&lt;/strong&gt;, except it is upside down, and that is indeed a well-studied physical system.&lt;/p&gt;
&lt;h3&gt;Force Analysis&lt;/h3&gt;
&lt;p&gt;Our goal is to obtain an analytical expression $\theta(t)$ that, given elapsed time $t$, we obtain the signed angle $\theta$.&lt;/p&gt;
&lt;p&gt;Assume the signed angle between the up direction and the rigid rod is $\theta$:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$Fs$ is the elastic force perpendicular to the rigid rod and always pointing towards the upright position. Similar to &lt;em&gt;Hooke&apos;s Law&lt;/em&gt;, this force is proportional to angle $\theta$. Hence, $$ Fs=m&lt;em&gt;k&lt;/em&gt;\theta$$ where $k$ is the elasticity of the grass.&lt;/li&gt;
&lt;li&gt;The gravity $G$ can be decomposed into a force that goes along the rigid rod $Gy$ and another that is perpendicular to the rigid rod, in the opposite direction of the elastic force $Gx$. $Gy$ is balanced out by the supportive force of the rigid rod, then it only leaves $Gx$ our point of interest. With a little trigonometry, we can find $$Gx = sin(\theta)&lt;em&gt;m&lt;/em&gt;g$$&lt;/li&gt;
&lt;li&gt;Another damping force $D$ proportional to the angular velocity of the mass: $$D = \frac{d\theta}{dt}&lt;em&gt;b&lt;/em&gt;m$$ where $b$ is the damping coefficient. This force opposes the direction of the velocity.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: Usually we do not multiply damping force by mass. But since both $b$ and $m$ are invariant during the simulation, $b*m$ can be seen as just a constant of the grass that replaces the conventional damping coefficient.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Therefore, with the 2nd law:&lt;/p&gt;
&lt;p&gt;$$
a = \frac{d^2x}{dt^2}=k*\theta- sin(\theta)*g - \frac{d\theta}{dt}*b
$$&lt;/p&gt;
&lt;p&gt;We know that linear displacement can be expressed as $dx = l_0*d\theta$ where $l_0$ is the length of the rod. Derive the expression 2 times, then we will have:&lt;/p&gt;
&lt;p&gt;$$
\frac{d^2x}{dt^2} = l_0*\frac{d^2\theta}{dt^2} = k*\theta- sin(\theta)*g - \frac{d\theta}{dt}*b
$$&lt;/p&gt;
&lt;p&gt;Simplify the equation and we have a familiar form:&lt;/p&gt;
&lt;p&gt;$$
l_0*\frac{d^2\theta}{dt^2} + b*\frac{d\theta}{dt} + g&lt;em&gt;sin(\theta) - k&lt;/em&gt;\theta = 0
$$&lt;/p&gt;
&lt;p&gt;This equation is nonlinear because of the $g*sin(\theta)$ term, and we have to do a classic trick to make it solvable analytically - &lt;strong&gt;Small Angle Approximation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The following graph shows $f(x) = sin(x)$ (green), $f(x) = x$ (red) and $f(x)=x-\frac{x^3}{6}$ (blue).&lt;/p&gt;
&lt;p&gt;Around 28 degrees (0.5 radians), we can see that  $f(x) = x$ is very close to $f(x) = sin(x)$. The Taylor expansion with 2 terms $f(x)=x-\frac{x^3}{6}$ gives a better approximation, but it doesn&apos;t make the equation easier to solve.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-1/sin-approximation.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;However, remember that we do not need a strictly physical simulation, we just need it to be &lt;em&gt;visually correct&lt;/em&gt;. So we will use $x$ to approximate $sin(x)$ in all cases. Our equation now becomes:&lt;/p&gt;
&lt;p&gt;$$
\frac{d^2\theta}{dt^2} + \frac{b}{l_0}&lt;em&gt;\frac{d\theta}{dt} + \frac{g-k}{l_0}&lt;/em&gt;\theta = 0
$$&lt;/p&gt;
&lt;p&gt;We now assume:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\gamma=\frac{b}{2*l_0}$&lt;/li&gt;
&lt;li&gt;$\omega_0 = \sqrt{\frac{g-k}{l_0}}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;then we can simplify the equation to an even more familiar form:&lt;/p&gt;
&lt;p&gt;$$
\frac{d^2\theta}{dt^2} + 2\gamma*\frac{d\theta}{dt} + \omega_0^2*\theta = 0
$$&lt;/p&gt;
&lt;h3&gt;Solving the Differential Equation&lt;/h3&gt;
&lt;p&gt;We will now use the &lt;strong&gt;Exponential Guess&lt;/strong&gt; method:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Guess an exponential solution of the form $Ae^{\alpha t}$&lt;/li&gt;
&lt;li&gt;Derive the condition $\alpha$ must satisfy in order for $Ae^{\alpha t}$  to be a solution of the equation.&lt;/li&gt;
&lt;li&gt;Solve the condition for values of $α_1,...,α_n$&lt;/li&gt;
&lt;li&gt;If the $α_i$ are unique, write general solution as $x(t) = A_1e^{\alpha_1 t}+A_2e^{\alpha_2t} + ··· + A_ne^{\alpha_nt}$.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If $\theta(t)$ is of form $Ae^{\alpha t}$ then:&lt;/p&gt;
&lt;p&gt;$$
A(\alpha^2+2\gamma\alpha+\omega_0^2)e^{\alpha t}=0
$$&lt;/p&gt;
&lt;p&gt;Which means:&lt;/p&gt;
&lt;p&gt;$$
\alpha^2+2\gamma\alpha+\omega_0^2=0
$$&lt;/p&gt;
&lt;p&gt;And the solution for $\alpha$ is&lt;/p&gt;
&lt;p&gt;$$
\alpha = -2\gamma \pm \sqrt{\gamma^2-\omega_0^2}
$$&lt;/p&gt;
&lt;p&gt;Depending on the value of $\gamma$ and $\omega_0$, we can have 3 different cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\omega_0&amp;gt;\gamma$ : weak damping or &lt;strong&gt;underdamped&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;$\omega_0=\gamma$ : critical damping or &lt;strong&gt;critically damped&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;$\omega_0&amp;lt;\gamma$: strong damping or &lt;strong&gt;overdamped&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-1/damping-cases.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;we also note:&lt;/p&gt;
&lt;p&gt;$$
\omega = \sqrt{abs(\gamma^2-\omega_0^2)}
$$&lt;/p&gt;
&lt;p&gt;Now to properly solve the equations we need some &lt;em&gt;initial conditions&lt;/em&gt;. In our grass simulation, we assume the grass has no initial velocity when it leaves an instigator&apos;s influence, and we can write the initial conditions as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\theta(0)=\theta_0$ (initial angle)&lt;/li&gt;
&lt;li&gt;$\frac{d\theta(0)}{dt}=0$ (no initial velocity)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Underdamped&lt;/h4&gt;
&lt;p&gt;When underdamped, the solution to our equation is complex:&lt;/p&gt;
&lt;p&gt;$$
\theta(t)=A_1e^{\alpha_1 t}+A_2e^{\alpha_2 t} = e^{-\gamma t}(A_1e^{i\omega t}+A_2e^{-i\omega t})
$$&lt;/p&gt;
&lt;p&gt;But since we need a real solution, we can just take the real part (recall Euler&apos;s formula $e^{it}=cos(t)+isin(t)$ ):&lt;/p&gt;
&lt;p&gt;$$
\theta(t)=e^{-\gamma t}(A_1cos(\omega t)+A_2sin(\omega t))
$$&lt;/p&gt;
&lt;p&gt;since cos and sin are only different in phase, we can further rewrite the previous solution as&lt;/p&gt;
&lt;p&gt;$$
\theta(t)=A_0*e^{-\gamma t}cos(\omega t-\phi)
$$&lt;/p&gt;
&lt;p&gt;We only need to use initial conditions to solve $A_0$ and $\phi$ now.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;given $\frac{d\theta(0)}{dt}=0$ we have $\phi = 0$&lt;/li&gt;
&lt;li&gt;given $\theta(0)=\theta_0$ we have $A_0 = \theta_0$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hence, for &lt;strong&gt;underdamped&lt;/strong&gt; case, we have&lt;/p&gt;
&lt;p&gt;$$
\theta(t)=\theta_0*e^{-\gamma t}cos(\omega t)
$$&lt;/p&gt;
&lt;h4&gt;Overdamped&lt;/h4&gt;
&lt;p&gt;Similar to underdamped, but without the imaginary:&lt;/p&gt;
&lt;p&gt;$$
\theta(t)=A_1e^{\alpha_1 t}+A_2e^{\alpha_2 t} = e^{-\gamma t}(A_1e^{\omega t}+A_2e^{-\omega t})
$$&lt;/p&gt;
&lt;p&gt;with the initial conditions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;given $\theta(0)=\theta_0$ we have $A_1+A_2 = \theta_0$&lt;/li&gt;
&lt;li&gt;given $\frac{d\theta(0)}{dt}=0$ we have $A_1-A_2=\frac{\gamma\theta_0}{\omega}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Solve them and we get&lt;/p&gt;
&lt;p&gt;$$
A_1 = \frac{\theta_0}{2} + \frac{\gamma\theta_0}{2\omega}
$$&lt;/p&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;p&gt;$$
A_2 = \frac{\theta_0}{2} - \frac{\gamma\theta_0}{2\omega}
$$&lt;/p&gt;
&lt;h4&gt;Critically Damped&lt;/h4&gt;
&lt;p&gt;In this case we have non-unique solution for $\alpha$ and we must write the solution in another form (note that we have $\omega_0 = \gamma$ then $\alpha = -\gamma$):&lt;/p&gt;
&lt;p&gt;$$
\theta(t) = (A+Bt)e^{-\gamma t}
$$
Again, applying our initial conditions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;given $\theta(0)=\theta_0$ we have $A = \theta_0$&lt;/li&gt;
&lt;li&gt;given $\frac{d\theta(0)}{dt}=0$ we have $B=\gamma\theta_0$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hence,&lt;/p&gt;
&lt;p&gt;$$
\theta(t) = \theta_0(1+\gamma t)e^{-\gamma t}
$$&lt;/p&gt;
&lt;p&gt;Now we have everything we need to describe the movement of the grass when the instigator&apos;s influence disappears!&lt;/p&gt;
&lt;h2&gt;Physical Abstraction of Instigators&lt;/h2&gt;
&lt;p&gt;Now the problem is how we determine the initial condition $\theta_0$ mentioned above. And this initial angle is tied to the &lt;em&gt;shape&lt;/em&gt; of the instigator.&lt;/p&gt;
&lt;p&gt;We assume the height of the instigator can be defined as a function $h(x)$ where $x$ represents the distance between the center of the instigator and the root of the grass.&lt;/p&gt;
&lt;p&gt;A simple approach would be to use &lt;em&gt;linear interpolation&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;$$
h(x) = cx
$$
where &lt;em&gt;c&lt;/em&gt; is an instigator-specific constant.&lt;/p&gt;
&lt;h3&gt;Second Order Polynomial&lt;/h3&gt;
&lt;p&gt;In our case, let us do something a little bit more unnecessary: use a curve instead.&lt;/p&gt;
&lt;p&gt;$$
h(x) = cx^2=\frac{h_0}{R_0^2}x^2
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: Later, I found out that I overlooked a crucial aspect - $$\theta(R_0) = 0$$. This is not easily satisfied when using a second order polynomial with the tangent case simplification. Eventually I would end up using elliptic curve as it guarantees $$\frac{dh(R_0)}{d(d)}=tan(\pi/2)$$. But I still leave the polynomial reasoning here to demonstrate the thought process. If you don&apos;t care about it, jump to the ellipse section.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$h_0$ is the height of the instigator.&lt;/li&gt;
&lt;li&gt;$R_0$ is the radius of the instigator.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The advantage of using a second order function is that it resembles placing a &lt;em&gt;capsule collider&lt;/em&gt; which is exactly what a &lt;em&gt;character controller&lt;/em&gt; looks like.&lt;/p&gt;
&lt;p&gt;Grass has a fixed length of $l_0$, thus we can use a segment $g(x)=p(x-d)$ to express it, where $d$ is the distance between the center of the instigator and the root of the grass. It also means we may have 2 cases to deal with when trying to solve the initial angle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Tangent Case&lt;/em&gt;: the grass blade is a tangent of the instigator curve.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Non-tangent Case&lt;/em&gt;: the grass blade&apos;s extremity touches the instigator curve.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-1/tangent-non-tangent-cases.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Tangent Case&lt;/h4&gt;
&lt;p&gt;Assume the intersection between the grass segment and the curve is $x_0$. The derivative of the curve at $x_0$ provides the slope of the grass segment. Hence:&lt;/p&gt;
&lt;p&gt;$$p=h&apos;(x_0)=2cx_0$$&lt;/p&gt;
&lt;p&gt;And since the line intersects with the curve at $x_0$, we have:&lt;/p&gt;
&lt;p&gt;$$x_0^2*c=2cx_0(x_0-d)$$&lt;/p&gt;
&lt;p&gt;after simplification:&lt;/p&gt;
&lt;p&gt;$$x_0 = 2d$$&lt;/p&gt;
&lt;p&gt;the grass equation becomes:&lt;/p&gt;
&lt;p&gt;$$g(x)=4cd(x-d)$$&lt;/p&gt;
&lt;p&gt;and we have:&lt;/p&gt;
&lt;p&gt;$$\theta_0(d) = \frac{\pi}{2}-arctan(4cd)$$&lt;/p&gt;
&lt;h4&gt;Non-tangent Case&lt;/h4&gt;
&lt;p&gt;In this case, the intersection is $(x_0,cx_0^2)$ by evaluating the instigator curve at $x_0$. The displacement relative to $(d,0)$ is $(x_0-d,cx_0^2)$. This vector has length of $l_0$. Then:&lt;/p&gt;
&lt;p&gt;$$(x_0-d)^2+c^2*x_0^4=l_0^2$$&lt;/p&gt;
&lt;p&gt;after expansion:&lt;/p&gt;
&lt;p&gt;$$c^2&lt;em&gt;x_0^4+x_0^2-2d&lt;/em&gt;x_0+(d^2-l_0^2)=0$$&lt;/p&gt;
&lt;p&gt;Unfortunately, this is not practical to solve. But remember we are not doing physically accurate simulation, so we will &lt;strong&gt;treat the non-tangent case as tangent case&lt;/strong&gt;.&lt;/p&gt;
&lt;h4&gt;The issue&lt;/h4&gt;
&lt;p&gt;The polynomial approach has a major issue that I only discovered later - it can create discontinuity at $R_0$ where we expect 0 degrees:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-1/polynomial-discontinuity.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is a fairly noticeable discontinuity and causes grass blade to sort of &quot;teleport&quot; when entering an entity&apos;s influence.&lt;/p&gt;
&lt;p&gt;Here&apos;s a better illustration of the problem:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-1/polynomial-discontinuity-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The curve has $$h_0 = 2$$ and $$R_0 = 5$$, ideally; we wish the grass whose root is at 5 to be close to upright, because anything outside of 5 should be upright. However, in the above diagram, we can see that the tangent of the curve passing point (5,0) is not at all upright. This means any grass that touches the rim of the object will suddenly lean with that angle, resulting in visual pops.&lt;/p&gt;
&lt;p&gt;To eradicate this problem, we need to change how the object is abstracted, And the curve that has a slope of 0 at origin and a slope of infinity (perpendicular to the x axis) at $R_0$, is part of an &lt;strong&gt;ellipse&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Ellipse&lt;/h3&gt;
&lt;p&gt;We now model the instigator as an ellipse, and its equation would look like this:&lt;/p&gt;
&lt;p&gt;$$\frac{y^2}{h_0^2}+\frac{x^2}{R_0^2}=1$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: $y$ is $h(x)$ seen in the polynomial approach.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This ellipse doesn&apos;t pass through $(0,0)$ and $(R_0,h_0)$, but $(R_0,0)$ and $(0,-h_0)$. For now, it is fine, because what we will calculate is the slope of the tangent, which doesn&apos;t change even if we move the ellipse.&lt;/p&gt;
&lt;p&gt;To obtain the slope $\frac{dy}{dx}$, we derive by $\frac{d}{dx}$:&lt;/p&gt;
&lt;p&gt;$$\frac{2}{h_0^2}&lt;em&gt;y&lt;/em&gt;\frac{dy}{dx}+\frac{2}{R_0^2}*x = 0$$&lt;/p&gt;
&lt;p&gt;After simplification, we obtain:&lt;/p&gt;
&lt;p&gt;$$\frac{dy}{dx}=-\frac{h_0^2}{R_0^2}\frac{x}{y}$$
Now we need to find a point $(x_0,y_0)$ on the ellipse to calculate the value of the slope.&lt;/p&gt;
&lt;p&gt;We already know that the tangent should pass through $(d,0)$, but since our ellipse is shifted down $h_0$ units, the point becomes $(d,-h_0)$. The tangent equation of an ellipse is given by:&lt;/p&gt;
&lt;p&gt;$$\frac{x_0x}{R_0^2}+\frac{y_0y}{h_0^2} = 1$$&lt;/p&gt;
&lt;p&gt;Substitute with $(d,-h_0)$, we obtain&lt;/p&gt;
&lt;p&gt;$$\frac{x_0d}{R_0^2}-\frac{y_0h_0}{h_0^2} = 1$$
moving the terms around, we get&lt;/p&gt;
&lt;p&gt;$$y_0=h_0(\frac{x_0d}{R_0^2}-1)$$
Now we plug it back to the ellipse equation:&lt;/p&gt;
&lt;p&gt;$$\frac{(h_0(\frac{x_0d}{R_0^2}-1))^2}{h_0^2}+\frac{x_0^2}{R_0^2}=1$$&lt;/p&gt;
&lt;p&gt;which simplifies to:&lt;/p&gt;
&lt;p&gt;$$x_0((R_0^2+d^2)x_0-2R_0^2h_0)=0$$
we have 2 solutions, either $$x_0=0$$ (valid but not what we are interested in), and&lt;/p&gt;
&lt;p&gt;$$x_0=\frac{2R_0^2h_0}{R_0^2+d^2}$$&lt;/p&gt;
&lt;p&gt;then we plug it back to the $y_0$ equation we have&lt;/p&gt;
&lt;p&gt;$$y_0 = h_0(\frac{2d^2}{R_0^2+d^2}-1)$$
with these we can express the slope as a function of $d$:&lt;/p&gt;
&lt;p&gt;$$\frac{dy}{dx}=-\frac{h_0^2}{R_0^2}\frac{\frac{2R_0^2h_0}{R_0^2+d^2}}{h_0(\frac{2d^2}{R_0^2+d^2}-1)}=\frac{2h_0d}{R_0^2-d^2}$$
Finally, we can express the initial angle as&lt;/p&gt;
&lt;p&gt;$$\theta_0(d) = \frac{\pi}{2}-arctan(\frac{2h_0d}{R_0^2-d^2})$$&lt;/p&gt;
&lt;p&gt;Here&apos;s the initial angle curve for an instigator that has 10 radius and 3 height, we can see the curve approaches 0 when approaching 10 from the left:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/grass-interaction-1/ellipse-initial-angle.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We must take precautions in this case : when $$d=R_0$$, we have division by zero. and we must account for it in shader.&lt;/p&gt;
&lt;p&gt;We will need to express $y$ with $x$ later, and this time we must move the ellipse up by $h_0$ units:&lt;/p&gt;
&lt;p&gt;$$\frac{(y-h_0)^2}{h_0^2}+\frac{x^2}{R_0^2}=1$$&lt;/p&gt;
&lt;p&gt;After simplification, we have&lt;/p&gt;
&lt;p&gt;$$y=h(x)=h_0(\sqrt{1-\frac{x^2}{R_0^2}}+1)$$&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;We established the 3 cases for grass motion $\theta(t)$ :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Underdamped&lt;/strong&gt; : $\theta(t)=\theta_0*e^{-\gamma t}cos(\omega t)$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Overdamped&lt;/strong&gt;: $\theta(t)= e^{-\gamma t}(A_1e^{\omega t}+A_2e^{-\omega t})$ where $A_1 = \frac{\theta_0}{2} + \frac{\gamma\theta_0}{2\omega}$ and $A_2 = \frac{\theta_0}{2} - \frac{\gamma\theta_0}{2\omega}$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Critically damped&lt;/strong&gt;: $\theta(t) = \theta_0(1+\gamma t)e^{-\gamma t}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\gamma=\frac{b}{2*l_0}$&lt;/li&gt;
&lt;li&gt;$\omega_0 = \sqrt{\frac{g-k}{l_0}}$&lt;/li&gt;
&lt;li&gt;$\omega = \sqrt{abs(\gamma^2-\omega_0^2)}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And a way to express initial angle $\theta_0$ with an elliptic instigator:&lt;/p&gt;
&lt;p&gt;$$\theta_0(d) = \frac{\pi}{2}-arctan(\frac{2h_0d}{R_0^2-d^2})$$&lt;/p&gt;
&lt;p&gt;The height of the lower surface at of the instigator at $d$ relative to its lowest point is expressed as&lt;/p&gt;
&lt;p&gt;$$h(d)=h_0(\sqrt{1-\frac{d^2}{R_0^2}}+1)$$&lt;/p&gt;
&lt;p&gt;In part 2 we will record the influence of the instigator (its position, velocity, and some calculated results) to fuel the eventual calculation in the vertex shader.&lt;/p&gt;
</content:encoded></item><item><title>Unity Profiling: C++ Capture</title><link>https://fukafukaseika.moe/posts/perf-cpp-capture/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/perf-cpp-capture/</guid><description>Learn how to use NVidia NSight Graphics to conduct c++ captures on Unity Editor, and perform otherwise discouraged GPU Trace to obtain more information about the rendering bottlenecks of your game.</description><pubDate>Sun, 05 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When you start to keep an eye on the frame rate of your game, chances are you will start with the official solution: &lt;em&gt;Unity Profiler&lt;/em&gt;. Indeed, most times the built-in profiler is more than enough to know what&apos;s causing frame rate drops. Though, the profiler is slightly limited when it comes to anything that&apos;s happening on the GPU - you only know vaguely in which pass the GPU is suffering, but you must use your experience or educated guess to pinpoint the exact problem.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Nsight Graphics&lt;/em&gt; and &lt;em&gt;Render Doc&lt;/em&gt; are used for frame debugging (mostly), and the timer in the frame debugger is not much better than Unity&apos;s built-in profiler.&lt;/p&gt;
&lt;p&gt;C++ Capture was a trick that I discovered myself to gain extra insight into performance issues. It still uses &lt;em&gt;Nsight Graphics&lt;/em&gt; - usually we can&apos;t access the GPU counter when launching Nsight, because that requires running Unity in admin mode, which Unity directly says &lt;em&gt;Don&apos;t do it&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The idea is simple: launch Nsight in frame debugging mode as usual - no admin privilege needed, and then play the game. Capture a frame, and then hit &lt;strong&gt;Export C++ Capture&lt;/strong&gt;. Build the C++ capture, and &lt;strong&gt;launch the C++ capture with GPU Trace in admin mode&lt;/strong&gt;. And there you can get all the analytics about shader bottlenecks and more accurate timer values.&lt;/p&gt;
&lt;p&gt;There&apos;s a small catch: this method isn&apos;t guaranteed to work. You could fail to export a capture, or the capture crashes your Nsight when attempting to use GPU Trace on it.&lt;/p&gt;
&lt;h2&gt;Step-by-Step Guide&lt;/h2&gt;
&lt;p&gt;First, launch Nsight, and click &lt;strong&gt;Start Activity&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In the pop-up, fill &lt;strong&gt;Application Executable&lt;/strong&gt; with the path to Unity Editor executable (if you are not sure where it is located, check out the &lt;strong&gt;Installs&lt;/strong&gt; tab in your &lt;strong&gt;Unity Hub&lt;/strong&gt;).&lt;/p&gt;
&lt;p&gt;Then, fill &lt;strong&gt;Command Line Arguments&lt;/strong&gt; with &lt;code&gt;--projectPath {path to your project}&lt;/code&gt;. You can also add other arguments specified &lt;a href=&quot;https://docs.unity3d.com/6000.0/Documentation/Manual/EditorCommandLineArguments.html&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Make sure in &lt;strong&gt;Activity&lt;/strong&gt;, you are launching &lt;strong&gt;Frame Debugger&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The editor should launch, and when it finishes, play the game regularly till you meet a performance critical scenario. Either hit F11 to capture or click &lt;strong&gt;Capture for Live Analysis&lt;/strong&gt; on the upper-right corner of your Nsight:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Sometimes it can be tricky to capture the window you want... to save yourself from such a hassle, maximize the game view before capturing.&lt;/p&gt;
&lt;p&gt;Still on the upper-right corner, there should be an  &lt;strong&gt;Export as C++ Capture&lt;/strong&gt; button, click that.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A new window should pop-up below - it represents the C++ Capture of your frame. Navigate to the &lt;strong&gt;Build&lt;/strong&gt; section, and click the Build button. Wait until the build finishes. (You can actually build the capture later, before launching GPU trace)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now you can &lt;strong&gt;close the Nsight&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Relaunch the Nsight &lt;strong&gt;with Admin rights&lt;/strong&gt;, navigate to the &lt;strong&gt;Continue&lt;/strong&gt; section of the welcome screen. There you should find your C++ capture. Click it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Scroll all the way down to the &lt;strong&gt;Run&lt;/strong&gt; section and click &lt;strong&gt;Connect&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Make sure you select &lt;strong&gt;GPU Trace Profiler&lt;/strong&gt; and click launch:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-8.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Collect GPU Trace&lt;/strong&gt; after seeing your capture launched:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-9.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; : The above screenshot failed to capture as it shows a dropdown box for window selection - it means my PC is using both an embedded GPU and a dedicated GPU. Nsight doesn&apos;t really like that. Force the PC to use discrete GPU via settings should solve the problem.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-10.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And now you have a complex interface to navigate... From this point on, I suggest looking for the dedicated documentation of the &lt;strong&gt;GPU Trace&lt;/strong&gt; as I simply can&apos;t cover that much content in this post, and many of the concepts are fairly low level.&lt;/p&gt;
&lt;p&gt;For some quick info, though, the hotspot section shows which shader caused the most performance issue:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-11.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Clicking on it will send you to the line where the problem happened:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/perf-cpp-capture/cpp-cap-12.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Euh, but this isn&apos;t shader code I&apos;m used to!&lt;/em&gt; - true, you will be looking at disassembly code, but usually the issue tells you the root cause and you should be able to pinpoint the line in the original shader code.&lt;/p&gt;
</content:encoded></item><item><title>Dynamic Resolution - &quot;Hardware&quot; vs &quot;Software&quot;</title><link>https://fukafukaseika.moe/posts/hdrp-dynamic-resolution-hardware-vs-software/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/hdrp-dynamic-resolution-hardware-vs-software/</guid><description>Learn the difference between Unity HDRP Dynamic Resolution&apos;s Hardware and Software modes, and when to prefer one over the other.</description><pubDate>Thu, 26 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In Unity&apos; HDRP, dynamic resolution can be used to improve framerate without sacrificing too much of the game&apos;s visual qualities. Most of the tweakable parameters are documented, except for &quot;Hardware&quot; and &quot;Software&quot; dynamic resolution modes. OK, then, what are those?&lt;/p&gt;
&lt;p&gt;I spent some time poking into the two different modes and it turned out &quot;Hardware&quot; and &quot;Software&quot; aren&apos;t that simple, they have serious impact on the whole render pipeline.&lt;/p&gt;
&lt;p&gt;What&apos;s more, in my opinion, dynamic resolution is not a hardware-level thing. They just use the graphics API differently to achieve the downscaling effect.&lt;/p&gt;
&lt;h3&gt;&quot;Software&quot; dynamic resolution (Viewport)&lt;/h3&gt;
&lt;p&gt;Before tackling what &quot;viewport implementation&quot; is, let us review how is the viewport data specified:&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;VkPipeline&lt;/code&gt; construction, we could specify &lt;code&gt;vkViewport&lt;/code&gt;s in &lt;code&gt;VkPipelineViewportStateCreateInfo&lt;/code&gt;, or when using dynamic viewports, it is set with &lt;code&gt;vkCmdSetViewport&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Non-dynamic viewports are preferable when viewport &lt;em&gt;does not&lt;/em&gt; change. If you &lt;em&gt;do&lt;/em&gt; change it however, you need to recreate the whole &lt;code&gt;VkPipeline&lt;/code&gt; which would result in significant stutter.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dynamic viewports are preferable when viewport &lt;em&gt;does&lt;/em&gt; change. This does induce small runtime performance costs due to the extra command.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unity uses dynamic viewport by default: (capture taken when dynamic resolution is disabled)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/dynamic-resolution/dy-res-viewport-call.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Therefore, dynamic resolution does not induce &lt;em&gt;extra&lt;/em&gt; overhead of set viewport commands.&lt;/p&gt;
&lt;p&gt;Now, the viewport implementation.&lt;/p&gt;
&lt;p&gt;Essentially, viewport implementation creates one single &lt;code&gt;VkImage&lt;/code&gt; with the size of the swapchain extents and draws on a limited area specified by the viewport:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/dynamic-resolution/dy-res-viewport-impl-alloc.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note the Dimension here: it is exactly the size of the swapchain extents&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/dynamic-resolution/dy-res-viewport-impl-dim.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Viewport affects clip space coordinates translation, mapping the range of the clip space to the area of the viewport. When the driver knows the viewport beforehand, it doesn&apos;t need to run the fragment program on pixels outside of the viewport, and that&apos;s how we gain the performance boost.&lt;/p&gt;
&lt;h3&gt;&quot;Hardware&quot; dynamic resolution (Memory Aliasing)&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Hardware&lt;/em&gt; implementation is only available in DX12 and Vulkan for good reason: &lt;strong&gt;Memory Aliasing&lt;/strong&gt; can only be achieved with lower-level graphics API.&lt;/p&gt;
&lt;p&gt;In a nutshell, Memory Aliasing is creating multiple &lt;code&gt;VkImage&lt;/code&gt; but map them to the same chunk of memory (the largest chunk required by all these images).&lt;/p&gt;
&lt;p&gt;Here&apos;s a capture taken when memory aliasing implementation is enabled:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/dynamic-resolution/dy-res-alias-impl-alloc.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note here the Dimension is no longer the extents of the swapchain:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/dynamic-resolution/dy-res-alias-impl-dim.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Unity still issues &lt;code&gt;vkCmdSetViewport&lt;/code&gt; call, but now the viewport covers the entire render target rather than just a portion.&lt;/p&gt;
&lt;p&gt;In Unity, render textures that have dynamic scaling enabled (&lt;code&gt;.useDynamicScale = true&lt;/code&gt;) are managed by &lt;code&gt;ScalableBufferManager&lt;/code&gt;. Unity issues a global update call to update the resolution (probably keeping which slice of the render target to use, managed in the C++ side of the engine).&lt;/p&gt;
&lt;h3&gt;Which one to choose?&lt;/h3&gt;
&lt;p&gt;The reason Unity used &lt;em&gt;Hardware&lt;/em&gt; and &lt;em&gt;Software&lt;/em&gt; to label the two implementations is probably, it is intuitive that if dynamic scaling is supported on a &quot;hardware&quot; level, it is better. And that is actually true. But here are the details why memory aliasing is better than viewport when both options are available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;When the GPU loads a texture (via OP_LOAD), only the mapped memory of &lt;code&gt;VkImage&lt;/code&gt; is pulled. The viewport implementation always loads the full resolution texture, but only draws on part of it, resulting in a waste of GPU bandwidth. In contrast, memory aliasing only loads what is necessary.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The viewport implementation only assures the specific area is written to, but does not prevent reading from areas that are &quot;out of bounds&quot;. This means if you were to sample a texture subject to viewport implementation dynamic resolution before the upscaling pass, not only you don&apos;t save any sampling calls, you must also take precautions that there might be invalid data. Unity hides this fact as usually, you would sample those textures in a post processing pass, while the upscaling pass is placed conveniently before post processing by default.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The conclusion is: prefer &lt;strong&gt;Hardware&lt;/strong&gt; whenever possible, and take precautions when sampling as not all devices support Vulkan/DX12 and will cause a fallback to &lt;strong&gt;Software&lt;/strong&gt;.&lt;/p&gt;
</content:encoded></item><item><title>The &lt;font&gt; Tag in Text Mesh Pro</title><link>https://fukafukaseika.moe/posts/tmp-font-tag/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/tmp-font-tag/</guid><description>Learn how to setup a custom font asset loader in Unity for better font tag usage in Text Mesh Pro.</description><pubDate>Mon, 16 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Changing font regularly might not be a common use case, let alone applying only to some text in a line of text. But sometimes this is necessary, for instance, encrypting a word as part of a linguistic puzzle.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/tmp-font-tag/tmp-font-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;When first using the font tag, the immediate result would be it does not work.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/tmp-font-tag/tmp-font-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;First Intuition: Erroneous Settings&lt;/h3&gt;
&lt;p&gt;When this happens, whether you have googled or thanks to a detective instinct, you would realize something hasn&apos;t been setup properly. And peeking at the &lt;code&gt;Project Settings &amp;gt; TextMesh Pro &amp;gt; Settings&lt;/code&gt;, there is indeed a path that points to a resources directory. (Note the screenshot isn&apos;t showing the default value, expect something like Fonts &amp;amp; Materials if this is the first time you see this).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/tmp-font-tag/tmp-font-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Moving your font assets under the indicated path would resolve the issue.&lt;/p&gt;
&lt;p&gt;Though, if you try to change the path to a custom path as I did, there&apos;s a chance that it doesn&apos;t work. This is because TMP internally stitches the path with a simple string concat rather than &lt;code&gt;Path.Combine&lt;/code&gt;, and if you forgot to add a slash (&quot;/&quot;) at the end of your custom path, the stitched path would be incorrect. For example, if you are trying to use &lt;em&gt;&amp;lt;font=&quot;DeliriumHW&quot;&amp;gt;&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Filling &quot;&lt;strong&gt;Fonts&lt;/strong&gt;&quot; in the settings will cause TMP to call &lt;code&gt;Resources.Load(&quot;Resources/**Fonts**DeliriumHW&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Filling &quot;&lt;strong&gt;Fonts/&lt;/strong&gt;&quot; in the settings will cause TMP to call &lt;code&gt;Resources.Load(&quot;Resources/**Fonts/**DeliriumHW&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Second Intuition: Custom Loader&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Resources.Load&lt;/code&gt; is definitely not the best practice to load things. What&apos;s more, in Addressables, assets in Resources will be duplicated.&lt;/p&gt;
&lt;p&gt;With some research, however, I was able to find an event that gets called before resorting to &lt;code&gt;Resources.Load&lt;/code&gt;, and it is indeed intended to give us a chance to inject a custom loader. The event is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; TMP_Text.OnFontAssetRequest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This would be invoked with an integer and a string, with the string matching the requested font asset name.&lt;/p&gt;
&lt;p&gt;Strangely, this event isn&apos;t mentioned in TMP&apos;s font tag documentation.&lt;/p&gt;
&lt;p&gt;Example of a custom loader:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; private TMP_FontAsset LoadFont(int arg1, string arg2)
 {
     TMP_FontAsset fontAsset;
     if (Application.isEditor &amp;amp;&amp;amp; !Application.isPlaying)
     {
         // when in editor and not in play mode, we load with AssetDatabase
         fontAsset = AssetDbShorthand.FindAndLoadFirst&amp;lt;TMP_FontAsset&amp;gt;(arg2);
     }
     else
     {
        // load with Addressables
         var handle = Addressables.LoadAssetAsync&amp;lt;TMP_FontAsset&amp;gt;($&quot;Fonts/{arg2}.asset&quot;);
         fontAsset = handle.WaitForCompletion();
     }
     return fontAsset;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Note that using this example directly is not advised, as it doesn&apos;t keep the Addressables handle and can cause resource management problems&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;With the addition of this event, we can eventually manage font resources reasonably. It is also possible to preload the fonts in the loading phase to eliminate the impact of a synchronous load.&lt;/p&gt;
&lt;p&gt;Some other events can be found around the &lt;code&gt;OnFontAssetRequest&lt;/code&gt; event, e.g. sprite request. This would solve other related resource management issues that are strangely not covered by the official documentation.&lt;/p&gt;
</content:encoded></item><item><title>Bypassing Unity Undo System&apos;s Memory Limitation</title><link>https://fukafukaseika.moe/posts/unity-undo/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/unity-undo/</guid><description>An insight for bypassing Unity&apos;s undo system&apos;s limitation when editing large terrains, via Harmony.</description><pubDate>Mon, 03 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It is said that in the crumbled city of &lt;em&gt;Uniograad&lt;/em&gt;, there is a catacomb buried deep under the debris. Whenever wind travels through the ruins, the empty chambers of the catacomb would produce a wailing sound. And if you dare to dig into the catacomb, you would find... the unspeakable abomination lurking in the walls...&lt;/p&gt;
&lt;p&gt;Which is Unity&apos;s Undo system.&lt;/p&gt;
&lt;p&gt;Usually this has not much of impact if you don&apos;t do a very specific thing, but if you dare to crank Terrain resolution to 4096, the undo system simply can&apos;t handle it. When you paint texture, chances are you are unable to undo.&lt;/p&gt;
&lt;p&gt;The reason is simple, undo system has a built-in &lt;strong&gt;buffer size limitation&lt;/strong&gt;. If your object is too large and surpasses this size, the undo system will not record it. A terrain with 4096 resolution and 16 layers will have 4 Alpha Map each has 4096x4096 in size, 4 channels, and that would give you something like 1 GB per undo step. It is still reasonable that Unity limited the size because this would quickly blow up your RAM or VRAM, but being unable to do even one undo is kinda... extreme.&lt;/p&gt;
&lt;p&gt;So how to solve this problem?&lt;/p&gt;
&lt;p&gt;Well, this complaint has been on Unity Forum for... more than &lt;strong&gt;8 years&lt;/strong&gt;! And Unity still hasn&apos;t exposed this buffer size limitation. Also, the Undo system is written in C++, so you cannot just use reflection to simply adjust a value.&lt;/p&gt;
&lt;p&gt;A dead end, and another day to curse Unity for being unable to solve a simple problem.&lt;/p&gt;
&lt;p&gt;But really, we can do nothing about it?&lt;/p&gt;
&lt;h2&gt;The Detour&lt;/h2&gt;
&lt;p&gt;Well, there is a way to hack our way out of this... It is still more than necessary, but if you are so desperate, this might be the only way out.&lt;/p&gt;
&lt;p&gt;First, when you paint, the Terrain Tools system will invoke a method in Undo system, requesting the Undo system to save the current state of the objects. And these objects, for painting terrain texture, are simply 4 texture 2D. The instance id of these textures does not change, which means no matter how many strokes you make on a terrain, the 4 textures all keep their instance id. This information is useful for restoring the state of the alpha maps.&lt;/p&gt;
&lt;p&gt;Now... if only we can hijack this invocation...&lt;/p&gt;
&lt;p&gt;Oh, we do have something. There is a plugin called &lt;strong&gt;Harmony&lt;/strong&gt; that can intercept a method call and run some code before actually calling the method. And you can also use the injected code to decide whether to run the original method. This &quot;some code&quot; is called a &quot;prefix&quot; in Harmony terminology.&lt;/p&gt;
&lt;p&gt;But! There is a caveat, Harmony can&apos;t take &lt;strong&gt;extern&lt;/strong&gt; methods. And the bad news is that most of the methods in the Undo system are extern. The good news is, the call made by Terrain Tools system, lands on a non extern method.&lt;/p&gt;
&lt;p&gt;So, heh, after intercepting the objects, we need to create copies for them. It is essentially copying data from one texture to another. And according to &lt;a href=&quot;https://blog.unity.com/engine-platform/accessing-texture-data-efficiently&quot;&gt;this blogpost&lt;/a&gt;, the best way to do this is to use &lt;code&gt;Graphics.CopyTexture&lt;/code&gt;, which takes no noticeable time. However, this means the data lives in GPU, so you probably need to be careful about it, as the textures are fairly large.&lt;/p&gt;
&lt;p&gt;To restore the textures, first we will need to find alpha maps from &lt;code&gt;TerrainData&lt;/code&gt; that have matching instance id as your recorded textures (you did record the instance id or a reference to the original texture as well, right? Because creating a new texture will have a different instance id). Then... you will need to use &lt;code&gt;SetAlphaMaps&lt;/code&gt; to update the original alpha maps. This method requires a &lt;code&gt;float[,,]&lt;/code&gt; to work, but we don&apos;t have it yet...&lt;/p&gt;
&lt;p&gt;The best approach I found is to use &lt;code&gt;GetPixels&lt;/code&gt; on the copied texture to download all pixel data from GPU to your RAM, and use a &lt;code&gt;Parallel.For&lt;/code&gt; to create the 3 dimensional float array. This should reduce the undo operation time to about 1~2 seconds.&lt;/p&gt;
&lt;p&gt;Oh, and finally... It is sad that we can&apos;t intercept the &lt;code&gt;PerformUndo&lt;/code&gt; call since it is extern, so I enforced &lt;em&gt;Alt+Z&lt;/em&gt; to undo when painting texture. This can be done with a simple &lt;code&gt;[EditorMenu]&lt;/code&gt; attribute.&lt;/p&gt;
&lt;p&gt;Anyway, this is not perfect, but still this is the best I can do if Unity keeps hiding that precious buffer size limitation.&lt;/p&gt;
&lt;h2&gt;Example Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using HarmonyLib;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using Object = UnityEngine.Object;

namespace UndoHijack
{
    public static class UndoHijackSys
    {
        private static Harmony _harmony;

        [MenuItem(&quot;Help/Hacks/Hijack Undo&quot;)]
        [InitializeOnLoadMethod]
        public static void Init()
        {
            _harmony = new Harmony(&quot;UndoHijack&quot;);

            var original = typeof(Undo).GetMethod(&quot;RegisterCompleteObjectUndo&quot;,
                new[] { typeof(UnityEngine.Object[]), typeof(string) });
            var prefix = typeof(UndoHijackSys).GetMethod(&quot;HijackTexturePaintingUndo&quot;,
                BindingFlags.NonPublic | BindingFlags.Static);
            _harmony.Patch(original, prefix: new HarmonyMethod(prefix));
        }

        internal class UndoElement: IDisposable
        {
            public int instanceId;
            public Texture2D values;

            public void Dispose()
            {
                Object.DestroyImmediate(values);
            }
        }

        public const int maxUndo = 6;
        private static List&amp;lt;UndoElement[]&amp;gt; _undo = new();

        private static bool HijackTexturePaintingUndo(UnityEngine.Object[] objectsToUndo, string name)
        {
            if (name != &quot;PaintTextureTool - Texture&quot;) return true;
            if (_undo.Count &amp;gt;= maxUndo)
            {
                var excessiveStep = _undo[0];
                _undo.RemoveAt(0);
                foreach (var element in excessiveStep)
                {
                    element.Dispose();
                }
            }

            _undo.Add(objectsToUndo.Select(x =&amp;gt; CreateUndoStep(x as Texture2D)).ToArray());
            Debug.Log($&quot;Adding undo ({objectsToUndo.Length})&quot;);
            return false;
        }

        private static UndoElement CreateUndoStep(Texture2D t2d)
        {
            var result = new UndoElement();
            result.instanceId = t2d.GetInstanceID();
            var width = t2d.width;
            var height = t2d.height;
            result.values = new Texture2D(width,height);
            Graphics.CopyTexture(t2d, result.values);
            return result;
        }

        [MenuItem(&quot;Help/Hacks/Alt Undo &amp;amp;z&quot;)]
        private static void Undo()
        {
            if (_undo.Count &amp;lt; 1)
            {
                Debug.LogWarning(&quot;Texture paint undo threshold reached.&quot;);
                return;
            }
            var stepToUndo = _undo[^1];
            var step = stepToUndo.ToDictionary(x =&amp;gt; x.instanceId, x =&amp;gt; x.values);
            var scene = EditorSceneManager.GetActiveScene();
            var terrains = scene.GetRootGameObjects()
                .SelectMany(x =&amp;gt; x.GetComponentsInChildren&amp;lt;Terrain&amp;gt;()).ToList();
            foreach (var t in terrains)
            {
                /*
                 *  Conclusion after conducting experiments:
                 *  A terrain-data&apos;s Alphamap Texture&apos;s instance id does not change.
                 *  BUT! These Alphamaps are created on the fly in the get method.
                 */
                // Only compare the first. If the first doesn&apos;t match, none will match.
                var allAlphamapTexures = t.terrainData.alphamapTextures;
                var alphamapTexture = t.terrainData.alphamapTextures[0];
                var id = alphamapTexture.GetInstanceID();
                if (!step.ContainsKey(id)) continue;
                // write back
                var width = t.terrainData.alphamapWidth;
                var height = t.terrainData.alphamapHeight;
                var map = new float[width, height, t.terrainData.terrainLayers.Length];
                var counter = 0;
                foreach (var texture in allAlphamapTexures)
                {
                    var instanceID = texture.GetInstanceID();
                    var undoTexture = step[instanceID];
                    WriteToArray(undoTexture, map, width, height, counter);
                    counter++;
                }

                t.terrainData.SetAlphamaps(0, 0, map);
            }

            foreach (var element in stepToUndo)
            {
                element.Dispose();
            }
            _undo.RemoveAt(_undo.Count - 1);
        }

        private static void WriteToArray(Texture2D t2d, float[,,] map, int width, int height, int layer)
        {
            var res = t2d.GetPixels();
            var result = Parallel.For(0, width, i =&amp;gt;
            {
                for (int j = 0; j &amp;lt; height; j++)
                {
                    map[i, j, layer * 4 + 0] = res[i * width + j].r;
                    map[i, j, layer * 4 + 1] = res[i * width + j].g;
                    map[i, j, layer * 4 + 2] = res[i * width + j].b;
                    map[i, j, layer * 4 + 3] = res[i * width + j].a;
                }
            });
        }

        
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Loading Raw 3D Models in Runtime</title><link>https://fukafukaseika.moe/posts/runtime-model-loading/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/runtime-model-loading/</guid><description>An insight for loading 3D models in Unity using the gltf format in runtime.</description><pubDate>Sat, 18 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This was a strange thing to tackle...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Attempt to import FBX is going to be a pure fantasy. OBJ, okay, but lacking advanced features. Your best pal would be &lt;strong&gt;GLB&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Now, explanations.&lt;/h2&gt;
&lt;p&gt;.glb/.gltf is a rather new format (compared to old fossil like obj), that is designed to be &quot;the jpeg for 3D models&quot;. And indeed it is. It is lightweight, can have material/texture/animation info, and even lights and cameras (if... that matter). I would recommend conduct some further reading on this format.&lt;/p&gt;
&lt;p&gt;But why are there 2 extensions? Well, .gltf isn&apos;t a standalone file, it consists of a gltf file in json format, and a data file in binary format. .glb however, packs everything in one file (b for binary). And of course we would be interested in .glb due to its portability. gltf can come in handy if you really want to debug your model for some reason.&lt;/p&gt;
&lt;p&gt;Now, how does Unity work with gltf/glb?&lt;/p&gt;
&lt;p&gt;It is officially supported by Unity via the &lt;strong&gt;glTFast&lt;/strong&gt; package (&lt;a href=&quot;https://docs.unity3d.com/Packages/com.unity.cloud.gltfast@6.3/manual/index.html&quot;&gt;link&lt;/a&gt;). It isn&apos;t well-known since 1. it isn&apos;t in package manager and 2. gltf is a format for presentation, not really for editing.&lt;/p&gt;
&lt;p&gt;I tested runtime import of glb files and to my surprise, not only it works, it also creates correct materials and animations... which isn&apos;t possible with obj.&lt;/p&gt;
&lt;p&gt;And most software support gltf/glb. Blender, for example, does it perfectly. And I tested Houdini by snatching a friend&apos;s computer. It can handle it partially, but if you try to animate an attribute that you created yourself... it will not work, Unity complains about blend shape weights. But if you simply import the glb in Blender and do nothing, export it, it will actually make Unity happy. The downside is that the glb would grow about 3 times in size; I suppose something hacky done by Houdini is reverted to a simpler state by Blender upon import.&lt;/p&gt;
&lt;h2&gt;Something about Animations&lt;/h2&gt;
&lt;p&gt;When you have animations in glb files, import it in the editor creates animations, and loading the asset creates a model instance with Animator. However, this is not the case when you import a model in runtime. There are two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the glTFast package defaults legacy animation system. and&lt;/li&gt;
&lt;li&gt;You can&apos;t create animator controller in runtime.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I actually tried to force glTFast to use Mechanim import instead of legacy, but it doesn&apos;t solve the animator controller problem, as there&apos;s no information in glb files to author the creation of animator controller.&lt;/p&gt;
&lt;p&gt;My final solution is to overthrow the animator controller and rewrite it using the &lt;em&gt;Playables API&lt;/em&gt;. And I had to use a manual update to control the playable graph because imported animations are one-shot. I had to manually control the &quot;elapsed time&quot;. Hopefully I will explain how this system is built in a future post.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/runtime-model-loading/runtime-model-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Pixel Perfect Outline in Lo-Res</title><link>https://fukafukaseika.moe/posts/pixel-perfect-outline-1/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/pixel-perfect-outline-1/</guid><description>Learn how to create pixel perfect outline for Unity games using screen space edge detection, while taking transparent objects into account.</description><pubDate>Mon, 04 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Pixel Perfect outlines for sprites aren&apos;t hard, but for 3D, I&apos;ve been through the darkest corners.&lt;/p&gt;
&lt;p&gt;BUT WAIT, you may say, why would it be hard? After all, there are so many articles out there who demonstrate how outline could be done on 3D meshes. Whether it is extruding the normals, screen space edge detection, there got to be an easy way to do this.&lt;/p&gt;
&lt;p&gt;Well, I am a little peculiar when it comes to trace outlines. You see, I am not satisfied with the contour; I need hard edges that are inside the contour. Usually, this can be done by sampling the normal map.&lt;/p&gt;
&lt;p&gt;But.... I have transparent objects. And they don&apos;t write to screen normals.&lt;/p&gt;
&lt;p&gt;This causes 2 problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Any line drawn based on screen normals but masked by transparent objects is still drawn. Which isn&apos;t visually appealing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Without normals we lose the clarity of outlines on transparent objects. In fact, with how renderers are normally set up, we don&apos;t even have depth. This would result in transparent objects rely solely on color.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Just before we delve deeper, let me explain why I can&apos;t use the normal extrusion method. If you have no idea what that is, we draw an object with vertices extruded in the direction of vertex normals, and give it the color of the outlines. This is basically doing outlines for 2d sprites - sample the original sprite with 1px offset in each direction, effectively extruding the sprite. The method requires smooth and watertight meshes, otherwise it would cause broken looking outlines, which is something I can&apos;t impose based on the game. Also, normal extrusion can&apos;t (afaik) achieve screen space pixel perfectness, which is a significant drawback.&lt;/p&gt;
&lt;p&gt;Now, back to our subject.&lt;/p&gt;
&lt;h3&gt;Edge Detect&lt;/h3&gt;
&lt;p&gt;When I first started, I used the method that I am most familiar with: Edge detection. I stumbled across an article that uses Roberts kernel on depth and normal map, which has a problem at acute viewing angles since that causes depth value to change dramatically. The author compensated that by using the screen normals - the more perpendicular the screen normal is to the view vector, the less weight should be put on the normal value.&lt;/p&gt;
&lt;p&gt;I implemented the Roberts kernel, but also a Laplacien kernel, which I use more often in computer vision projects. The result is interesting, as the Laplacien kernel does not have the acute viewing angle artifacts that are present when using Roberts kernel. Aside from that, there&apos;s no significant difference in outline quality. The Laplacien kernel is heavier though, because it is a 3x3 kernel while Roberts is 2x2. However, considering we don&apos;t need to apply view angle adjustments and we are in low resolution, I&apos;d say it is a gain.&lt;/p&gt;
&lt;p&gt;Here I am only showing Laplacien, as I have deleted everything about Roberts from my shader.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Laplacien Edge Detect:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/_posts/2023-09-23-SG-Convolution.md&quot;&gt;This post&lt;/a&gt; explains the building blocks of this convolution node.&lt;/p&gt;
&lt;p&gt;Edge detection on normals texture comes with a natural problem when we are in low resolution though, when we detect a sudden change in value, two color channels are affected, with a displacement of 1px. This is not prominent in high resolution, but in low resolution, 2px is easily told apart from 1px. I haven&apos;t come up with a way to solve this but perhaps later.&lt;/p&gt;
&lt;p&gt;Anyway I did this for color, depth and screen normals.&lt;/p&gt;
&lt;h3&gt;Retrieve Depth and Screen Normals (Shader Graph)&lt;/h3&gt;
&lt;p&gt;Depth is straightforward, we need the &lt;strong&gt;SceneDepth&lt;/strong&gt; node. But since it takes UV and outputs depth value directly, we can&apos;t use the same graphs as with the color texture. Feasible but tedious.&lt;/p&gt;
&lt;p&gt;For normals, we need the name of a global variable, which is &lt;code&gt;_CameraNormalsTexture&lt;/code&gt;. Create a &lt;code&gt;Texture2D&lt;/code&gt; variable and name it &lt;code&gt;CameraNormalsTexture&lt;/code&gt;, set the scope to &lt;code&gt;Global&lt;/code&gt;, and &lt;strong&gt;ticking off &lt;em&gt;Show In Inspector&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It can now be sampled as any other texture, but here&apos;s a trap: Unity stores normal vectors as &lt;strong&gt;Vector2&lt;/strong&gt; (r,g) instead of Vector3 (r,g,b). This is because normal vector is a unit vector ( magnitude = 1 ), thus it has only 2 degrees of freedom. The third component of a normal vector can be calculated with the 2 known by using &lt;em&gt;Pythagorean Theorem&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Note on Depth and Screen Normals Texture&lt;/h3&gt;
&lt;p&gt;The shader I am developing was previously a full-screen effect. Later I discovered that I only need to render 3D objects in a more constrained area of the screen, thus it is turned in to an effect that applies to 2D textures. I still conserved the ability to apply it full-screen though, as a matter of fact, it is just the difference between using &lt;code&gt;_MainTex&lt;/code&gt; or &lt;code&gt;_CameraSortingLayerTexture&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But what about depth and screen normals? Turned out that these two, if I render the result to a render texture, will have size equal to that render texture. And if we don&apos;t have other things that write to depth and screen normals in different size, they will always remain of that size.&lt;/p&gt;
&lt;p&gt;This is what happens if we render the 3D objects to a 200x200 render texture while enabling depth and normals based outline, using the &quot;full screen&quot; mode of the shader:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We can see that only the color based contours are correct, because the screen color texture is of the correct size, but depth and normals based contours are like stretching a 200x200 texture to full screen.&lt;/p&gt;
&lt;h3&gt;Intermezzo: Camera Sorting Layer Texture&lt;/h3&gt;
&lt;p&gt;I write this because a funny problem has occurred to me twice, each time I have no idea what is happening in the first 10 minutes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is simply due to the fact that the shader using the camera sorting layer texture renders to it as well.&lt;/p&gt;
&lt;p&gt;For example, this is the canvas containing a shader that uses the camera sorting layer texture.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And in the URP settings, the foremost camera sorting layer texture is UI.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-8.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The solution is therefore to make sure everything using the camera sorting layer texture should be put in the layer above the foremost sorting layer texture. In this case, OverlayA.&lt;/p&gt;
&lt;h3&gt;Transparent Objects&lt;/h3&gt;
&lt;p&gt;Now here is a sort of like untraveled hinterland, because not much attempt has been made on drawing contours/structure lines on transparent objects.&lt;/p&gt;
&lt;p&gt;The difficulty, as I have explained earlier, is that transparent objects don&apos;t write to depth and screen normals by default. It is, however, possible to force write to depth buffer.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Default (transparent objects don&apos;t write to depth buffer)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-9.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Write Depth with Less comparison&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-10.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It looks... fine. But there&apos;s reason why transparent object don&apos;t write to scene depth by default.&lt;/p&gt;
&lt;p&gt;Here is a scene where a transparent object is not in front of the cube which looks correct - we are only seeing the cube&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-11.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now if we take another transparent object in front of the cube, miracle happens, the transparent object behind the cube is showing up...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-12.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As I tested many other options, there is no depth write option that ensures a correct perspective under all circumstances.&lt;/p&gt;
&lt;p&gt;My current approach, unfortunately, is to rely only on color for transparent objects. It looks OK, with the only problem that contours that should be obscured by transparent objects are showing through.&lt;/p&gt;
&lt;p&gt;What if... we are able to mark all pixels that are affected by transparent objects?&lt;/p&gt;
&lt;p&gt;My first attempt was to add another camera that renders transparent objects only, to a render texture. But that doesn&apos;t work, as we are not able to detect any opaque objects in front of transparent objects. And the ordering of the renderer is strange, if below normal 3D renderer it would clear depth and normals texture, and if above, it clears color.&lt;/p&gt;
&lt;p&gt;After days struggling with URP&apos;s internal access modifiers, I finally did it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-13.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I&apos;ve created a global texture called &lt;code&gt;_CustomColorTexture&lt;/code&gt; that can be accessed from shaders, and the &lt;strong&gt;Custom Render Objects Pass&lt;/strong&gt; renders objects to that texture (only color, we still use depth texture form main camera, but since transparent objects don&apos;t write depth, it is effectively read-only).&lt;/p&gt;
&lt;p&gt;It isn&apos;t magic though, I simply copied the built-in &lt;strong&gt;RenderObjectsPass&lt;/strong&gt;, fix access modifier problems with cached reflections, and replaced render attachment with the self-allocated render texture (allocating with &lt;code&gt;RTHandles.Alloc(...)&lt;/code&gt; and import with &lt;code&gt;renderGraph.ImportTexture(...)&lt;/code&gt; ).&lt;/p&gt;
&lt;p&gt;Bind to global shader variable is done by using &lt;code&gt;builder.SetGlobalTextureAfterPass(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Unity is shifting towards Render Graph, a technology with currently very poor documentation. I just hope my code still works after the release of Unity 6... I might try to write something about it later.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Please Unity, be open (public)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-14.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: And indeed, one of them became publicly accessible in Unity 6.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With this obstacle cleared the rest should be relatively simple.
&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-15.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The contour lines obscured by transparent objects are no longer showing through.&lt;/p&gt;
&lt;h3&gt;Finale&lt;/h3&gt;
&lt;p&gt;I end this article here because I think all the necessary puzzle pieces are in place. With these we can easily enhance the quality of transparent object&apos;s outline using the custom color texture, dither the contour line based on how many layers of transparent objects are in front of that object, and other refinements if required.&lt;/p&gt;
&lt;h3&gt;IMPORTANT UPDATE&lt;/h3&gt;
&lt;p&gt;Past Unity 6000.0.5f1, &lt;code&gt;_CameraNormalsTexture&lt;/code&gt; no longer works as intended. This is because &lt;strong&gt;GBuffers are no longer global.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/pixel-perfect-outline-1/ppo-16.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I temporarily solved it by adding a pass after deferred lighting that copies GBuffer2 to a persistent texture. &lt;em&gt;Now think of it, I should just declare using the GBuffer2 and then the Render Graph will solve this issue for me&lt;/em&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Multi-pass Convolution with Shader Graph</title><link>https://fukafukaseika.moe/posts/shader-graph-convolution/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/shader-graph-convolution/</guid><description>In this article I will demonstrate how to use Custom Render Texture and Shader Graph to achieve multi pass convolution - effect like gaussian blur, edge detection, etc.</description><pubDate>Sat, 23 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: This article requires you to already know what convolution in image processing is.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: Check out &lt;a href=&quot;https://github.com/Myonmu/ShaderUtils&quot;&gt;this repo&lt;/a&gt; which features the assets mentioned in this blog post.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Convolution, being the most essential and useful technique in image processing, can create a range of effects, such as Gaussian blur, edge detection, sharpening, and so on.&lt;/p&gt;
&lt;p&gt;The most standard implementation of a multi-pass convolution effect is by using custom render features. (well, essentially, the convolution is still implemented inside a shader). But here I would like to take a step further, to see if it is possible to do something similar as what I did with the Kawase blur effect - limiting the effect to a certain zone instead of a global post processing that covers the whole screen, and can be resized with an animation.&lt;/p&gt;
&lt;p&gt;And with the help of Custom Render Texture, it has been done and is very modular.&lt;/p&gt;
&lt;h2&gt;Implementation Detail&lt;/h2&gt;
&lt;p&gt;First, a custom render texture, just like render texture, can hold the result of a convolution operation and be fed to another as input. The documentation states that by setting a custom  render texture as the initial texture of another, Unity will handle the update order. With this in mind, what left to do will be to create a custom render texture for each pass and assign a convolution operation (material) to each of them.&lt;/p&gt;
&lt;p&gt;Implementing convolution is simply sampling the input texture with an offset and then multiply it by a kernel value. For a 3x3 kernel, that means to sample in a tic-tac-toe pattern, and then sum the cells up.&lt;/p&gt;
&lt;p&gt;the following image shows the &quot;Convolve Cell&quot; sub-graph, noting that it also expects UV so that you can customize whether to use texture UV0 or screen position.&lt;/p&gt;
&lt;p&gt;The channel mask is to select which channel or perceptual grey scale to use when running the convolution.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/rg-convolution/conv-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Therefore, it is straightforward to construct a 3x3 convolution node.&lt;/p&gt;
&lt;p&gt;And with perhaps a little bit of work, a 5x5 or even 7x7 convolution node. But unfortunately Unity only supports up to Vector4 thus making a proper editor can be potentially demanding.&lt;/p&gt;
&lt;p&gt;(Oh, by the way, Matrix3x3 is not available for the inspector, thus I used 3 Vector3 instead)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/rg-convolution/conv-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Support for Custom Render Texture in Shader Graph seems to be a very new thing, because the documentation is still stated that you can only write shader for it with code. But that needs to be updated as you can already find &quot;Custom Render Texture&quot; in the shader graph creation menu.&lt;/p&gt;
&lt;p&gt;Well anyway, since we already have the convolution node, it is faster to create a shader for custom render textures. Strangely, I hoped that there is a magic property like &lt;code&gt;_MainTex&lt;/code&gt; that automatically binds the custom render texture to that property, but I&apos;ve tried and nothing worked.&lt;/p&gt;
&lt;p&gt;Finally, create materials for common kernels, like the one showing on the right.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/rg-convolution/conv-4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Extra&lt;/h2&gt;
&lt;p&gt;I&apos;ve created a little script that helps to ease the pain of configuring the pipeline. By clicking Configure after setting everything else up, it will automatically create custom render textures, copy the materials, and link them in top-to-bottom order. This is currently written with Odin. The created assets will become sub-assets of the Scriptable Object.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/rg-convolution/conv-5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;An example of what happens if you click the Configure button. Each Pass N will result from each pass. Fetch the last one to get the final result.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/rg-convolution/conv-6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: For a more code-oriented approach, check out &lt;a href=&quot;https://github.com/Myonmu/ImageProcessingTools&quot;&gt;this repo&lt;/a&gt; which does the same thing but without the hassle of setting up custom render textures. And honestly, this is the lib I have been using practically.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Irregular Puzzles</title><link>https://fukafukaseika.moe/posts/irregular-puzzles/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/irregular-puzzles/</guid><description>Building a puzzle game that simulates putting together shredded paper pieces can be tricky, especially when dealing with merging and moving pieces. In this post, I share my approach to implementing a drag-and-drop system, managing piece merging, and handling the puzzle completion logic in Unity.</description><pubDate>Thu, 01 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have seen many puzzle games that use a mechanism that simulates putting together pieces of shredded paper... It is a simple mechanism, but when actually implementing it, it can give you a slight headache...&lt;/p&gt;
&lt;p&gt;First, what are the use cases such a system needs to achieve?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Drag around pieces in a limited area.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When two pieces of matching edges connect, they are &quot;merged&quot;, and further dragging should treat those pieces as one single piece.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Merged pieces should still be inside the limited area&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The game should know when the puzzle is completed.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The difficulty is mostly in the 2nd use case, since &quot;detecting edges&quot; like in computer vision is not feasible, and you also need to keep track of different groups of merged pieces... also, imagine you have a group of 2 pieces and another group of 2 pieces, you will need to merge the two merged pieces as a whole...&lt;/p&gt;
&lt;p&gt;So, here is my non-perfect approach...&lt;/p&gt;
&lt;h2&gt;I. Drag and Drop&lt;/h2&gt;
&lt;p&gt;I am a faithful Input System user, so when I started the project, I went for Input System without second thoughts. However, when I started implementing it, Input System fell short... at least not intuitive enough as the old system. Going though the setup of Actions as button based games can be painful, so I searched elsewhere...&lt;/p&gt;
&lt;p&gt;The final answer is to make whatever you want to drag and drop, to implement the following interfaces:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IPointerDownHandler&lt;/code&gt; (When you press the left mouse button)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IBeginDragHandler&lt;/code&gt; (When mouse button is held and you start to move the mouse)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IEndDragHandler&lt;/code&gt; (When you stop dragging)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IDragHandler&lt;/code&gt; (When you are actually dragging)&lt;/p&gt;
&lt;p&gt;Like the old system, these handlers are triggered by raycasting on a collider, so you will need to attach a Collider2D to the object.&lt;/p&gt;
&lt;p&gt;There is still one thing you need to add: a Physics 2D Raycaster to your camera.&lt;/p&gt;
&lt;p&gt;Ok, now about those handlers. They are actually from the UI system, so the &lt;code&gt;PointerEventData&lt;/code&gt; that is passed as a parameter can be confusing... I have tried to use the mouse position in that data, but it gives ridiculously huge values even after applying &lt;code&gt;ScreenToWorldPoint&lt;/code&gt;...&lt;/p&gt;
&lt;p&gt;Well, use mouse position of something else, and that is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Mouse.current.position.ReadValue();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can use &lt;code&gt;Camera.main.ScreenToWorldPoint()&lt;/code&gt; to convert this value to world position and use it to update the position in your &lt;code&gt;OnDrag&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;II. Limiting Movement&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/irregular-puzzle/irregular-puzzle-bounds.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The most common practice is to use &lt;code&gt;Mathf.Clamp()&lt;/code&gt; to restrict the position. However, it needs information about the delimiting area. It can be done by directly supply values of borders, but for me I usually want something more visual...&lt;/p&gt;
&lt;p&gt;My method of choice is to create a dedicated class to store this information, and in most cases, the delimiting area is a rectangle. This class will expose the 4 values of the borders (up, down, left, right), and retrieve this information in its &lt;code&gt;Awake()&lt;/code&gt;. To define this information, it needs to keep reference of two Transforms, one representing the upper left corner, and the other the lower right corner. After reading the corresponding values, these two objects will be set inactive.&lt;/p&gt;
&lt;p&gt;The purpose of doing this is that you can reposition the two corners with drag and drop in editor. You can add a &lt;code&gt;OnDrawGizmos()&lt;/code&gt; like I did to visualize the borders.&lt;/p&gt;
&lt;h2&gt;III. Determine the Correct Position&lt;/h2&gt;
&lt;p&gt;As stated before, when determine if two pieces would fit together, it is not feasible to judge based on the shape of the edges. Instead, the viable method that I found is to record the relative position of each of the two adjacent pieces when they are in a completed state. This method is certainly not perfect, because the more pieces you have, the more information you need to record. In my case, 6 pieces require 10 adjacency information to well define the puzzle.&lt;/p&gt;
&lt;p&gt;Each adjacency information will contain 3 information: 2 references to the involved pieces, and a Vector2 that represents the relative position (&lt;code&gt;transformB.position - transformA.position&lt;/code&gt; when in connected state). Now, with a context menu function or custom editor call, you should be able to generate the relative position fairly easily. The hard work is how to reduce the pain of assigning references...&lt;/p&gt;
&lt;p&gt;The trick that I found is to use Raycasting. It does NOT guarantee the detection of all adjacencies but will significantly reduce the need for manual operation.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/irregular-puzzle/irregular-puzzle-raycast.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The idea is, for each pair of two pieces A and B, draw a line (&lt;code&gt;RaycastAll()&lt;/code&gt;) from A to B and see if the second hit collider is on B. Why second? because the first hit will always be A.&lt;/p&gt;
&lt;p&gt;This will work most times, EXCEPT if the contacting point is not on the drawn ray (like A and B in the graph above). To reduce these exceptional cases, you can try to sample multiple points on the same pair and see if at least one raycast is successful. Again, even if you do so, it will still not guarantee that you detect all the adjacencies, as a well-designed counter-example can easily make your effort useless, though such a case is very rare.&lt;/p&gt;
&lt;p&gt;Besides that, I suggest adding &lt;code&gt;OnDrawGizmos&lt;/code&gt; to the class by storing the adjacency information to visually show the adjacency so that it is easier to detect missing adjacencies.&lt;/p&gt;
&lt;p&gt;To sum up, when you have properly created your adjacency recording system, first you need to finish the puzzle in the editor manually (so, if you have a large amount of pieces that can be problematic), and use the system to generate potential adjacency information, spot any missing adjacencies and add them manually, and finally use the system again to calculate the relative position in each adjacency.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/irregular-puzzle/irregular-puzzle-inspector.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;To determine if two pieces should merge during runtime, first you need to know which piece is being dropped, and then verify for each adjacency information if the piece is involved. If it is involved (say, it is pieceB), calculate the absolute position based on the position of the other piece (pieceA), and if the dropped piece is in a certain range (Tolerance) of the absolute position, they are merged. Modify the position of the dropped piece directly to the absolute position, and delete the adjacency information (it is not useful anymore).&lt;/p&gt;
&lt;p&gt;To know which piece is being dropped in the adjacency storage class, my approach is to create an &lt;code&gt;Action&amp;lt;DragAndDrop&amp;gt; Drag&lt;/code&gt; in the DragAndDrop class and invoke the action in &lt;code&gt;Drag()&lt;/code&gt;. Since in the adjacency class you have reference to all pieces, simply subscribe to your merge verification of that action.&lt;/p&gt;
&lt;p&gt;Deleting information after successful merge has two effects: (1) reduce the search time for further merge attempts, and (2) when the list of adjacency information is empty, you know all pieces are connected and the puzzle is complete.&lt;/p&gt;
&lt;h2&gt;IV. Move Pieces as One&lt;/h2&gt;
&lt;p&gt;Now here is another tricky part. I have actually thought about two methods; I implemented the first, and the second is purely theoretical and I will not really discuss it. I will leave the judgement of which one is better for you.&lt;/p&gt;
&lt;h3&gt;The First Method&lt;/h3&gt;
&lt;p&gt;This method is straightforward: you store all pieces linked to the piece you are moving in a list, and move them too. So each drag-and-drop object will also keep a list of references to the linked pieces. There are two critical problems that need to be solved though: (1) how to propagate the information when a new piece or group of pieces is being merged into a group? (2) how to calculate the correct displacement to ensure all the pieces do not exceed the delimiting area and keep the global shape of the group unchanged?&lt;/p&gt;
&lt;p&gt;For the first (1) point: The process is actually like mathematical induction. We first assume the piece that should be linked contains all pieces currently linked to that piece (excluding itself). Then, follow the following steps illustrated below:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/irregular-puzzle/irregular-puzzle-merged-move.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Basically, you construct two lists of linked objects in each group, and for each object in one list, you link it with each object in the other list.&lt;/p&gt;
&lt;p&gt;The presence of a group of pieces means you will also need to change something when you detect merges (Step III.). You will also need to check if there is a merge for each linked piece of the dropped piece, otherwise you will have undeleted adjacency in your list even if visually the puzzle is complete.&lt;/p&gt;
&lt;p&gt;For the second (2) point: You cannot apply movement directly to each piece. Instead, you need to separate the Attempt to move the piece and actually Moving the piece. The Attempt returns a displacement of the piece, which can be less than the actual mouse delta since it can be truncated by the delimiting area. When you drag the piece under your mouse cursor, you gather all the displacement returned by the move attempts of all the linked pieces, as well as the piece itself. You can then take the shortest displacement to ensure that no piece will exceed the borders and call the actual Moving function with this shortest displacement provided as a parameter.&lt;/p&gt;
&lt;h3&gt;The Second Method&lt;/h3&gt;
&lt;p&gt;So this method delegates the moving part to Unity. My thought is that you can re-parent the linked pieces to an object created at runtime, and whenever you click a piece in a group, the movement is redirected to the parent object (you can do this by using delegates).&lt;/p&gt;
&lt;p&gt;However, when rethinking this potential idea, it can get a little messy, and since my first method is functioning, I quickly abandoned the second method...&lt;/p&gt;
</content:encoded></item><item><title>Weapon Trail</title><link>https://fukafukaseika.moe/posts/weapon-trail/</link><guid isPermaLink="true">https://fukafukaseika.moe/posts/weapon-trail/</guid><description>Creating a weapon trail effect in 3D is far more complex than in 2D, especially when considering how the slash plane changes based on animation. In this post, I’ll share a method I used to generate 3D weapon trails in Unity using mesh generation and interpolation techniques.</description><pubDate>Mon, 20 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is an effect I have been doing a lot in 2D, but in 3D it adds many complications, notably the plane of slash is no longer a flat plane and it varies a lot depending on the animation, which caused my usual method of using a pre-made mesh + shader + vfx to render the trail unusable.&lt;/p&gt;
&lt;p&gt;With a little digging, I found a method to create the effect in 3D. But I have to say this method is not at all simple and took me 3 days of research to achieve the result.&lt;/p&gt;
&lt;p&gt;In fact, in this method, (which I believe should be the same method used in the classic asset X Weapon Trail by watching the presentation video), you will create the mesh in runtime, and apply a material to render it). Most of the hard work really is in the part of creating a mesh, and I will explain why it can cause a headache.&lt;/p&gt;
&lt;h2&gt;Mesh Generation&lt;/h2&gt;
&lt;p&gt;Introducing the two most important components: &lt;strong&gt;Mesh Filter&lt;/strong&gt; and &lt;strong&gt;Mesh Renderer&lt;/strong&gt;. Mesh Filter is actually very simple, it keeps a reference to a mesh and allows the mesh renderer to render the mesh. The &lt;strong&gt;Mesh Renderer&lt;/strong&gt; keeps information about rendering details (notably the material). With these two components set up, it is possible to assign a mesh created at runtime to the Mesh Filter (&lt;code&gt;MeshFilter.mesh&lt;/code&gt;) and get it rendered.&lt;/p&gt;
&lt;p&gt;However, there is a little twist.&lt;/p&gt;
&lt;p&gt;When I first try this method, I created the two components in edit mode and reference them in my script. The object (I call it &lt;em&gt;Trail Object&lt;/em&gt;), is a child of the weapon. This will create strange results, since render result will be originated from the Mesh Filter&apos;s position, plus any rotation applied to it. To avoid such complications, it is better to create the object in Start and leave it in the root hierarchy (parent to nothing). Since it is in &lt;code&gt;Start&lt;/code&gt;, the overhead can be ignored.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/weapon-trail/weapon-trail-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now, to construct the mesh, we start with something easy. A mesh should at least contain 2 basic informations: Vertices, an array of Vector3, and Triangles (sometimes called Indices), an array of int. If you don&apos;t already know, the computer only draws triangles (so if you have a rectangle, the computer draws 2 triangles). Vertex (singular for Vertices), is the point on the corners of a triangle, and an Index (singular for Indices), is a way to tell the renderer how to draw the vertices (how should the triangle be constructed from the vertices).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/weapon-trail/weapon-trail-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Observe the example here above. If your Vertices contain {V1, V2, V3}, to draw a triangle following the order V1=&amp;gt;V2=&amp;gt;V3, you would put {0,1,2} in your Indices array. The number in indices array corresponds to the vertex&apos;s index number in Vertices array.&lt;/p&gt;
&lt;p&gt;For each triangle, you need 3 vertices to signify their corners, and actually 6 indices to correctly draw the triangle. This is because if you only draw your triangle in one order, your triangle will be single-sided. That means when the camera is not facing the triangle&apos;s normal (vector perpendicular to the triangle&apos;s plane), the triangle will not be rendered. The direction of the triangle is dependent according to which order you draw your triangle. So, in our case, to make the triangle visible in any angle, the triangle should be drawn in both clockwise and counter-clockwise.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/weapon-trail/weapon-trail-4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With that in mind, we can start working on our mesh. We will assume the weapon is a straight sword, and the blade can be described with two points: base and tip. I call the segment formed by base and tip stem. For each frame, base and tip will be updated by animation, and if we keep track of the history positions, we should have a list of stems. By connecting adjacent tips and bases, we will have a zig zag plane that describes the trail of the weapon. Now, since each pair of adjacent stems contains 4 vertices, we should draw 4 triangles (2 unique shapes * 2 directions to ensure that the mesh is double sided), that means 12 indices for each pair.&lt;/p&gt;
&lt;p&gt;For now, it is relatively easy to implement. You will also need to define a maximum frame length so that when a stem&apos;s life exceeds this limit, it is discarded and replaced by a fresh stem (this looks like a Queue, but I did it manually with a List since it is easier to iterate). By defining this maximum frame length, you can also more easily allocate the space needed for arrays.&lt;/p&gt;
&lt;h2&gt;Interpolation&lt;/h2&gt;
&lt;p&gt;You may quickly find a problem: If an action is so quick that the two adjacent stems are far away from each other, your trail will not look good (sharp edges). To solve this, and this is also the hardest part, you will need to apply for an Interpolation Algorithm. What Interpolation means is that the algorithm generates stems to fill in the gaps between the two adjacent stems. And the generated stems will be positioned so that the edge is a curve instead of a straight line. I found an article that discuss some of the mechanism behind X Weapon Trail, and in the article, the Interpolation used is &lt;strong&gt;Catmull-Rom&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/weapon-trail/weapon-trail-5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A Catmull-Rom spline requires 4 points (P) to produce the interpolated point (Q) - 2 points prior to the interpolated point and 2 points after the interpolated point. It also requires a parameter u (in some reference, t ), which represents how far in the curve the interpolated point is. This can be defined by the proportion between the distance P1Q and P1P2 as shown above. The calculation is defined by a matrix multiplication:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/weapon-trail/weapon-trail-6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The tau value is called the tension of the curve, which directly affects how the resulting curve looks like. The value is normally 0.5.&lt;/p&gt;
&lt;p&gt;You may notice that P in the above function is a vector (it is bold). And since matrix multiplication is linear, you can apply this function to each component of the vector (x, y and z) to produce the resulting component.&lt;/p&gt;
&lt;p&gt;So you should be good to translate the function to code. The only possible issue is how would you produce the u value ?&lt;/p&gt;
&lt;p&gt;A possible solution is that you don&apos;t care about the actual position of the stems. You only use them to interpolate points. Your actual mesh will be separated by evenly spaced interpolated points:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/weapon-trail/weapon-trail-7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You define u before finding which 4 points should be used for interpolation. If you know the total length of your mesh (preferably the length of the zig-zag curve created by the middle points of the stems), you can divide this length by a user defined integer (resolution of the mesh) to have the spacing between each interpolated stem. Note that the step is not yet the parameter u. To demonstrate how the parameter u can be found, consider the following example:&lt;/p&gt;
&lt;p&gt;For instance, the fourth interpolated stem (red) has a distance of 3x step to the beginning of the mesh (stem0). Visually, this interpolated stem should be interpolated by using stem0, stem1, stem2 and stem3, and the value u is given by:&lt;/p&gt;
&lt;p&gt;$$ ( 3*step - dist( stem1 , stem0 ) ) / dist( stem2 ,  stem1 ) $$&lt;/p&gt;
&lt;p&gt;Where &lt;code&gt;dist()&lt;/code&gt; gives the distance between the two stems.&lt;/p&gt;
&lt;p&gt;So what you end up with is to find the stem P1, which should be the closest stem on the left (closer to the start of the mesh). By closest, you can define as the distance between the mid-points of the stems. If you keep a record of the stems&apos; distance to the first stem (the final one will contain the total length of your mesh), you can just iterate through the list and find the stem P1. And there you go you should have all the parameters needed for Catmull Spline.&lt;/p&gt;
&lt;h2&gt;Applying Material&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/assets/postimg/weapon-trail/weapon-trail-8.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you did all the above correctly, you should have something that works. But what about material? Well, you will need to specify the UV coordinates of the vertices.&lt;/p&gt;
&lt;p&gt;UV tells the renderer how to map the texture on your mesh, and it is an array of Vector2. Each vertex will have its own UV value. UV coordinates should be clamped between 0 and 1, with (0,0) the top left corner and (1,1) the bottom right corner. In our case, you need to know the distance between two adjacent tip or base vertices and the total length of your mesh, and you can calculate the relative proportion and use that as UV&apos;s horizontal value. For vertical, it is just 0 and 1 since the stem should cover the total vertical length.&lt;/p&gt;
&lt;p&gt;With all these settled, it is possible to create a powerful weapon trail plugin, but surely it demands dedication... I am still thinking about whether it can be fun to support extremely curved swords (which will need extra points to define the blade), or even whips (that can be diabolical).&lt;/p&gt;
</content:encoded></item></channel></rss>