{"id":216,"date":"2021-03-13T16:12:15","date_gmt":"2021-03-13T16:12:15","guid":{"rendered":"https:\/\/bronsonzgeb.com\/?p=216"},"modified":"2021-03-13T16:13:19","modified_gmt":"2021-03-13T16:13:19","slug":"particle-metaballs-in-unity-using-urp-and-shader-graph-part-3","status":"publish","type":"post","link":"https:\/\/bronsonzgeb.com\/index.php\/2021\/03\/13\/particle-metaballs-in-unity-using-urp-and-shader-graph-part-3\/","title":{"rendered":"Particle Metaballs in Unity using URP and Shader Graph Part 3"},"content":{"rendered":"\n<p>This post is part 3 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 or&nbsp;<a target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/2021\/03\/06\/particle-metaballs-in-unity-using-urp-and-shader-graph-part-2\/\" rel=\"noreferrer noopener\">here<\/a>&nbsp;for part 2. This part describes:&nbsp;<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>How to dig through URPs source to find the built-in PBR lighting function.<\/li><li>Setting up a custom PBR lighting node.<\/li><li>How to add HDRI and baked lighting to make shiny reflective Metaballs like the ones in the preview.<\/li><\/ul>\n\n\n\n<p>Let&#8217;s go!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">PBR Lighting<\/h2>\n\n\n\n<p>In an ideal world, we could plug our Metaballs into Shader Graph&#8217;s PBR master node. Unfortunately, that node relies on data that the traditional rendering pipeline would normally calculate for us. In our case, we render Metaballs using an unconventional approach so that data is absent or wrong. As a result, the best solution is to create a new custom PBR lighting node that accepts this data as input. However, rather than write our own version of the lighting function, we&#8217;ll call the one that&#8217;s built-in to URP. So the question becomes, where is this function, and how can we use it?<\/p>\n\n\n\n<p>Time for a bit of a sidebar. One of the greatest strengths of Unity&#8217;s Scriptable Render Pipelines is the ability to look through all the source code. Unity has always provided a download link to their built-in shaders, which has been an invaluable resource for me, and now the pipeline itself is open as well. I&#8217;ve learned a great deal about graphics programming from reading Unity&#8217;s built-in shader code. So, let&#8217;s dive into URP.<\/p>\n\n\n\n<p>Inside the Unity Editor&#8217;s Project tab, you should see two top-level folders: Assets and Packages. If you expand the Packages folder, and if URP is installed, you&#8217;ll find a Universal RP folder. This folder contains all the URP code, including all the shaders. The ShaderLibrary folder contains all the shared shader code, and the Shader folder contains all the URP shaders. URP&#8217;s shader code is well organized. Inside ShaderLibrary is a Lighting.hlsl file, and that&#8217;s where all the built-in lighting functions live. Search inside this file for PBR, and you&#8217;ll find two functions called&nbsp;<em>UniversalFragmentPBR<\/em>. One of these is just a helper function for the main one. The helper function is convenient because it allows us to pass the&nbsp;<em>SurfaceData<\/em>&nbsp;as a list of parameters instead of a struct, but what about the other argument:&nbsp;<em>InputData<\/em>? Our custom PBR function serves this purpose. We&#8217;re going to create a function that takes a list of arguments, fills in our&nbsp;<em>InputData<\/em>&nbsp;struct, and passes it to the UniversalFragmentPBR function. But how do we know what goes into the&nbsp;<em>InputData<\/em>&nbsp;struct? If you search through all of URP&#8217;s files, you&#8217;ll find it inside&nbsp;<em>Input.hlsl<\/em>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>struct InputData\n{\n    float3  positionWS;\n    half3   normalWS;\n    half3   viewDirectionWS;\n    float4  shadowCoord;\n    half    fogCoord;\n    half3   vertexLighting;\n    half3   bakedGI;\n    float2  normalizedScreenSpaceUV;\n    half4   shadowMask;\n};<\/code><\/pre>\n\n\n\n<p>This struct has a lot of data to fill out, so let&#8217;s get to it! The first three:&nbsp;<code>positionWS<\/code>,&nbsp;<code>normalWS<\/code>, and&nbsp;<code>viewDirectionWS&nbsp;<\/code>are the position, normal and view direction of a pixel of our Metaball in World-Space. Lucky for us, we already calculate these in our&nbsp;<code>SphereTraceMetaballs<\/code> function. We already output the normal, so we&#8217;ll add position and view direction as new outputs. The modified function looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void SphereTraceMetaballs_float(float3 WorldPosition, out float3 PositionWS, out float3 NormalWS, out float Alpha, out float3 ViewDirection)\n{\n    #if defined(SHADERGRAPH_PREVIEW)\n    PositionWS = float3(0, 0, 0);\n    NormalWS = float3(0, 0, 0);\n    ViewDirection = float3(0, 0, 0);\n    Alpha = 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            PositionWS = from;\n            NormalWS = CalculateNormalMetaball(from);\n            ViewDirection = viewDir;\n            outAlpha = 1;\n            break;\n        }\n\n        t += minDistance;\n        ++numSteps;\n    }\n    \n    Alpha = outAlpha;\n    #endif\n}<\/code><\/pre>\n\n\n\n<p>The difference is there are new outputs in the function signature, and we set those outputs along with the&nbsp;<code>NormalWS<\/code> and&nbsp;<code>outAlpha<\/code>. Next, we&#8217;re going to cheat by setting most of the&nbsp;<code>InputData<\/code> to a reasonable default. That means setting most of them to 0. I can justify this because rendering Metaballs is arguably too expensive for a real-time 3d application anyway, so we don&#8217;t need to support all the features. However, if you&#8217;re motivated, you can dig through the URP code to find how to calculate each value and implement them in your code. Now we can write our PBR Custom Function Node:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void PBR_float(float3 positionWS, half3 normalWS, half3 viewDirectionWS, half3 bakedGI, half3 albedo, half metallic, half3 specular, half smoothness, half occlusion, half3 emission, half alpha, out float3 Color)\n{\n    #if defined(SHADERGRAPH_PREVIEW)\n    Color = float3(1, 1, 1);\n    #else\n    InputData inputData;\n    inputData.positionWS = positionWS;\n    inputData.normalWS = NormalizeNormalPerPixel(normalWS);\n    inputData.viewDirectionWS = SafeNormalize(-viewDirectionWS);\n    inputData.shadowCoord = half4(0, 0, 0, 0);\n    inputData.fogCoord = 0;\n    inputData.vertexLighting = half3(0, 0, 0);\n    inputData.normalizedScreenSpaceUV = half2(0, 0);\n    inputData.shadowMask = half4(0, 0, 0, 0);\n    inputData.bakedGI = bakedGI;\n    Color = UniversalFragmentPBR(inputData, albedo, metallic, specular, smoothness, occlusion, emission, alpha);\n    #endif\n}<\/code><\/pre>\n\n\n\n<p>The function is straightforward. Take all the standard PBR inputs (with a couple of extras), create and fill a new\u00a0<code>InputData<\/code> struct, pass it to the built-in\u00a0<code>UniversalFragmentPBR<\/code> function, and that&#8217;s it! Now in Shader Graph, we create a new Custom Function node for this PBR function. At this point, we need to plug in values. The position, normal, and view direction come from the <code>SphereTraceMetaballs<\/code> node output. We&#8217;ll get the rest of the inputs from material properties. Add Albedo, Metallic, Specular, Smoothness and Emission properties to the graph. Connect those values, set <code>BakedGI<\/code> to <code>(0, 0, 0)<\/code> and <code>Occlusion<\/code> to <code>1<\/code>. Alternatively, if you want to use <code>BakedGI<\/code>, Shader Graph has a built-in <code>BakedGI<\/code> node you could use as well. It&#8217;s a bit messy, so I put mine into a subgraph that looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"997\" height=\"677\" src=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/MetaballSubgraph.png\" alt=\"\" class=\"wp-image-230\" srcset=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/MetaballSubgraph.png 997w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/MetaballSubgraph-300x204.png 300w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/MetaballSubgraph-768x522.png 768w\" sizes=\"auto, (max-width: 997px) 100vw, 997px\" \/><\/figure>\n\n\n\n<p>And my main graph looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1010\" height=\"737\" src=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/MetaballMainGraph.png\" alt=\"\" class=\"wp-image-231\" srcset=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/MetaballMainGraph.png 1010w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/MetaballMainGraph-300x219.png 300w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/MetaballMainGraph-768x560.png 768w\" sizes=\"auto, (max-width: 1010px) 100vw, 1010px\" \/><\/figure>\n\n\n\n<p>Great! To finish it off, let&#8217;s add an HDRI to create some interesting reflections.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Add an HDRI<\/h2>\n\n\n\n<p>So, let&#8217;s grab a free example HDRI from\u00a0<a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/hdrihaven.com\/\">HDRI Haven<\/a>. Any HDRI will do, so pick one that seems interesting to you. I&#8217;m using an indoor environment because they have more details that add to the visual interest. Drop your HDRI into the project and set the\u00a0<em>Texture Shape<\/em>\u00a0to\u00a0<em>Cube<\/em>\u00a0in the import settings. Here&#8217;s what mine looks like: <\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"448\" height=\"695\" src=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/ImportSettingsHDRI.png\" alt=\"\" class=\"wp-image-227\" srcset=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/ImportSettingsHDRI.png 448w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/ImportSettingsHDRI-193x300.png 193w\" sizes=\"auto, (max-width: 448px) 100vw, 448px\" \/><\/figure>\n\n\n\n<p>Create a new material using the\u00a0<em>Skybox > Cubemap<\/em>\u00a0shader and add your newly imported Cubemap. Finally, go into the\u00a0<em>Lighting Settings<\/em>\u00a0and set the\u00a0<em>Skybox Material<\/em>\u00a0to your new material. The last step is to create a Reflection Probe in the scene. You can do this from the menu by following:\u00a0<em>Game Object > Light > Reflection Probe<\/em>. Select the new Reflection Probe and press\u00a0<em>Bake<\/em>\u00a0in the inspector. Now, run the simulation and you&#8217;ll see something like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"748\" height=\"748\" src=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/image_001_0000.jpg\" alt=\"\" class=\"wp-image-228\" srcset=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/image_001_0000.jpg 748w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/image_001_0000-300x300.jpg 300w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/image_001_0000-150x150.jpg 150w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/image_001_0000-238x238.jpg 238w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/03\/image_001_0000-100x100.jpg 100w\" sizes=\"auto, (max-width: 748px) 100vw, 748px\" \/><\/figure>\n\n\n\n<p>So with that, we&#8217;ve finished our high resolution, albeit expensive to render, Metaballs. Unfortunately, I think they&#8217;re too costly to use in a real-time 3d application, so I&#8217;ve experimented with ways to make them more performant. In the next post, I&#8217;ll share those experiments and the interesting tricks I learned along the way.<\/p>\n\n\n\n<p><strong>The complete project for part 3 of the series is available\u00a0<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/github.com\/bzgeb\/UnityURPMetaballParticlesPart3\/\"><strong>here<\/strong><\/a><strong>\u00a0on Github. If you enjoy this content, please consider signing 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 the next post is released.<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post is part 3 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 or&nbsp;here&nbsp;for part 2. This part describes:&nbsp; How to dig through URPs source to find the built-in PBR lighting function. Setting up a custom PBR lighting [&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-216","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\/216","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=216"}],"version-history":[{"count":14,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/216\/revisions"}],"predecessor-version":[{"id":234,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/216\/revisions\/234"}],"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=216"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/categories?post=216"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/tags?post=216"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}