yum-archive/Tooner

A toon shader for Unity's BIRP.

git clone https://git.yummers.dev/yum-archive/Tooner

yumFix clearcoat07a94ee

master
5.3 KiB207 linesraw
1// !! AI ARTIFACT !!
2// This code was originally generated by Claude 3.5 Sonnet.
3// I wanted to write this tooling like I want a fucking hole in the head so I
4// kindly asked Claude to write it for me. It's shitty and poorly designed, but
5// it works well enough for my purposes.
6// It has been slightly tweaked by me, and validated on *this* codebase. It is
7// provided with no warranty.
8using UnityEngine;
9using UnityEditor;
10using System.IO;
11using System.Text.RegularExpressions;
12using System.Collections.Generic;
13using System.Linq;
14
15public class ShaderInliner : EditorWindow
16{
17	private string inputShaderPath;
18	private string outputShaderPath;
19
20	[MenuItem("Tools/yum_food/Shader Inliner")]
21	public static void ShowWindow()
22	{
23		GetWindow<ShaderInliner>("Shader Inliner");
24	}
25
26	private void OnGUI()
27	{
28		GUILayout.Label("Shader Inliner", EditorStyles.boldLabel);
29
30		inputShaderPath = EditorGUILayout.TextField("Input Shader Path", inputShaderPath);
31		if (GUILayout.Button("Select Input Shader"))
32		{
33			inputShaderPath = EditorUtility.OpenFilePanel("Select Shader", "", "shader");
34		}
35
36		if (GUILayout.Button("Inline Shader"))
37		{
38			if (string.IsNullOrEmpty(inputShaderPath))
39			{
40				EditorUtility.DisplayDialog("Error", "Please select an input shader.", "OK");
41				return;
42			}
43
44			InlineShader();
45		}
46	}
47
48	private void InlineShader()
49	{
50		string shaderContent = File.ReadAllText(inputShaderPath);
51		string inlinedShader = ProcessShader(shaderContent, Path.GetDirectoryName(inputShaderPath));
52
53		string fileName = Path.GetFileNameWithoutExtension(inputShaderPath);
54		outputShaderPath = Path.Combine(Path.GetDirectoryName(inputShaderPath), $"{fileName}_inlined.shader");
55		File.WriteAllText(outputShaderPath, inlinedShader);
56
57		AssetDatabase.Refresh();
58		//EditorUtility.DisplayDialog("Success", $"Inlined shader saved to:\n{outputShaderPath}", "OK");
59	}
60
61	private string ProcessShader(string content, string basePath)
62	{
63		// Update shader name
64		content = Regex.Replace(content, @"Shader\s+""(.+?)""", match =>
65				{
66				string shaderName = match.Groups[1].Value;
67				return $"Shader \"{shaderName}_inlined\"";
68				});
69
70		// Process each Pass independently
71		content = Regex.Replace(content, @"(CGPROGRAM.*?ENDCG)", match =>
72				{
73				return ProcessPass(match.Value, basePath);
74				}, RegexOptions.Singleline);
75
76		// Check for mismatched preprocessor macros in the entire shader
77		CheckMismatchedMacros(content);
78
79		return content;
80	}
81
82	private string ProcessPass(string passContent, string basePath)
83	{
84		HashSet<string> includedFiles = new HashSet<string>();
85
86		string pattern = @"#include\s+""(.+?)""";
87		return Regex.Replace(passContent, pattern, match =>
88				{
89				string includePath = match.Groups[1].Value;
90				string fullPath = Path.Combine(basePath, includePath);
91
92				if (File.Exists(fullPath))
93				{
94				if (!includedFiles.Contains(fullPath))
95				{
96				includedFiles.Add(fullPath);
97				string includeContent = File.ReadAllText(fullPath);
98				return ProcessInclude(includeContent, Path.GetDirectoryName(fullPath), includedFiles);
99				}
100				else
101				{
102				return "// Already included in this pass: " + includePath;
103				}
104				}
105				else
106				{
107				Debug.LogWarning($"Include file not found: {fullPath}");
108				return match.Value;
109				}
110				});
111	}
112
113	private string ProcessInclude(string content, string basePath, HashSet<string> includedFiles)
114	{
115		string pattern = @"#include\s+""(.+?)""";
116		return Regex.Replace(content, pattern, match =>
117				{
118				string includePath = match.Groups[1].Value;
119				string fullPath = Path.Combine(basePath, includePath);
120
121				if (File.Exists(fullPath))
122				{
123				if (!includedFiles.Contains(fullPath))
124				{
125				includedFiles.Add(fullPath);
126				string includeContent = File.ReadAllText(fullPath);
127				return ProcessInclude(includeContent, Path.GetDirectoryName(fullPath), includedFiles);
128				}
129				else
130				{
131				return "// Already included in this pass: " + includePath;
132				}
133				}
134				else
135				{
136				Debug.LogWarning($"Include file not found: {fullPath}");
137				return match.Value;
138				}
139				});
140	}
141
142	private void CheckMismatchedMacros(string content)
143	{
144		var stack = new Stack<string>();
145		var lines = content.Split('\n');
146		var macroPattern = @"^\s*#(if|ifdef|ifndef|elif|else|endif|if\s+defined)";
147
148		for (int i = 0; i < lines.Length; i++)
149		{
150			var line = lines[i].Trim();
151			var match = Regex.Match(line, macroPattern);
152
153			if (match.Success)
154			{
155				var directive = match.Groups[1].Value;
156
157				switch (directive)
158				{
159					case "if":
160					case "ifdef":
161					case "ifndef":
162					case "if defined":
163						stack.Push(directive);
164						break;
165					case "elif":
166						if (stack.Count == 0 || (stack.Peek() != "if" && stack.Peek() != "elif"))
167						{
168							Debug.LogError($"Mismatched #elif at line {i + 1}");
169						}
170						else
171						{
172							stack.Pop();
173							stack.Push("elif");
174						}
175						break;
176					case "else":
177						if (stack.Count == 0 || (stack.Peek() != "if" && stack.Peek() != "elif"))
178						{
179							Debug.LogError($"Mismatched #else at line {i + 1}");
180						}
181						else
182						{
183							stack.Pop();
184							stack.Push("else");
185						}
186						break;
187					case "endif":
188						if (stack.Count == 0)
189						{
190							Debug.LogError($"Mismatched #endif at line {i + 1}");
191						}
192						else
193						{
194							stack.Pop();
195						}
196						break;
197				}
198			}
199		}
200
201		if (stack.Count > 0)
202		{
203			Debug.LogError($"Unclosed preprocessor directives: {string.Join(", ", stack)}");
204		}
205	}
206}
207