{"id":568,"date":"2021-10-04T13:55:16","date_gmt":"2021-10-04T13:55:16","guid":{"rendered":"https:\/\/bronsonzgeb.com\/?p=568"},"modified":"2021-10-12T17:24:53","modified_gmt":"2021-10-12T17:24:53","slug":"custom-lighting-in-urp-with-shader-graph","status":"publish","type":"post","link":"https:\/\/bronsonzgeb.com\/index.php\/2021\/10\/04\/custom-lighting-in-urp-with-shader-graph\/","title":{"rendered":"Custom Lighting in URP with Shader Graph"},"content":{"rendered":"\n<p>This article will demonstrate how to support custom lighting functions when using URP and Shader Graph. We&#8217;ll start by exploring how Shader Graph works. Then, we&#8217;ll build a tool to override the default behaviour with our custom behaviour. There&#8217;s a lot to cover, so let&#8217;s get started.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why would we want to do this?<\/h2>\n\n\n\n<p>Shader Graph is a fantastic tool to enable artists to iterate on shaders. However, because it only supports PBR and Unlit workflows, it&#8217;s not very good at allowing non-photorealistic rendering. If you look around the internet, you&#8217;ll find hacky ways to do toon shading in Shader Graph, but in one way or another, they all struggle with casting and receiving shadows. So we&#8217;ll solve this problem by enabling Shader Graph to use our custom lighting functions. You&#8217;ll be able to write an entire custom vertex and fragment function, too, so you can push your lighting &#8220;backend&#8221; as far as you need while still enabling artists to describe an object&#8217;s surface via Shader Graph. If that&#8217;s appealing to you, then read on!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How does Shader Graph work?<\/h2>\n\n\n\n<p>When you create a Shader Graph, you&#8217;re writing a function that describes the surface of an object. The output of the graph are properties such as the <code>BaseColor<\/code>, <code>Emission<\/code>, <code>Smoothness<\/code>, etc. These are properties that describe a surface. So a Shader Graph generates a function called <code>SurfaceDescriptionFunction<\/code>. Then, the system grabs a pre-written shader template and links it to the newly generated <code>SurfaceDescriptionFunction<\/code>. It does other stuff too, but that&#8217;s irrelevant for our goals. This system allows artists to focus on defining an object&#8217;s surface without worrying about all the technical boilerplate code the renderer needs. It also means we can use custom boilerplate code if we can inject it into the process. So that&#8217;s what we&#8217;ll do!<\/p>\n\n\n\n<p>Let&#8217;s get deeper into the details. Click on any Shader Graph file, and you&#8217;ll see an option to &#8220;View Generated Shader&#8221; in the Inspector. Inspect the generated shader file, and you&#8217;ll see it&#8217;s mostly a bunch of similar-looking passes that <code>#include<\/code> several common files. There isn&#8217;t even a vertex or fragment function. That&#8217;s because the Shader Graph doesn&#8217;t generate the vertex and fragment functions; it includes them from other files. This simplifies our job because if we modify which files we include, we can control how we render objects. In other words, remove the generated vertex and frag functions, insert ours, and the pipeline is under our control.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The manual way<\/h2>\n\n\n\n<p>Let me show you. Currently, I&#8217;m inspecting a generated shader. Since I&#8217;m accustomed to writing shaders in Unity, I can identify the Forward Pass called &#8220;Universal Forward&#8221;. The pass references a function <code>frag<\/code>, that&#8217;s not present in this file. I also notice the line <code>#include \"Packages\/com.unity.render-pipelines.universal\/Editor\/ShaderGraph\/Includes\/PBRForwardPass.hlsl\"<\/code>. Since URP is a package, I can investigate the code, so I open that file. What do I find inside? The <code>frag<\/code> function.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>half4 frag(PackedVaryings packedInput) : SV_TARGET\n{\n    Varyings unpacked = UnpackVaryings(packedInput);\n    UNITY_SETUP_INSTANCE_ID(unpacked);\n    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(unpacked);\n\n    SurfaceDescriptionInputs surfaceDescriptionInputs = BuildSurfaceDescriptionInputs(unpacked);\n    SurfaceDescription surfaceDescription = SurfaceDescriptionFunction(surfaceDescriptionInputs);\n\n    #if _AlphaClip\n        half alpha = surfaceDescription.Alpha;\n        clip(alpha - surfaceDescription.AlphaClipThreshold);\n    #elif _SURFACE_TYPE_TRANSPARENT\n        half alpha = surfaceDescription.Alpha;\n    #else\n    half alpha = 1;\n    #endif\n\n    InputData inputData;\n    BuildInputData(unpacked, surfaceDescription, inputData);\n\n    #ifdef _SPECULAR_SETUP\n        float3 specular = surfaceDescription.Specular;\n        float metallic = 1;\n    #else\n    float3 specular = 0;\n    float metallic = surfaceDescription.Metallic;\n    #endif\n\n    SurfaceData surface = (SurfaceData)0;\n    surface.albedo = surfaceDescription.BaseColor;\n    surface.metallic = saturate(metallic);\n    surface.specular = specular;\n    surface.smoothness = saturate(surfaceDescription.Smoothness),\n        surface.occlusion = surfaceDescription.Occlusion,\n        surface.emission = surfaceDescription.Emission,\n        surface.alpha = saturate(alpha);\n    surface.clearCoatMask = 0;\n    surface.clearCoatSmoothness = 1;\n\n    #ifdef _CLEARCOAT\n        surface.clearCoatMask       = saturate(surfaceDescription.CoatMask);\n        surface.clearCoatSmoothness = saturate(surfaceDescription.CoatSmoothness);\n    #endif\n\n    half4 color = UniversalFragmentPBR(inputData, surface);\n\n    color.rgb = MixFog(color.rgb, inputData.fogCoord);\n    return color;\n}<\/code><\/pre>\n\n\n\n<p>Here, I also noticed a reference to <code>SurfaceDescriptionFunction<\/code>, which was generated from the Shader Graph and placed inside the generated shader file.<\/p>\n\n\n\n<p>So, all we have to do is copy the file <code>PBRForwardPass.hlsl<\/code> into our project and modify the <code>#include<\/code> line to point to our version. If you want to test it, take the code from the generated file and save it into a new file in the same directory as your custom <code>PBRForwardPass.hlsl<\/code> file. Then, replaced every instance of <code>#include \"Packages\/com.unity.render-pipelines.universal\/Editor\/ShaderGraph\/Includes\/PBRForwardPass.hlsl\"<\/code> with <code>#include \"PBRForwardPass.hlsl\"<\/code>, or whatever name you chose. Finally, modify the <code>frag<\/code> function to <code>return half4(1, 0, 0, 1)<\/code>. Now every material you apply this shader to will be red. Pretty neat, right? Well, there are two big issues. First, manually changing the generated file every time is a terrible workflow. Second, I haven&#8217;t shown you how to write a custom lighting function. Let&#8217;s start with the second one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Custom Lighting Function<\/h2>\n\n\n\n<p>This next part is a little technical. If you aren&#8217;t familiar with handwritten shaders, it may be challenging to follow. That&#8217;s because, unfortunately, this approach to custom lighting functions requires a handwritten pass. However, we don&#8217;t have to start from scratch! All Unity&#8217;s shader code is available in one way or another, so let&#8217;s start by duplicating the files we need and go from there.<\/p>\n\n\n\n<p>Like we said in the previous section, the file we want is <code>Packages\/Universal RP\/Editor\/ShaderGraph\/Includes\/PBRForwardPass.hlsl<\/code>. If you haven&#8217;t already, copy that into your project and rename it to <code>CustomForwardPass.hlsl<\/code>. This file contains the <code>vert<\/code> and <code>frag<\/code> functions that Lit URP Shader Graphs pass through. By the way, we could isolate the file with the lighting function instead, but why not take control of the entire forward pass?<\/p>\n\n\n\n<p>The <code>frag<\/code> function references a function called <code>UniversalFragmentPBR<\/code> to calculate the lighting. This function isn&#8217;t part of this file, so let&#8217;s duplicate it as well. This one is inside <code>Packages\/Universal RP\/ShaderLibrary\/Lighting.hlsl<\/code>. Copy the function <code>half4 UniversalFragmentPBR(InputData inputData, SurfaceData surfaceData)<\/code> to the top of <code>CustomForwardPass.hlsl<\/code>. Let&#8217;s also copy <code>LightingPhysicallyBased<\/code> from <code>Lighting.hlsl<\/code> into <code>CustomForwardPass.hlsl<\/code>, since we&#8217;re going to need it soon. We&#8217;ll give the functions custom names and make sure we change all references to our custom version. In the end, you&#8217;ll have a version that looks like this.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>half3 LightingCustom(BRDFData brdfData, BRDFData brdfDataClearCoat,\n                                    half3 lightColor, half3 lightDirectionWS, half lightAttenuation,\n                                    half3 normalWS, half3 viewDirectionWS,\n                                    half clearCoatMask, bool specularHighlightsOff)\n{\n    half NdotL = saturate(dot(normalWS, lightDirectionWS));\n    half3 radiance = lightColor * (lightAttenuation * NdotL);\n\n    half3 brdf = brdfData.diffuse;\n    #ifndef _SPECULARHIGHLIGHTS_OFF\n    &#91;branch] if (!specularHighlightsOff)\n    {\n        brdf += brdfData.specular * DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS);\n\n        #if defined(_CLEARCOAT) || defined(_CLEARCOATMAP)\n        \/\/ Clear coat evaluates the specular a second timw and has some common terms with the base specular.\n        \/\/ We rely on the compiler to merge these and compute them only once.\n        half brdfCoat = kDielectricSpec.r * DirectBRDFSpecular(brdfDataClearCoat, normalWS, lightDirectionWS, viewDirectionWS);\n\n            \/\/ Mix clear coat and base layer using khronos glTF recommended formula\n            \/\/ https:\/\/github.com\/KhronosGroup\/glTF\/blob\/master\/extensions\/2.0\/Khronos\/KHR_materials_clearcoat\/README.md\n            \/\/ Use NoV for direct too instead of LoH as an optimization (NoV is light invariant).\n            half NoV = saturate(dot(normalWS, viewDirectionWS));\n            \/\/ Use slightly simpler fresnelTerm (Pow4 vs Pow5) as a small optimization.\n            \/\/ It is matching fresnel used in the GI\/Env, so should produce a consistent clear coat blend (env vs. direct)\n            half coatFresnel = kDielectricSpec.x + kDielectricSpec.a * Pow4(1.0 - NoV);\n\n        brdf = brdf * (1.0 - clearCoatMask * coatFresnel) + brdfCoat * clearCoatMask;\n        #endif \/\/ _CLEARCOAT\n    }\n    #endif \/\/ _SPECULARHIGHLIGHTS_OFF\n\n    return brdf * radiance;\n}\n\nhalf3 LightingCustom(BRDFData brdfData, BRDFData brdfDataClearCoat, Light light, half3 normalWS,\n                                    half3 viewDirectionWS, half clearCoatMask, bool specularHighlightsOff)\n{\n    return LightingCustom(brdfData, brdfDataClearCoat, light.color, light.direction,\n                                         light.distanceAttenuation * light.shadowAttenuation, normalWS, viewDirectionWS,\n                                         clearCoatMask, specularHighlightsOff);\n}\n\nhalf4 UniversalFragmentCustom(InputData inputData, SurfaceData surfaceData)\n{\n    #ifdef _SPECULARHIGHLIGHTS_OFF\n    bool specularHighlightsOff = true;\n    #else\n    bool specularHighlightsOff = false;\n    #endif\n\n    BRDFData brdfData;\n\n    \/\/ NOTE: can modify alpha\n    InitializeBRDFData(surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness,\n                       surfaceData.alpha, brdfData);\n\n    BRDFData brdfDataClearCoat = (BRDFData)0;\n    #if defined(_CLEARCOAT) || defined(_CLEARCOATMAP)\n    \/\/ base brdfData is modified here, rely on the compiler to eliminate dead computation by InitializeBRDFData()\n    InitializeBRDFDataClearCoat(surfaceData.clearCoatMask, surfaceData.clearCoatSmoothness, brdfData, brdfDataClearCoat);\n    #endif\n\n    \/\/ To ensure backward compatibility we have to avoid using shadowMask input, as it is not present in older shaders\n    #if defined(SHADOWS_SHADOWMASK) &amp;&amp; defined(LIGHTMAP_ON)\n    half4 shadowMask = inputData.shadowMask;\n    #elif !defined (LIGHTMAP_ON)\n    half4 shadowMask = unity_ProbesOcclusion;\n    #else\n    half4 shadowMask = half4(1, 1, 1, 1);\n    #endif\n\n    Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask);\n\n    #if defined(_SCREEN_SPACE_OCCLUSION)\n        AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(inputData.normalizedScreenSpaceUV);\n        mainLight.color *= aoFactor.directAmbientOcclusion;\n        surfaceData.occlusion = min(surfaceData.occlusion, aoFactor.indirectAmbientOcclusion);\n    #endif\n\n    MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI);\n    half3 color = GlobalIllumination(brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,\n                                     inputData.bakedGI, surfaceData.occlusion,\n                                     inputData.normalWS, inputData.viewDirectionWS);\n    color += LightingCustom(brdfData, brdfDataClearCoat,\n                                           mainLight,\n                                           inputData.normalWS, inputData.viewDirectionWS,\n                                           surfaceData.clearCoatMask, specularHighlightsOff);\n\n    #ifdef _ADDITIONAL_LIGHTS\n    uint pixelLightCount = GetAdditionalLightsCount();\n    for (uint lightIndex = 0u; lightIndex &lt; pixelLightCount; ++lightIndex)\n    {\n        Light light = GetAdditionalLight(lightIndex, inputData.positionWS, shadowMask);\n    #if defined(_SCREEN_SPACE_OCCLUSION)\n            light.color *= aoFactor.directAmbientOcclusion;\n    #endif\n        color += LightingCustom(brdfData, brdfDataClearCoat,\n                                         light,\n                                         inputData.normalWS, inputData.viewDirectionWS,\n                                         surfaceData.clearCoatMask, specularHighlightsOff);\n    }\n    #endif\n\n    #ifdef _ADDITIONAL_LIGHTS_VERTEX\n    color += inputData.vertexLighting * brdfData.diffuse;\n    #endif\n\n    color += surfaceData.emission;\n\n    return half4(color, surfaceData.alpha);\n}\n\nvoid BuildInputData(Varyings input, SurfaceDescription surfaceDescription, out InputData inputData)\n{\n    inputData.positionWS = input.positionWS;\n\n    #ifdef _NORMALMAP\n    #if _NORMAL_DROPOFF_TS\n    \/\/ IMPORTANT! If we ever support Flip on double sided materials ensure bitangent and tangent are NOT flipped.\n    float crossSign = (input.tangentWS.w &gt; 0.0 ? 1.0 : -1.0) * GetOddNegativeScale();\n    float3 bitangent = crossSign * cross(input.normalWS.xyz, input.tangentWS.xyz);\n    inputData.normalWS = TransformTangentToWorld(surfaceDescription.NormalTS,\n                                                 half3x3(input.tangentWS.xyz, bitangent, input.normalWS.xyz));\n    #elif _NORMAL_DROPOFF_OS\n            inputData.normalWS = TransformObjectToWorldNormal(surfaceDescription.NormalOS);\n    #elif _NORMAL_DROPOFF_WS\n            inputData.normalWS = surfaceDescription.NormalWS;\n    #endif\n    #else\n        inputData.normalWS = input.normalWS;\n    #endif\n    inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);\n    inputData.viewDirectionWS = SafeNormalize(input.viewDirectionWS);\n\n    #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)\n        inputData.shadowCoord = input.shadowCoord;\n    #elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)\n        inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);\n    #else\n    inputData.shadowCoord = float4(0, 0, 0, 0);\n    #endif\n\n    inputData.fogCoord = input.fogFactorAndVertexLight.x;\n    inputData.vertexLighting = input.fogFactorAndVertexLight.yzw;\n    inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.sh, inputData.normalWS);\n    inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);\n    inputData.shadowMask = SAMPLE_SHADOWMASK(input.lightmapUV);\n}\n\nPackedVaryings vert(Attributes input)\n{\n    Varyings output = (Varyings)0;\n    output = BuildVaryings(input);\n    PackedVaryings packedOutput = (PackedVaryings)0;\n    packedOutput = PackVaryings(output);\n    return packedOutput;\n}\n\nhalf4 frag(PackedVaryings packedInput) : SV_TARGET\n{\n    Varyings unpacked = UnpackVaryings(packedInput);\n    UNITY_SETUP_INSTANCE_ID(unpacked);\n    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(unpacked);\n\n    SurfaceDescriptionInputs surfaceDescriptionInputs = BuildSurfaceDescriptionInputs(unpacked);\n    SurfaceDescription surfaceDescription = SurfaceDescriptionFunction(surfaceDescriptionInputs);\n\n    #if _AlphaClip\n        half alpha = surfaceDescription.Alpha;\n        clip(alpha - surfaceDescription.AlphaClipThreshold);\n    #elif _SURFACE_TYPE_TRANSPARENT\n        half alpha = surfaceDescription.Alpha;\n    #else\n    half alpha = 1;\n    #endif\n\n    InputData inputData;\n    BuildInputData(unpacked, surfaceDescription, inputData);\n\n    #ifdef _SPECULAR_SETUP\n        float3 specular = surfaceDescription.Specular;\n        float metallic = 1;\n    #else\n    float3 specular = 0;\n    float metallic = surfaceDescription.Metallic;\n    #endif\n\n    SurfaceData surface = (SurfaceData)0;\n    surface.albedo = surfaceDescription.BaseColor;\n    surface.metallic = saturate(metallic);\n    surface.specular = specular;\n    surface.smoothness = saturate(surfaceDescription.Smoothness),\n    surface.occlusion = surfaceDescription.Occlusion,\n    surface.emission = surfaceDescription.Emission,\n    surface.alpha = saturate(alpha);\n    surface.clearCoatMask = 0;\n    surface.clearCoatSmoothness = 1;\n\n    #ifdef _CLEARCOAT\n        surface.clearCoatMask       = saturate(surfaceDescription.CoatMask);\n        surface.clearCoatSmoothness = saturate(surfaceDescription.CoatSmoothness);\n    #endif\n\n    half4 color = UniversalFragmentCustom(inputData, surface);\n\n    color.rgb = MixFog(color.rgb, inputData.fogCoord);\n    return color;\n}<\/code><\/pre>\n\n\n\n<p>I know this isn&#8217;t a great code viewer, and this code is tough to read. Hopefully, you grab the complete version from the Github project linked at the bottom. The great news is now you have total control over ShaderGraph&#8217;s URP forward pass! You can do anything you want with it. Let&#8217;s make one teensy change to get that toon look from the header image.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>half3 LightingCustom(BRDFData brdfData, BRDFData brdfDataClearCoat,\n                                    half3 lightColor, half3 lightDirectionWS, half lightAttenuation,\n                                    half3 normalWS, half3 viewDirectionWS,\n                                    half clearCoatMask, bool specularHighlightsOff)\n{\n    <strong>half NdotL = step(0.5, saturate(dot(normalWS, lightDirectionWS)));<\/strong>\n.\n.\n.<\/code><\/pre>\n\n\n\n<p>Voila! By stepping the <code>NdotL<\/code> calculation, we give our shadows a sharp cutoff, which is the basis of the cel-shaded look. It sucks how much effort it takes to apply this tiny change. <\/p>\n\n\n\n<figure class=\"wp-block-gallery columns-2 is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex\"><ul class=\"blocks-gallery-grid\"><li class=\"blocks-gallery-item\"><figure><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph4-1024x576.png\" alt=\"\" data-id=\"571\" data-full-url=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph4.png\" data-link=\"https:\/\/bronsonzgeb.com\/windwakerurpshadergraph4\/\" class=\"wp-image-571\" srcset=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph4-1024x576.png 1024w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph4-300x169.png 300w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph4-768x432.png 768w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph4.png 1404w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"blocks-gallery-item__caption\">Standard PBR Lighting<\/figcaption><\/figure><\/li><li class=\"blocks-gallery-item\"><figure><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"577\" src=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph3-1024x577.png\" alt=\"Toon Link in URP with Shader Graph\" data-id=\"570\" data-full-url=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph3.png\" data-link=\"https:\/\/bronsonzgeb.com\/windwakerurpshadergraph3\/\" class=\"wp-image-570\" srcset=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph3-1024x577.png 1024w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph3-300x169.png 300w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph3-768x433.png 768w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/WindWakerURPShaderGraph3.png 1403w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"blocks-gallery-item__caption\">Stepped Lighting<\/figcaption><\/figure><\/li><\/ul><\/figure>\n\n\n\n<p>Next, let&#8217;s see how we can improve this manual workflow.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Improved workflow<\/h2>\n\n\n\n<p>Let me start by saying I&#8217;m still working towards a great solution. However, I&#8217;ll share my progress so far.The Shader Graph importer has an option to copy the generated shader into your system&#8217;s copy\/paste buffer. Since we can access the copy\/paste buffer as well, we can search and replace every instance of <code>#include \"Packages\/com.unity.render-pipelines.universal\/Editor\/ShaderGraph\/Includes\/PBRForwardPass.hlsl\"<\/code> with <code>CustomForwardPass.hlsl<\/code>. Since this is still a manual process, consider it the first step. Let&#8217;s write a tool to do that.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using System;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing UnityEditor;\nusing UnityEngine;\n\npublic class ShaderGraphConversionTool : Editor\n{\n    const string PBRForwardPassInclude =\n        \"#include \\\"Packages\/com.unity.render-pipelines.universal\/Editor\/ShaderGraph\/Includes\/PBRForwardPass.hlsl\\\"\";\n\n    const string CustomInclude = \"#include \\\"CustomForwardPass.hlsl\\\"\";\n\n    &#91;MenuItem(\"Tools\/Convert Shader in CopyPaste Buffer\")]\n    static void ConvertShaderInBuffer()\n    {\n        var shader = GUIUtility.systemCopyBuffer;\n        var convertedShader = ConvertShader(shader);\n        GUIUtility.systemCopyBuffer = convertedShader;\n        WriteShaderToFile(convertedShader, \"ConvertedShader\");\n    }\n\n    static string ConvertShader(string shader)\n    {\n        return shader.Replace(PBRForwardPassInclude, CustomInclude);\n    }\n\n    static void WriteShaderToFile(string shader, string defaultFileName)\n    {\n        var filePath = EditorUtility.SaveFilePanelInProject(\"Converted Shader\", defaultFileName, \"shader\",\n            \"Where should we save the converted shader?\");\n\n        if (!string.IsNullOrEmpty(filePath))\n        {\n            File.WriteAllText(filePath, shader);\n            AssetDatabase.Refresh();\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>This little tool is available from the <code>Tools<\/code> menu in your Unity project. It replaces all instances defined by <code>PBRForwardPassInclude<\/code> with the string defined by <code>CustomInclude<\/code> inside your system&#8217;s copy buffer. Then, it&#8217;ll ask you where to save the newly processed shader file. By the way, don&#8217;t forget that the converted shader must reside in the same folder as the <code>CustomForwardPass.hlsl<\/code> file. So, to use the tool, select any Shader Graph asset in your project. Then, in the Inspector, find the <code>Copy Shader<\/code> button. Press that to copy the generated shader into your system&#8217;s copy buffer. Then select Tools &gt; Convert Shader in CopyPaste Buffer to execute the conversion. Great! It&#8217;s still a terrible workflow, but now there are slightly fewer steps!<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"493\" height=\"431\" src=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/CopyShaderButton.png\" alt=\"\" class=\"wp-image-572\" srcset=\"https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/CopyShaderButton.png 493w, https:\/\/bronsonzgeb.com\/wp-content\/uploads\/2021\/10\/CopyShaderButton-300x262.png 300w\" sizes=\"auto, (max-width: 493px) 100vw, 493px\" \/><figcaption>The Copy Shader button in the Inspector<\/figcaption><\/figure><\/div>\n\n\n\n<p>There&#8217;s a version that will semi-reliably convert your Shader Graph every time you save it in the GitHub project at the end. It uses an asset processor to detect a change in Shader Graph files, and then through Reflection, it converts the Shader Graph to its text format for processing. It also uses a <code>ScriptableSingleton<\/code> to track where to save each converted Shader Graph shader, so the file dialogue will only pop up after the first save. Giving an in-depth explanation is enough content for an entire post, so I&#8217;ll save that for another time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wrap-Up<\/h2>\n\n\n\n<p>So, in this post, we saw how to take control of the so-called &#8220;backend&#8221; of Shader Graph. In other words, where Shader Graph stops, we take over. Doing so still requires some proficiency with handwritten shaders, but artists can experiment to their heart&#8217;s content through Shader Graph once the backend is written.<\/p>\n\n\n\n<p><strong>Check out the project&nbsp;<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/github.com\/bzgeb\/CustomForwardPassLightingShaderGraph\"><strong>here on GitHub<\/strong><\/a><strong>. Check out part 2 <a href=\"https:\/\/bronsonzgeb.com\/index.php\/2021\/10\/11\/custom-lighting-in-shader-graph-part-2\/\">here<\/a>. If you want to support my work,&nbsp;<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/join-my-mailing-list\/\"><strong>join my mailing list<\/strong><\/a><strong>. If you do, I&#8217;ll notify you whenever I write a new post.<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This article will demonstrate how to support custom lighting functions when using URP and Shader Graph. We&#8217;ll start by exploring how Shader Graph works. Then, we&#8217;ll build a tool to override the default behaviour with our custom behaviour. There&#8217;s a lot to cover, so let&#8217;s get started. Why would we want to do this? Shader [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":570,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[17,1],"tags":[8,55,14,5,13,12],"class_list":["post-568","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-graphics","category-unity-programming","tag-game-development","tag-npr","tag-shader-graph","tag-unity","tag-universal-render-pipeline","tag-urp"],"_links":{"self":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/568","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=568"}],"version-history":[{"count":7,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/568\/revisions"}],"predecessor-version":[{"id":584,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/568\/revisions\/584"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media\/570"}],"wp:attachment":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media?parent=568"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/categories?post=568"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/tags?post=568"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}