{"id":190,"date":"2021-03-06T16:05:29","date_gmt":"2021-03-06T16:05:29","guid":{"rendered":"https:\/\/bronsonzgeb.com\/?p=190"},"modified":"2021-03-12T16:27:04","modified_gmt":"2021-03-12T16:27:04","slug":"particle-metaballs-in-unity-using-urp-and-shader-graph-part-2","status":"publish","type":"post","link":"https:\/\/bronsonzgeb.com\/index.php\/2021\/03\/06\/particle-metaballs-in-unity-using-urp-and-shader-graph-part-2\/","title":{"rendered":"Particle Metaballs in Unity using URP and Shader Graph Part 2"},"content":{"rendered":"\n<p>This post is part 2 in a series of articles that explain how to draw Metaballs in Unity using the Universal Render Pipeline (URP) and Shader Graph. Click&nbsp;<a target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/2021\/02\/27\/particle-metaballs-in-unity-using-urp-and-shader-graph-part-1\/\" rel=\"noreferrer noopener\">here<\/a>&nbsp;for part 1. This part describes:&nbsp;<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>How to send position and size data from a particle system to our shader.<\/li><li>How to write the Metaball shader function.<\/li><li>How to add lighting to our Metaballs.<\/li><\/ul>\n\n\n\n<p>Let&#8217;s get to work!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Sending data from a Particle System to the GPU<\/h3>\n\n\n\n<p>Compared to everything else, sending data from the Particle System to the GPU is relatively easy. Create a new <code>MonoBehaviour<\/code> called <em>MetaballParticleManager.cs<\/em> to start. This script will read information from the Particle System and pass it to the GPU. In this case, my preferred way to send the data is using a <code>MaterialPropertyBlock<\/code> because it&#8217;s easy to support multiple Particle Systems without juggling instanced materials. By the way, MaterialPropertyBlocks are exactly what they sound like: a block of data that holds material properties. The three things we need are a Particle System from which we read the data, a <code>MaterialPropertyBlock <\/code>into which we write the data, and a renderer to accept the <code>MaterialPropertyBlock<\/code>. So in our&nbsp;<em>MetaballParticleManager<\/em> we&#8217;ll create a reference to each of these. We&#8217;ll set them up&nbsp;<code>OnEnable&nbsp;<\/code>and clean them up&nbsp;<code>OnDisable<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ParticleSystem _particleSystem;\nRenderer _renderer;\nMaterialPropertyBlock _materialPropertyBlock;\n\nvoid OnEnable()\n{\n    _particleSystem = GetComponent&lt;ParticleSystem&gt;();\n    _materialPropertyBlock = new MaterialPropertyBlock();\n    _renderer = _particleSystem.GetComponent&lt;Renderer&gt;();\n}\n\nvoid OnDisable()\n{\n    _materialPropertyBlock.Clear();\n    _materialPropertyBlock = null;\n    _renderer.SetPropertyBlock(null);\n}<\/code><\/pre>\n\n\n\n<p>Next, in&nbsp;<code>Update<\/code>, we&#8217;ll get each of our particles&#8217; position and size to write to the <code>MaterialPropertyBlock<\/code>. By the way, we&#8217;re going to use a fixed-size array on the GPU to hold our particle data for the sake of simplicity. So we&#8217;ll need a constant to hold the maximum number of particles and send the current number of particles over to the GPU. Ok, back to&nbsp;<code>Update<\/code>. Getting access to the particle data is simple but a bit lengthy. First, we get the number of particles in the system, then we use&nbsp;<code>GetParticles&nbsp;<\/code>and pass in an array to store them in. The array is of size&nbsp;<code>MaxParticles<\/code>, and&nbsp;<code>GetParticles&nbsp;<\/code>takes a count as a parameter, so we also pass in&nbsp;<code>MaxParticles<\/code>. This way,&nbsp;<code>GetParticles&nbsp;<\/code>will only return the first 256 particles (in our case) rather than go out of the array bounds. We then store each particle&#8217;s position and size into their respective arrays and exit our&nbsp;<code>for<\/code> loop&nbsp;early if we reach the current number of particles. Finally, we set all these values in our <code>MaterialPropertyBlock <\/code>and pass the block to the renderer.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const int MaxParticles = 256;\nint _numParticles;\n\nParticleSystem.Particle&#91;] _particles = new ParticleSystem.Particle&#91;MaxParticles];\n\nreadonly Vector4&#91;] _particlesPos = new Vector4&#91;MaxParticles];\nreadonly float&#91;] _particlesSize = new float&#91;MaxParticles];\n\nvoid Update()\n{\n    _numParticles = _particleSystem.particleCount;\n    _particleSystem.GetParticles(_particles, MaxParticles);\n\n    int i = 0;\n    foreach (var particle in _particles)\n    {\n        _particlesPos&#91;i] = particle.position;\n        _particlesSize&#91;i] = particle.GetCurrentSize(_particleSystem);\n        ++i;\n            \n        if (i &gt;= _numParticles) break;\n    }\n\n    _materialPropertyBlock.SetVectorArray(ParticlesPos, _particlesPos);\n    _materialPropertyBlock.SetFloatArray(ParticlesSize, particlesSize);\n    _materialPropertyBlock.SetInt(NumParticles, _numParticles);\n    _renderer.SetPropertyBlock(_materialPropertyBlock);\n}<\/code><\/pre>\n\n\n\n<p>You&#8217;ll notice some variables I haven&#8217;t mentioned:&nbsp;<code>ParticlePos<\/code><strong><em>, <\/em><\/strong><code>ParticlesSize<\/code>,<strong><em>&nbsp;<\/em><\/strong>and&nbsp;<code>NumParticles<\/code>. These are the names of our shader properties converted into an ID. In the <code>MonoBehaviour<\/code> we declare them like so:&nbsp;<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>static readonly int NumParticles = Shader.PropertyToID(\"_NumParticles\");\nstatic readonly int ParticlesSize = Shader.PropertyToID(\"_ParticlesSize\");\nstatic readonly int ParticlesPos = Shader.PropertyToID(\"_ParticlesPos\");<\/code><\/pre>\n\n\n\n<p>And then, in our shader, we&#8217;ll declare the properties <code>_NumParticles<\/code>, <code>_ParticlesSize<\/code>, and <code>_ParticlesPos<\/code>. We&#8217;re telling the <code>MaterialPropertyBlock<\/code> how to map the data we&#8217;re supplying to variables in our shader. So then, add these lines to the top of our&nbsp;<em>Metaball.hlsl<\/em>&nbsp;shader:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#define MAX_PARTICLES 256\n\nfloat4 _ParticlesPos&#91;MAX_PARTICLES];\nfloat _ParticlesSize&#91;MAX_PARTICLES];\nfloat _NumParticles;<\/code><\/pre>\n\n\n\n<p>And with that, we&#8217;re done passing data from our Particle System to our shader. You can see the completed&nbsp;<em>MetaballParticleManager.cs<\/em> <a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/github.com\/bzgeb\/UnityURPMetaballParticlesPart2\/blob\/main\/Assets\/Metaballs\/Scripts\/MetaballParticleManager.cs\">here<\/a>&nbsp;and&nbsp;<em>Metaball.hlsl<\/em> <a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/github.com\/bzgeb\/UnityURPMetaballParticlesPart2\/blob\/main\/Assets\/Metaballs\/Shaders\/Includes\/Metaball.hlsl\">here<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">GetDistanceMetaball Function<\/h3>\n\n\n\n<p>At the end of the last post, we were drawing an SDF Sphere. Now we&#8217;re going to upgrade from SDFs to Metaballs. What&#8217;s the difference between an SDF sphere and a Metaball? Not that much in practice. They&#8217;re both implicitly defined sphere-shaped objects. Using SDF spheres with a blending function, we can recreate something visually identical to Metaballs. However, unlike SDF spheres that merge using a blending function, a Metaball&#8217;s density defines when they combine and split apart. You could say that the Metaball definition merges objects by default, whereas joining SDF-based objects is optional. Anyway, let&#8217;s get to the code. I won&#8217;t go over this code line-by-line. Instead, I&#8217;ll share the source from which I learned how to write it later in this post. To truly understand how Metaballs work, I recommend reading that source. Here&#8217;s the <code>GetDistanceMetaball <\/code>function:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>float GetDistanceMetaball(float3 p)\n{\n    float sumDensity = 0;\n    float sumRi = 0;\n    float minDistance = 100000;\n    for (int i = 0; i &lt; _NumParticles; ++i)\n    {\n        float4 center = _ParticlesPos&#91;i];\n        float radius = 0.3 * _ParticlesSize&#91;i];\n        float r = length(center - p);\n        if (r &lt;= radius)\n        {\n            sumDensity += 2 * (r * r * r) \/ (radius * radius * radius) - 3 * (r * r) \/ (radius * radius) + 1;\n        }\n        minDistance = min(minDistance, r - radius);\n        sumRi += radius;\n    }\n\n    return max(minDistance, (0.2 - sumDensity) \/ (3 \/ 2.0 * sumRi));\n}<\/code><\/pre>\n\n\n\n<p>You&#8217;ll notice that we&#8217;re using the data from the <code>ParticleSystem<\/code>. The gist of this is that for each pixel, we have it iterate through every particle. For each particle, we check if the given pixel has enough density to be within our mass of balls or not. If it&#8217;s not apparent already, rendering Metaballs is not cheap. I&#8217;ll show you some of the ways I tried to make them more performant later in the series. Let&#8217;s modify the&nbsp;<code>SphereTraceMetaballs<\/code> function in&nbsp;<em>Metaball.hlsl<\/em>&nbsp;to use this new function. Replace <code>GetDistanceSphere<\/code> on line 30 with <code>GetDistanceMetaball<\/code>. We no longer need to pass the position and radius because we have that data from the Particle System. So line 30 becomes: <\/p>\n\n\n\n<p><code>float d = GetDistanceMetaball(from);<\/code><\/p>\n\n\n\n<p>Now feel free to test it out. In your scene, create a new Particle System. Attach the&nbsp;<em>MetaballParticleManager<\/em> component. Ensure the Particle System Simulation Space is&nbsp;<em>World&nbsp;<\/em>rather than&nbsp;<em>Local<\/em>; otherwise, the particle positions we&#8217;re supplying to the shader will be wrong. Finally, change the material to our Sphere-Tracer material, and you&#8217;ll see Metaballs in your scene. It&#8217;s hard to know if they&#8217;re merging without lighting, so let&#8217;s add that next.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Lighting<\/h3>\n\n\n\n<p>To calculate lighting, we need normals. Let&#8217;s add a function to calculate the normals of our Metaballs. The explanation for this source code is available from the same reference as the&nbsp;<code>GetDistanceMetaball <\/code>function, which I&#8217;ll share later in this post. We&#8217;ll add this&nbsp;<code>CalculateNormalMetaball<\/code> function to the&nbsp;<em>Metaball.hlsl<\/em>&nbsp;file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>float3 CalculateNormalMetaball(float3 from)\n{\n    float delta = 10e-5;\n    float3 normal = float3(\n        GetDistanceMetaball(from + float3(delta, 0, 0)) - GetDistanceMetaball(from + float3(-delta, 0, 0)),\n        GetDistanceMetaball(from + float3(0, delta, 0)) - GetDistanceMetaball(from + float3(-0, -delta, 0)),\n        GetDistanceMetaball(from + float3(0, 0, delta)) - GetDistanceMetaball(from + float3(0, 0, -delta))\n    );\n    return normalize(normal);\n}<\/code><\/pre>\n\n\n\n<p>It works by checking the change in position between the current pixel and the surrounding pixels. Modify the&nbsp;<code>SphereTraceMetaballs_float<\/code> function to add a new output:&nbsp;<code>out float3 NormalWS<\/code>. Then after we set the <code>outAlpha<\/code> on line 78 we calculate the normal. That&#8217;s because this is the point in the code where we&#8217;ve confirmed the existence of a Metaball. So, add the line <code>NormalWS = CalculateNormalMetaball(from);<\/code>. Also, don&#8217;t forget to add <code>NormalWS = float3(0, 0, 0);<\/code> inside the <code>#if defined(SHADERGRAPH_PREVIEW)<\/code> block. Otherwise, you&#8217;ll get an error when the Shader Graph file is open. In the end the function will look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void SphereTraceMetaballs_float(float3 WorldPosition, out float Alpha, out float3 NormalWS)\n{\n    #if defined(SHADERGRAPH_PREVIEW)\n    Alpha = 1;\n    NormalWS = float3(0, 0, 0);\n    #else\n    float maxDistance = 100;\n    float threshold = 0.00001;\n    float t = 0;\n    int numSteps = 0;\n    \n    float outAlpha = 0;\n    \n    float3 viewPosition = GetCurrentViewPosition();\n    half3 viewDir = SafeNormalize(WorldPosition - viewPosition);\n    while (t &lt; maxDistance)\n    {\n        float minDistance = 1000000;\n        float3 from = viewPosition + t * viewDir;\n        float d = GetDistanceMetaball(from);\n        if (d &lt; minDistance)\n        {\n            minDistance = d;\n        }\n    \n        if (minDistance &lt;= threshold * t)\n        {\n            outAlpha = 1;\n            NormalWS = CalculateNormalMetaball(from);\n            break;\n        }\n    \n        t += minDistance;\n        ++numSteps;\n    }\n    \n    Alpha = outAlpha;\n    #endif\n}<\/code><\/pre>\n\n\n\n<p>Now let&#8217;s modify the&nbsp;<em>Metaball<\/em>&nbsp;Shader Graph. Add an output to our <code>SphereTraceMetaballs<\/code> custom function node for the&nbsp;<code>NormalWS<\/code>. Change Fragment Normal Space of Graph Settings to&nbsp;<strong><em>World<\/em><\/strong>&nbsp;since our normal is in world space. Finally, plug the output from the Custom Function node into the Normal slot of the Fragment output. When finished, the graph will look like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"736\" height=\"580\" src=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/GraphPart2.png\" alt=\"\" class=\"wp-image-198\" srcset=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/GraphPart2.png 736w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/GraphPart2-300x236.png 300w\" sizes=\"auto, (max-width: 736px) 100vw, 736px\" \/><\/figure>\n\n\n\n<p>And you&#8217;ll have simple lighting on your Metaballs. Unfortunately, if you&#8217;re hoping for full fancy PBR lighting, we aren&#8217;t there yet. However, this is enough to test if your Metaballs are working correctly. To finish our lighting setup, we need to dig through the URP source code. Since I&#8217;ve already covered a lot today, I&#8217;m going to leave that until part 3.<\/p>\n\n\n\n<p>In this part, we passed data from a particle system to our Metaball shader. We replaced our SDF Sphere with multiple Metaballs. And, we calculated the normal to add simple lighting.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"874\" height=\"668\" src=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/Part2Finished.png\" alt=\"\" class=\"wp-image-208\" srcset=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/Part2Finished.png 874w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/Part2Finished-300x229.png 300w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/Part2Finished-768x587.png 768w\" sizes=\"auto, (max-width: 874px) 100vw, 874px\" \/><\/figure>\n\n\n\n<p><strong>You can find the completed part 2 of the series\u00a0<\/strong><a href=\"https:\/\/github.com\/bzgeb\/UnityURPMetaballParticlesPart2\/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>here<\/strong><\/a><strong>\u00a0on Github. As promised,\u00a0<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/www.scratchapixel.com\/lessons\/advanced-rendering\/rendering-distance-fields\/blobbies\"><strong>here&#8217;s<\/strong><\/a><strong>\u00a0a link to the series where I learned the technical details of Metaballs. Sign up to my mailing list\u00a0<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/join-my-mailing-list\/\"><strong>here<\/strong><\/a><strong>\u00a0to be notified when part 3 of the series is released.<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post is part 2 in a series of articles that explain how to draw Metaballs in Unity using the Universal Render Pipeline (URP) and Shader Graph. Click&nbsp;here&nbsp;for part 1. This part describes:&nbsp; How to send position and size data from a particle system to our shader. How to write the Metaball shader function. How [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":173,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[17,1],"tags":[15,16,14,5,13,12],"class_list":["post-190","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-graphics","category-unity-programming","tag-metaball","tag-sdf","tag-shader-graph","tag-unity","tag-universal-render-pipeline","tag-urp"],"_links":{"self":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/190","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/comments?post=190"}],"version-history":[{"count":18,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/190\/revisions"}],"predecessor-version":[{"id":214,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/190\/revisions\/214"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media\/173"}],"wp:attachment":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media?parent=190"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/categories?post=190"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/tags?post=190"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}