A game about forced loneliness, made by TACStudios
1using System;
2using System.Linq;
3using System.Collections.Generic;
4using UnityEngine;
5using UnityEditor.Graphing;
6using UnityEditor.ShaderGraph.Internal;
7using UnityEditor.Rendering;
8
9namespace UnityEditor.ShaderGraph
10{
11 [Serializable]
12 [Title("Custom Interpolators", "Instance")]
13 class CustomInterpolatorNode : AbstractMaterialNode
14 {
15 [SerializeField]
16 internal string customBlockNodeName = "K_INVALID";
17
18 [SerializeField]
19 private BlockNode.CustomBlockType serializedType = BlockNode.CustomBlockType.Vector4;
20
21 public override bool hasPreview { get { return true; } }
22
23 internal override bool ExposeToSearcher { get => false; } // This is exposed in a special way.
24
25 public override bool allowedInSubGraph { get => false; }
26
27 internal BlockNode e_targetBlockNode // weak indirection via customBlockNodeName
28 {
29 get => (owner?.vertexContext.blocks.Find(cib => cib.value.descriptor.name == customBlockNodeName))?.value ?? null;
30 }
31
32 public CustomInterpolatorNode()
33 {
34 UpdateNodeAfterDeserialization();
35 }
36
37 internal void ConnectToCustomBlock(BlockNode node)
38 {
39 // if somehow we were already connected, be sure to unregister.
40 if (e_targetBlockNode != null)
41 {
42 e_targetBlockNode.UnregisterCallback(OnCustomBlockModified);
43 }
44 // if a new cib is renamed to match us when we didn't have a target (unusual case, but covering all bases here).
45 if (node?.isCustomBlock ?? false)
46 {
47 name = node.customName + " (Custom Interpolator)";
48 customBlockNodeName = node.customName;
49 serializedType = node.customWidth;
50 BuildSlot();
51 node.RegisterCallback(OnCustomBlockModified);
52 }
53 }
54
55 internal void ConnectToCustomBlockByName(string customBlockName)
56 {
57 // see above
58 if (e_targetBlockNode != null)
59 {
60 e_targetBlockNode.UnregisterCallback(OnCustomBlockModified);
61 }
62
63 name = customBlockName + " (Custom Interpolator)";
64 customBlockNodeName = customBlockName;
65 if (e_targetBlockNode != null)
66 {
67 serializedType = e_targetBlockNode.customWidth;
68 BuildSlot();
69 e_targetBlockNode.RegisterCallback(OnCustomBlockModified);
70 }
71 else
72 {
73 // We should get badged in OnValidate.
74 }
75 }
76
77 void OnCustomBlockModified(AbstractMaterialNode node, Graphing.ModificationScope scope)
78 {
79 if (node is BlockNode bnode)
80 {
81 if (bnode?.isCustomBlock ?? false)
82 {
83 name = bnode.customName + " (Custom Interpolator)";
84 customBlockNodeName = bnode.customName;
85 if (e_targetBlockNode != null && e_targetBlockNode.owner != null)
86 {
87 serializedType = e_targetBlockNode.customWidth;
88 BuildSlot();
89 Dirty(ModificationScope.Node);
90 Dirty(ModificationScope.Topological);
91 }
92 }
93 }
94 // bnode information we got is somehow invalid, this is probably case for an exception.
95 }
96
97 public override void ValidateNode()
98 {
99 // Our node was deleted or we had bad deserialization, we need to badge.
100 if (e_targetBlockNode == null || e_targetBlockNode.owner == null)
101 {
102 e_targetBlockNode?.UnregisterCallback(OnCustomBlockModified);
103 owner.AddValidationError(objectId, String.Format("Custom Block Interpolator '{0}' not found.", customBlockNodeName), ShaderCompilerMessageSeverity.Error);
104 }
105 else
106 {
107 // our blockNode reference is somehow valid again after it wasn't,
108 // we can reconnect and everything should be restored.
109 ConnectToCustomBlockByName(customBlockNodeName);
110 }
111 }
112
113 public override void UpdateNodeAfterDeserialization()
114 {
115 // our e_targetBlockNode is unsafe here, so we build w/our serialization info and hope for the best!
116 BuildSlot();
117 base.UpdateNodeAfterDeserialization();
118 }
119
120 void BuildSlot()
121 {
122 switch (serializedType)
123 {
124 case BlockNode.CustomBlockType.Float:
125 AddSlot(new Vector1MaterialSlot(0, "Out", "Out", SlotType.Output, default(float), ShaderStageCapability.Fragment));
126 break;
127 case BlockNode.CustomBlockType.Vector2:
128 AddSlot(new Vector2MaterialSlot(0, "Out", "Out", SlotType.Output, default(Vector2), ShaderStageCapability.Fragment));
129 break;
130 case BlockNode.CustomBlockType.Vector3:
131 AddSlot(new Vector3MaterialSlot(0, "Out", "Out", SlotType.Output, default(Vector3), ShaderStageCapability.Fragment));
132 break;
133 case BlockNode.CustomBlockType.Vector4:
134 AddSlot(new Vector4MaterialSlot(0, "Out", "Out", SlotType.Output, default(Vector4), ShaderStageCapability.Fragment));
135 break;
136 }
137 RemoveSlotsNameNotMatching(new[] { 0 });
138 }
139
140 public override string GetVariableNameForSlot(int slotid)
141 {
142 var slotRef = GetSlotReference(0);
143 return GetOutputForSlot(slotRef, slotRef.slot.concreteValueType, GenerationMode.Preview);
144 }
145
146 protected internal override string GetOutputForSlot(SlotReference fromSocketRef, ConcreteSlotValueType valueType, GenerationMode generationMode)
147 {
148 // check to see if we can inline a value.
149 List<PreviewProperty> props = new List<PreviewProperty>();
150 e_targetBlockNode?.CollectPreviewMaterialProperties(props);
151
152 // if the cib is inActive, this node still might be in an active branch.
153 bool isActive = e_targetBlockNode?.isActive ?? false;
154
155 // if the cib has no input node, we can use the input property to inline a magic value.
156 bool canInline = e_targetBlockNode?.GetInputNodeFromSlot(0) == null && props.Count != 0;
157
158 // vector width of target slot
159 int toWidth = SlotTypeToWidth(valueType);
160
161 string finalResult = "";
162
163 // If cib is inactive (or doesn't exist), then we default to black (as is the case for other nodes).
164 if (!isActive || CustomInterpolatorUtils.generatorSkipFlag)
165 {
166 finalResult = ConvertVector("$precision4(0,0,0,0)", 4, toWidth);
167 }
168 // cib has no input; we can directly use the inline value instead.
169 else if (canInline)
170 {
171 Vector4 v = default;
172 if (props[0].propType != PropertyType.Float)
173 v = props[0].vector4Value;
174
175 int outWidth = 4;
176 string result;
177 switch (props[0].propType)
178 {
179 case PropertyType.Float:
180 result = $" $precision1({props[0].floatValue}) ";
181 outWidth = 1;
182 break;
183 default:
184 result = $" $precision4({v.x},{v.y},{v.z},{v.w}) ";
185 outWidth = 4;
186 break;
187 }
188 finalResult = ConvertVector(result, outWidth, toWidth);
189 }
190 // Preview Node doesn't support CI, but we can fake it by asking the cib's source input for it's value instead.
191 else if (CustomInterpolatorUtils.generatorNodeOnly)
192 {
193 var sourceSlot = FindSourceSlot(out var found);
194 // CIB's type needs to constrain the incoming value (eg. vec2(out)->float(cib) | float(cin)->vec2(in))
195 // If we didn't do this next line, we'd get vec2(out)->vec2(in), which would ignore the truncation in the preview.
196 var result = sourceSlot.node.GetOutputForSlot(sourceSlot, FindSlot<MaterialSlot>(0).concreteValueType, GenerationMode.Preview);
197 finalResult = ConvertVector(result, (int)e_targetBlockNode.customWidth, toWidth);
198 }
199 // If we made it this far, then cib is in a valid and meaningful configuration in the SDI struct.
200 else
201 {
202 // pull directly out of the SDI and just use it.
203 var result = string.Format("IN.{0}", customBlockNodeName);
204 finalResult = ConvertVector(result, (int)e_targetBlockNode.customWidth, toWidth);
205 }
206 return finalResult.Replace(PrecisionUtil.Token, concretePrecision.ToShaderString());
207 }
208
209 SlotReference FindSourceSlot(out bool found)
210 {
211 try
212 {
213 found = true;
214 return owner.GetEdges(e_targetBlockNode).First().outputSlot;
215 }
216 catch
217 {
218 found = false;
219 return default;
220 }
221 }
222
223 private static int SlotTypeToWidth(ConcreteSlotValueType valueType)
224 {
225 switch (valueType)
226 {
227 case ConcreteSlotValueType.Boolean:
228 case ConcreteSlotValueType.Vector1: return 1;
229 case ConcreteSlotValueType.Vector2: return 2;
230 case ConcreteSlotValueType.Vector3: return 3;
231 default: return 4;
232 }
233 }
234
235 private static string ConvertVector(string name, int fromLen, int toLen)
236 {
237 if (fromLen == toLen)
238 return name;
239
240 var key = new char[] { 'x', 'y', 'z', 'w' };
241
242 string begin = $"$precision{toLen}({name}.";
243 var mid = "";
244 string end = ")";
245
246 if (toLen == 4)
247 {
248 // We assume homogenous coordinates for some reason.
249 end = ", 1.0)";
250 toLen -= 1;
251 }
252
253 if (fromLen == 1)
254 {
255 // we expand floats for each component for some reason.
256 fromLen = toLen;
257 key = new char[] { 'x', 'x', 'x' };
258 }
259
260 // expand the swizzle
261 int swizzLen = Math.Min(fromLen, toLen);
262 for (int i = 0; i < swizzLen; ++i)
263 mid += key[i];
264
265 // fill gaps
266 for (int i = fromLen; i < toLen; ++i)
267 mid += ", 0.0";
268
269 // float<toLen>(<name>.<swizz>, <gap...>, 1.0)"
270 return $"({begin}{mid}{end})";
271 }
272 }
273}