{"id":577,"date":"2021-10-11T17:25:52","date_gmt":"2021-10-11T17:25:52","guid":{"rendered":"https:\/\/bronsonzgeb.com\/?p=577"},"modified":"2021-10-12T17:24:20","modified_gmt":"2021-10-12T17:24:20","slug":"custom-lighting-in-shader-graph-part-2","status":"publish","type":"post","link":"https:\/\/bronsonzgeb.com\/index.php\/2021\/10\/11\/custom-lighting-in-shader-graph-part-2\/","title":{"rendered":"Custom Lighting in Shader Graph Part 2"},"content":{"rendered":"\n<p>In this article, we&#8217;ll continue to improve the workflow of the <a href=\"https:\/\/bronsonzgeb.com\/index.php\/2021\/10\/04\/custom-lighting-in-urp-with-shader-graph\/\">previous article<\/a>, in which we created a process for converting Shader Graphs to a custom lighting function. <a href=\"https:\/\/bronsonzgeb.com\/index.php\/2021\/10\/04\/custom-lighting-in-urp-with-shader-graph\/\">The last article<\/a> ended with a manual workflow, so we&#8217;ll fully automate the process in this post. Along the way, we&#8217;ll learn about Asset Postprocessors, Reflection and Scriptable Singletons.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What&#8217;s the goal?<\/h2>\n\n\n\n<p>We finished the previous article with a less-than-ideal workflow for converting Shader Graphs to our custom lighting function. After saving your changes, you&#8217;d select the Shader Graph, copy the code into a buffer, then process that buffer and save it to disk as a new shader. This workflow is too tedious to be helpful when experimenting with Shader Graph. What we&#8217;re going to do now is make the entire process automatic. In other words, every time you save your Shader Graph, the converted shader will receive the updates automatically. Let&#8217;s outline the steps to accomplish this:<\/p>\n\n\n\n<ol class=\"wp-block-list\"><li>Detect when the user saves a Shader Graph.<ul><li>We can use an Asset Postprocessor for this.<\/li><\/ul><\/li><li>Copy the code of the saved Shader Graph into a text buffer.<ul><li>Generating the code is tricky because the API is internal, so we&#8217;ll use C#&#8217;s Reflection to accomplish this.<\/li><\/ul><\/li><li>Pass the generated code into our pipeline.<\/li><li>Save the converted shader to disk.<ul><li>We&#8217;ll cache the destination file in a Scriptable Singleton, so the user only needs to interact with the Save File Dialog the first time.<\/li><\/ul><\/li><\/ol>\n\n\n\n<p>Now that we have a plan let&#8217;s get to work!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Shader Graph Asset Postprocessor<\/h2>\n\n\n\n<p>In case you&#8217;re not familiar, Unity has an API for writing asset postprocessors. This API allows users to run custom code in the asset import pipeline. Given that every time an asset changes, it gets reimported, we&#8217;ll write a Shader Graph asset postprocessor to detect whenever a user saves a graph.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using System.IO;\nusing UnityEditor;\n\npublic class ShaderGraphModificationProcessor : AssetPostprocessor\n{\n    static void OnPostprocessAllAssets(string&#91;] importedAssets, string&#91;] deletedAssets, string&#91;] movedAssets,\n        string&#91;] movedFromAssetPaths)\n    {\n        foreach (var importedAsset in importedAssets)\n        {\n            if (Path.GetExtension(importedAsset).ToLower() == \".shadergraph\")\n            {\n                var guid = new GUID(AssetDatabase.AssetPathToGUID(importedAsset));\n                \/\/TODO: Convert Shader Graph\n            }\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>Here&#8217;s an asset postprocessor that will check every asset for the file extension <code>.shadergraph<\/code>. You can drop this file inside your project in a folder called <code>Editor<\/code>, and it&#8217;ll immediately work. Next, we&#8217;ll convert the Shader Graph to text and copy it into a text buffer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Convert Shader Graph to Text<\/h2>\n\n\n\n<p>I dug into Shader Graph&#8217;s code to learn how to convert graphs to text. I found a <code>Generator<\/code> class that can accept a <code>GraphData<\/code> object and output the generated shader as a string. Unfortunately, the APIs are all internal, so we have to use Reflection to access them.<\/p>\n\n\n\n<p>In case you don&#8217;t know, Reflection is a way to inspect and manipulate code at runtime. It has many uses. For our purposes, we&#8217;ll use it to locate the internal methods we want to use and invoke them. On an important note, we&#8217;re about to use a lot of runtime string lookups. That means that if any of these methods change in future versions of Shader Graph, this code may stop working. In other words, using Reflection is a bit frail.<\/p>\n\n\n\n<p>So, inside <code>ShaderGraphImporterEditor.cs<\/code>, we can locate the code that runs when you press &#8220;Copy Shader&#8221; in the Inspector.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public override void OnInspectorGUI()\n{\n.\n.\n.\n    if (GUILayout.Button(\"Copy Shader\"))\n    {\n        AssetImporter importer = target as AssetImporter;\n        string assetName = Path.GetFileNameWithoutExtension(importer.assetPath);\n\n        var graphData = GetGraphData(importer);\n        var generator = new Generator(graphData, null, GenerationMode.ForReals, assetName, null);\n        GUIUtility.systemCopyBuffer = generator.generatedShader;\n    }\n.\n.\n.\n}<\/code><\/pre>\n\n\n\n<p>This code uses a local function <code>GetGraphData<\/code> and passes the result into a <code>Generator<\/code> constructor. Then, it extracts the generated shader code and stores it in the <code>systemCopyBuffer<\/code>. To replicate this, we&#8217;ll get the <code>ShaderGraphImporterEditor<\/code> assembly, locate the <code>GetGraphData<\/code> method and invoke it with our Shader Graph. Then, we&#8217;ll locate the <code>Generator<\/code> constructor and invoke that with the <code>GraphData<\/code>. Finally, we&#8217;ll find the method to access the <code>generatedShader<\/code> field, invoke that, and complete this step.<\/p>\n\n\n\n<p>This next block of code searches all the loaded assemblies for the first one of the type <code>UnityEditor.ShaderGraph.ShaderGraphImporterEditor<\/code>. Then, from that assembly, we retrieve the <code>ShaderGraphImporterEditor<\/code> <code>Type<\/code> itself. Finally, we search the static non-public methods inside that <code>Type<\/code> and find one named <code>GetGraphData<\/code>. By the way, local functions are functions defined inside of methods. In this case, the local function was turned into a static method at compile-time. That&#8217;s why <code>GetGraphData<\/code> is a static method; its definition is inside <code>OnInspectorGUI<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>var shaderGraphImporterAssembly = AppDomain.CurrentDomain.GetAssemblies().First(assembly =&gt;\n{\n    return assembly.GetType(\"UnityEditor.ShaderGraph.ShaderGraphImporterEditor\") != null;\n});\nvar shaderGraphImporterEditorType = shaderGraphImporterAssembly.GetType(\"UnityEditor.ShaderGraph.ShaderGraphImporterEditor\");\n\nvar getGraphDataMethod = shaderGraphImporterEditorType\n    .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)\n    .First(info =&gt; { return info.Name.Contains(\"GetGraphData\"); });<\/code><\/pre>\n\n\n\n<p>With the method located, let&#8217;s invoke it. It requires an <code>AssetImporter<\/code> as an argument, though, so we&#8217;ll make one. The next block creates an <code>AssetImporter<\/code> from an asset <code>GUID<\/code> and invokes the <code>getGraphDataMethod<\/code> with a single argument.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>var path = AssetDatabase.GUIDToAssetPath(guid);\nvar assetImporter = AssetImporter.GetAtPath(path);\nvar graphData = getGraphDataMethod.Invoke(null, new object&#91;] {assetImporter});<\/code><\/pre>\n\n\n\n<p>Next, we&#8217;ll do a similar thing for the <code>Generator<\/code>. First, get the <code>Type<\/code>, then the constructor method, and finally, invoke the constructor with the appropriate arguments. I copied the arguments from the <code>ShaderGraphImporterEditor.cs<\/code> file above.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>var generatorType = shaderGraphImporterAssembly.GetType(\"UnityEditor.ShaderGraph.Generator\");\nvar generatorConstructor = generatorType.GetConstructors().First();\nvar generator = generatorConstructor.Invoke(new object&#91;]\n    {graphData, null, 1, $\"Converted\/{shaderGraphName}\", null});<\/code><\/pre>\n\n\n\n<p>Finally, the last step is to get the generated shader code from the <code>Generator<\/code>. So again: get the <code>Type<\/code>, get the method, invoke the method.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>var generatedShaderMethod = generator.GetType().GetMethod(\"get_generatedShader\",\n    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);\nvar generatedShader = generatedShaderMethod.Invoke(generator, new object&#91;] { });<\/code><\/pre>\n\n\n\n<p>Put the whole method together, and this is what you get.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public static void ConvertShaderGraphWithGuid(GUID guid)\n{\n    var path = AssetDatabase.GUIDToAssetPath(guid);\n    var assetImporter = AssetImporter.GetAtPath(path);\n    if (assetImporter.GetType().FullName != \"UnityEditor.ShaderGraph.ShaderGraphImporter\")\n    {\n        Debug.Log(\"Not a shader graph importer\");\n        return;\n    }\n\n    var shaderGraphName = Path.GetFileNameWithoutExtension(path);\n\n    var shaderGraphImporterAssembly = AppDomain.CurrentDomain.GetAssemblies().First(assembly =&gt;\n    {\n        return assembly.GetType(\"UnityEditor.ShaderGraph.ShaderGraphImporterEditor\") != null;\n    });\n    var shaderGraphImporterEditorType = shaderGraphImporterAssembly.GetType(\"UnityEditor.ShaderGraph.ShaderGraphImporterEditor\");\n\n    var getGraphDataMethod = shaderGraphImporterEditorType\n        .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)\n        .First(info =&gt; { return info.Name.Contains(\"GetGraphData\"); });\n\n    var graphData = getGraphDataMethod.Invoke(null, new object&#91;] {assetImporter});\n    var generatorType = shaderGraphImporterAssembly.GetType(\"UnityEditor.ShaderGraph.Generator\");\n    var generatorConstructor = generatorType.GetConstructors().First();\n    var generator = generatorConstructor.Invoke(new object&#91;]\n        {graphData, null, 1, $\"Converted\/{shaderGraphName}\", null});\n\n    var generatedShaderMethod = generator.GetType().GetMethod(\"get_generatedShader\",\n        BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);\n    var generatedShader = generatedShaderMethod.Invoke(generator, new object&#91;] { });\n\n    WriteShaderToFile(ConvertShader((string) generatedShader), path, shaderGraphName);\n}<\/code><\/pre>\n\n\n\n<p>The worst is over! The last line plugs this into the pipeline from the previous article. Every time you save a graph, it&#8217;ll convert it and ask you where to put it. For our final step, let&#8217;s remember where the user chooses to save their converted graph, so they only have to deal with the File popup the first time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tracking file paths<\/h2>\n\n\n\n<p>The easiest way to track our file paths is through a <code>ScriptableSingleton<\/code>. This object serializes to disk, so it&#8217;ll persist through domain reloads as well as closing the project. I covered <code>ScriptableSingletons<\/code> in a previous article, so I&#8217;ll run through the code quickly this time. Our object will store pairs of asset GUIDs. Using the GUIDs rather than the paths will allow our system to work even if we move files.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using System.Collections.Generic;\nusing UnityEditor;\nusing UnityEngine;\n\n&#91;FilePath(\"Assets\/CustomForwardPass\/Editor\/ShaderGraphConversionSettings.asset\",\n    FilePathAttribute.Location.ProjectFolder)]\npublic class ShaderGraphConversionToolSettings : ScriptableSingleton&lt;ShaderGraphConversionToolSettings&gt;,\n    ISerializationCallbackReceiver\n{\n    readonly Dictionary&lt;GUID, GUID&gt; _shaderPairs = new Dictionary&lt;GUID, GUID&gt;();\n\n    #region Serialization\n\n    &#91;SerializeField, HideInInspector] List&lt;string&gt; _shaderGraphGuids;\n    &#91;SerializeField, HideInInspector] List&lt;string&gt; _convertedGraphGuids;\n\n    #endregion\n\n\n    public void AddFilePair(string shaderGraphPath, string convertedShaderPath)\n    {\n        var shaderGraphGuid = new GUID(AssetDatabase.AssetPathToGUID(shaderGraphPath));\n        var convertedPathGuid = new GUID(AssetDatabase.AssetPathToGUID(convertedShaderPath));\n\n        if (_shaderPairs.ContainsKey(shaderGraphGuid))\n        {\n            _shaderPairs&#91;shaderGraphGuid] = convertedPathGuid;\n        }\n        else\n        {\n            _shaderPairs.Add(shaderGraphGuid, convertedPathGuid);\n        }\n\n        Save(true);\n    }\n\n    public string GetConvertedShaderPath(string shaderGraphPath)\n    {\n        var shaderGraphGuid = new GUID(AssetDatabase.AssetPathToGUID(shaderGraphPath));\n        if (_shaderPairs.TryGetValue(shaderGraphGuid, out GUID convertedShaderGuid))\n        {\n            return AssetDatabase.GUIDToAssetPath(convertedShaderGuid);\n        }\n\n        return null;\n    }\n\n    public void OnBeforeSerialize()\n    {\n        _shaderGraphGuids = new List&lt;string&gt;();\n        _convertedGraphGuids = new List&lt;string&gt;();\n\n        foreach (var shaderPair in _shaderPairs)\n        {\n            _shaderGraphGuids.Add(shaderPair.Key.ToString());\n            _convertedGraphGuids.Add(shaderPair.Value.ToString());\n        }\n    }\n\n    public void OnAfterDeserialize()\n    {\n        if (_shaderGraphGuids != null)\n        {\n            for (int i = 0; i &lt; _shaderGraphGuids.Count; ++i)\n            {\n                var _shaderGraphGuid = new GUID(_shaderGraphGuids&#91;i]);\n                var _convertedGraphGuid = new GUID(_convertedGraphGuids&#91;i]);\n\n                if (_shaderPairs.ContainsKey(_shaderGraphGuid))\n                {\n                    _shaderPairs&#91;_shaderGraphGuid] = _convertedGraphGuid;\n                }\n                else\n                {\n                    _shaderPairs.Add(_shaderGraphGuid, _convertedGraphGuid);\n                }\n            }\n        }\n\n        _shaderGraphGuids = null;\n        _convertedGraphGuids = null;\n    }\n}<\/code><\/pre>\n\n\n\n<p>There&#8217;s one minor caveat with this code. I&#8217;m using a <code>Dictionary<\/code> to pair a Shader Graph <code>GUID<\/code> to its converted shader counterpart. Since Dictionaries don&#8217;t serialize, I convert the dictionary to two lists, then convert back to a dictionary on deserialization. Now let&#8217;s jump over to the code to write the shader to a file.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>static void WriteShaderToFile(string shader, string shaderGraphPath, string defaultFileName)\n{\n    var filePath = \"\";\n    if (!string.IsNullOrEmpty(shaderGraphPath))\n    {\n        filePath = ShaderGraphConversionToolSettings.instance.GetConvertedShaderPath(shaderGraphPath);\n    }\n\n    if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))\n    {\n        filePath = EditorUtility.SaveFilePanelInProject(\"Converted Shader\", defaultFileName, \"shader\",\n            \"Where should we save the converted shader?\");\n    }\n\n    if (!string.IsNullOrEmpty(filePath))\n    {\n        File.WriteAllText(filePath, shader);\n        AssetDatabase.ImportAsset(filePath);\n        AssetDatabase.Refresh();\n\n        ShaderGraphConversionToolSettings.instance.AddFilePair(shaderGraphPath, filePath);\n    }\n}<\/code><\/pre>\n\n\n\n<p>If a destination file doesn&#8217;t exist when we write to disk, we&#8217;ll show a save file panel for the user to choose a destination. Otherwise, overwrite the existing converted shader.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wrap-Up<\/h2>\n\n\n\n<p>This article covered Asset Postprocessors to detect changes in Assets, Reflection to access internal Editor APIs, and Scriptable Singletons to store information. With this automated workflow, experimenting with Shader Graph and a custom lighting function is quick and easy.<\/p>\n\n\n\n<p><strong>Try out the project&nbsp;<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/github.com\/bzgeb\/CustomForwardPassLightingShaderGraph\"><strong>here on GitHub<\/strong><\/a><strong>. 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>In this article, we&#8217;ll continue to improve the workflow of the previous article, in which we created a process for converting Shader Graphs to a custom lighting function. The last article ended with a manual workflow, so we&#8217;ll fully automate the process in this post. Along the way, we&#8217;ll learn about Asset Postprocessors, Reflection and [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":570,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[43,17,1],"tags":[8,55,14,5,13,12],"class_list":["post-577","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-editor-tools","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\/577","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=577"}],"version-history":[{"count":4,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/577\/revisions"}],"predecessor-version":[{"id":583,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/577\/revisions\/583"}],"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=577"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/categories?post=577"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/tags?post=577"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}