Unity Shader GraphでNamed Redirectを作ってみた
こんにちは!
株式会社マトリックス 開発部 技術課の雉鳥です!
今回は、Shader Graph のノード整理に役立つ「Named Redirect」機能を実装してみたので、その過程と効果についてご紹介します。
Unreal Engine の Material Editor にある Named Reroute のようなものを Shader Graph に導入することで、ノードグラフの可読性を向上させることができました。
Named Redirectってなに?
Named Redirect は、Shader Graph 内のノード接続を、名前を介して行う機能です。
値を「Set ノード」で名前と紐付けて保存し、「Get ノード」でその名前を指定することで、値を取得できます。
これにより、ノード間を直接線で繋ぐ必要がなくなり、複雑なグラフでも見やすく整理できます。
2025/01/17時点では検討中だったので作ってみることにしました。
本来は線をつながないと出力されないが…
Named Redirectノードを経由することで線を繋がずに値を流すことが出来る
こちらはUnityのSampleにあるParticleEffect
ある程度の塊で処理をまとめられるので細部を確認しやすい。
NamedRedirect使うと何がいいの?
Named Redirect を導入することで、以下のメリットが得られます。
- ノード間の線が減り、グラフが見やすくなる
複雑な計算を行うShaderでは、ノード間の接続線が入り乱れてグラフが非常に見づらくなってしまうことがあります。
Named Redirect を使用することで、これらの接続線を削減しグラフの可読性を向上させることができます。 - 計算途中の変数に名前を付けることができる
計算の途中結果に名前を付けることで、その値が何を表しているのかを明確にすることができます。
これにより、Shader Graph の理解とメンテナンスが容易になります。
SubGraphでも整理は出来る。
SubGraphを用いることで処理をまとめることは出来ます。
ですが、現状では引数に構造体を用いることが出来ないため、
Input・Outputともに混線しやすくなります。
実装について
ここからは、Named Redirect の実装方法について解説します。
拡張準備
今回の実装ではShaderGraphパッケージの内部にも手を加えるので、
編集可能な状態にしていきます。
ShaderGraphのパッケージをLibraryフォルダからPackageフォルダに移動させます。
Setノード
- NamedRedirectSetNode
PreviewノードやRedirectノードを参考にノードを作成します。
今回は「Inputで受け取った値をOutputとして返却する関数」になるようにしたいので、
FlipNode.csを参考にしていきます。- ”Flip”となってるところは適宜リネームしていきます。
- ToggleControlは不要なので4チャンネルすべて削除します。
- Shader機能を目的の処理に書き換えます
public void GenerateNodeFunction(FunctionRegistry registry, GenerationMode generationMode){
registry.ProvideFunction(GetFunctionName(), s =>{
s.AppendLine("void {0}({1} In, {2} Flip, out {3} Out)",
GetFunctionName(),
FindInputSlot<MaterialSlot>(InputSlotId).concreteValueType.ToShaderString(),
FindInputSlot<MaterialSlot>(InputSlotId).concreteValueType.ToShaderString(),
FindOutputSlot<MaterialSlot>(OutputSlotId).concreteValueType.ToShaderString());
using (s.BlockScope()){
//s.AppendLine("Out = (Flip * -2 + 1) * In;");
s.AppendLine("Out = In;"); // 今回は入力をそのまま返却する
}
});
}
Getノード
- NamedRedirectGetNode
Setノードと同様にFlipNodeを参考にしながら作成していきます。
Inputポートは見かけ上は非表示にしますが、Setから入力受付をするため削除せずに残しておきます。
また、名前を設定した際に「対象のSetノード」を検索してスロットを接続するようにします。
public string InputNodeID {
get {
return _inputNodeID;
}
set {
_inputNodeID = value;
TryConnect();
}
}
[SerializeField]
private string _inputNodeID = String.Empty;
void TryConnect() {
var nameTargets = owner.GetNodes<NamedRedirectSetNode>().Where(n => n.name == nodeName + "_Set");
if (nameTargets.Any()) {
var edge = owner.Connect(nameTargets.First().GetSlotReference(1), GetSlotReference(0));
return;
}
var refNode = owner.GetNodeFromId(InputNodeID.Replace("_",""));
if (refNode == null) {
return;
}
{
var edge = owner.Connect(refNode.GetSlotReference(1), GetSlotReference(0));
}
}
VisualElement
Set/Getノード自体はこの時点で動作するようになってますが、
名前つけたり参照したりが出来ないのでGUIを整えます。
- NamedRedirectSetNodePropertyDrawer
- NamedRedirectSetView
- NamedRedirectGetNodePropertyDrawer
- NamedRedirectGetView
VisualElementで見た目を定義し、PropertyDrawerでNodeSettingに表示します
SetノードのViewはテキストボックスを配置して命名が出来るようにします。
GetノードのViewはドロップダウンリストでSetノードの名前を選択できるようにします。
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using UnityEditor.Graphing;
namespace UnityEditor.ShaderGraph.Drawing {
internal class NamedRedirectSetView : VisualElement {
private EnumField m_Type;
private TextField m_FunctionName;
private ObjectField m_FunctionSource;
private TextField m_FunctionBody;
internal NamedRedirectSetView(NamedRedirectSetNode setNode) {
styleSheets.Add(Resources.Load<StyleSheet>("Styles/HlslFunctionView"));
Draw(setNode);
}
private void Draw(NamedRedirectSetNode setNode) {
var currentControls = this.Children().ToArray();
for (int i = 0; i < currentControls.Length; i++) {
currentControls[i].RemoveFromHierarchy();
}
m_FunctionName = new TextField { value = setNode.nodeName, multiline = false };
m_FunctionName.RegisterCallback<FocusInEvent>(s => {
if (m_FunctionName.value == CustomFunctionNode.defaultFunctionName)
m_FunctionName.value = "";
});
m_FunctionName.RegisterCallback<FocusOutEvent>(s => {
if (m_FunctionName.value == "") {
m_FunctionName.value = CustomFunctionNode.defaultFunctionName;
} else {
m_FunctionName.value = NodeUtils.ConvertToValidHLSLIdentifier(m_FunctionName.value);
}
});
VisualElement nameRow = new VisualElement() { name = "Row" };
nameRow.Add(new Label("Name"));
nameRow.Add(m_FunctionName);
Add(nameRow);
}
}
}
using System;
using System.Reflection;
using UnityEngine.UIElements;
using UnityEngine;
namespace UnityEditor.ShaderGraph.Drawing.Inspector.PropertyDrawers{
[SGPropertyDrawer(typeof(NamedRedirectSetNode))]
public class NamedRedirectSetNodePropertyDrawer : IPropertyDrawer, IGetNodePropertyDrawerPropertyData {
private Action _mSetNodesAsDirtyCallback;
private Action _mUpdateNodeViewsCallback;
void IGetNodePropertyDrawerPropertyData.GetPropertyData(Action setNodesAsDirtyCallback, Action updateNodeViewsCallback) {
_mSetNodesAsDirtyCallback = setNodesAsDirtyCallback;
_mUpdateNodeViewsCallback = updateNodeViewsCallback;
}
VisualElement CreateGUI(NamedRedirectSetNode setNode, InspectableAttribute attribute,
out VisualElement propertyVisualElement) {
var propertySheet = new PropertySheet(PropertyDrawerUtils.CreateLabel($"{setNode.name} Node", 0, FontStyle.Bold));
PropertyDrawerUtils.AddDefaultNodeProperties(propertySheet, setNode, _mSetNodesAsDirtyCallback, _mUpdateNodeViewsCallback);
propertySheet.Add(new NamedRedirectSetView(setNode));
propertyVisualElement = null;
return propertySheet;
}
public Action inspectorUpdateDelegate { get; set; }
public VisualElement DrawProperty(PropertyInfo propertyInfo, object actualObject,
InspectableAttribute attribute) {
return this.CreateGUI(
(NamedRedirectSetNode)actualObject,
attribute,
out var propertyVisualElement);
}
void IPropertyDrawer.DisposePropertyDrawer() { }
}
}
エッジを隠す
ポートのHiddenフラグを用いることでエッジとポートを隠すことが出来ます。
public sealed override void UpdateNodeAfterDeserialization(){
AddSlot(new DynamicVectorMaterialSlot(InputSlotId, kInputSlotName, kInputSlotName, SlotType.Input, Vector4.zero,hidden:true));
AddSlot(new DynamicVectorMaterialSlot(OutputSlotId, kOutputSlotName, kOutputSlotName, SlotType.Output, Vector4.zero));
RemoveSlotsNameNotMatching(new[] { InputSlotId, OutputSlotId });
}
…と思いきや、Hiddenになってるポートは処理を中断するようになってるのでエラーが発生してしまいます。
パッケージ内の処理を変更し、Hiddenでも処理を行えるようにします。
GraphEditorView.cs L.1302
AddLine: "if (sourceSlot.hidden || targetSlot.hidden) return null;"
AbstractMaterialNode.cs L761
AddLine: "if(edge.outputSlot.slot.hidden||edge.inputSlot.slot.hidden)continue;"
名前変更を受け取れるようにする
Setノードでリネームした際に、参照してるGetノードの表示名も変える
NamedRedirectSetView.csのFocusOutEventで関連するノードを検索して変更を通知する処理を追加します
if (m_FunctionName.value != setNode.nodeName) {
if (!setNode.owner.GetNodes<NamedRedirectSetNode>().GroupBy(n => n.nodeName)
.Select(x => x.Key).Contains(m_FunctionName.value)) {
setNode.owner.owner.RegisterCompleteObjectUndo("Change Function Name");
setNode.nodeName = m_FunctionName.value;
setNode.ValidateNode();
setNode.Dirty(ModificationScope.Graph);
}
}
完成!
これでNamedRedirectの機能を作ることが出来ました。
実際にShaderGraph上で使用するとこのような感じになります。
今後の拡張
線がなくなったことでSetノードがどこにあるのかがわかりにくいため、
Getノードから探しやすくなるアップデートを検討中です。
- Getノードをダブルクリックした時にSetノードまでジャンプ
SubGraphを開くときの操作感でSetノードへ表示を切り替える - ノードに任意の色を付ける
ノード自体に色を付けることで遠くに配置せても判別しやすくなったり、
用途別に塗り分けたり出来るように - 線の表示切替
GraphWindowの機能としてNamedRedirect用の接続線を表示/非表示切り替え出来るように
最後に
いかがでしたでしょうか。
今回の拡張は既存処理の流用で作れるため拡張のハードルは低いかなと思います。
低コストの実装でグラフのスパゲッティ化改善に貢献できる拡張なのでよろしければお試しください。
Discussion