Visual Compositor カスタムノードクラスの作成
ノードクラスの作成
カスタムノードクラスは Unity.VisualCompositor.CompositorNode
ベースクラスを継承する必要があります。
このクラスは抽象的な基底クラスで、1つのメソッドを実装する必要があります。
using Unity.VisualCompositor;
public class MyCustomNode : CompositorNode {
public override void Render() { }
}
Visual Compositor は、 Compositor Graph アセットを実行する際にこのメソッドを呼び出します。
UI コントロールの作成
カスタムノードのUIを作成するために、[ExposeField]
属性でフィールドを宣言します。
例:
[ExposeField] public float param;
これはパラメータとして、ノード内に FloatField
を作成します。
ほぼすべてのシリアライズ可能な型を使用することができ、それぞれが対応するUnity UIを作成することになります。
Node Inputs/Outputs
Node Inputs
他のノードの出力からカスタムノードへの入力を得るために、フィールドを [InputPort]
という特別な属性で飾ります。
そのフィールドを [InputPort]
という特別な属性で装飾します。
カスタムノードで Render()
メソッドを呼び出すと、これらのフィールドに値が格納されます。
これらのフィールドには、すぐに使える値が格納されます。
例:
[InputPort(name: "input")] private RenderTexture m_input;
これは m_input
というフィールドを宣言しており、input
というラベルを付けて、他のノードから RenderTexture を受け取ることができます。
入力ノードは、任意の種類をいくつでも定義することができます。
例えば、Selection Group というノードがあります。
は HashSet<GameObject>
という型を持つ値を出力します。
Node Outputs
Visual Compositorは、すべての出力フィールドの値を、接続されているノードに自動的にコピーします。
これらの出力フィールドは [OutputPort]
属性で修飾されており、カスタムノードの Render()
メソッド内で値を設定することができます。
例:
[OutputPort(name: "output")] private RenderTexture m_output;
これは m_output
という名前の出力ポートを宣言しており、output
というラベルを付けて、接続されているノードに RenderTexture を送ることができます。
RenderTextureを接続されたノードに送信することができます。
Node UI Extension
カスタムノードは [CompositorNode]
属性でデコレートすることができ、さらにカスタマイズするために以下のパラメータで指定します。
番号 | 名前 | 解説 | 必須 |
---|---|---|---|
1 | title | 'Create Node' メニューで使用する名前です。 | ✔️ |
2 | tooltip | ノードのドキュメント文字列(ツールチップ)です。 | ✔️ |
3 | w | エディタに表示されるノードウィジェットの幅。 | |
4 | userCreatable | trueの場合、ユーザーが 'Create Node' メニューからノードを作成できるようにします。 | |
5 | icon | ノードのカスタムアイコンへのパス。 |
また、カスタムノードのUIを拡張するには、CustomCompositorNodeEditor
を継承したクラスを [CompositorNodeEditor]
属性付きで記述します。
例えば
using Unity.VisualCompositor.Editor;
[CompositorNodeEditor(typeof(MyCustomNode))]
public class MyCustomNodeEditor : CustomCompositorNodeEditor {
public override NodeUI CreateUI(CompositorNode compositorNode) {
// This method is called first. If you need use the node
// in the below method, capture it here.
// Eg: this.node = compositorNode;
return new NodeUI();
}
public override void ConfigureUI(NodeUI ui) {
// This is called last, you can modify the default UI here.
}
}
完成例
Runtime Script
using UnityEngine;
using Unity.VisualCompositor;
// Any class that inherits Compositor Node will be available to use in the Compositor.
[CompositorNode("My Custom Node", "A node.", icon: "SpeechBubble.png")]
public class MyCustomNode : CompositorNode {
// This method implements the functionality of the node. You can read from input port fields and write to output port fields,
// the Compositor takes care of moving those values around the graph.
public override void Render() {
if (null == m_input) {
if (null != m_output) {
ClearRenderTexture(m_output);
}
return;
}
if (null == m_output) {
m_output = new RenderTexture(m_input);
m_output.hideFlags = HideFlags.DontSaveInEditor;
}
if(m_material == null) {
Shader shader = Shader.Find("MyCustomNodeShader");
m_material = new Material(shader);
m_material.hideFlags = HideFlags.DontSaveInEditor;
}
Graphics.Blit(m_input, m_output, m_material);
}
private static void ClearRenderTexture(RenderTexture rt) {
RenderTexture prevRT = RenderTexture.active;
RenderTexture.active = rt;
GL.Clear(true, true, Color.clear);
RenderTexture.active = prevRT;
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------------
// Fields that have an InputPort attribute will become input ports in the Compositor Editor.
[InputPort(name: "input")] private RenderTexture m_input;
// Fields that have an OutputPort attribute will become output ports in the Compositor Editor.
[OutputPort(name: "output")] private RenderTexture m_output;
// Fields that have an ExposeField attribute will have the appropriate ui controls created in
// the node editor. Any serializable field types can be used.
[ExposeField] public float param;
Material m_material;
}
Shader
Shader "MyCustomNodeShader" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
}
SubShader {
// No culling or depth
Cull Off ZWrite Off ZTest Always
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
float4 frag(v2f i) : SV_Target {
float4 c = tex2D(_MainTex, i.uv);
const half t = c.r;
c.r = c.g;
c.g = c.b;
c.b = t;
return c;
}
ENDCG
}
}
}
Editor Script
using Unity.VisualCompositor;
using Unity.VisualCompositor.Editor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
[CompositorNodeEditor(typeof(MyCustomNode))]
public class MyCustomNodeEditor : CustomCompositorNodeEditor {
public override NodeUI CreateUI(CompositorNode compositorNode) {
NodeUI nodeUI = new NodeUI();
m_node = compositorNode as MyCustomNode;
return nodeUI;
}
public override void ConfigureUI(NodeUI ui) {
ui.tooltip = "Hey, this is a customised tool tip!";
Label label = new Label("Custom Editor");
ui.extensionContainer.Add(label);
TextField textField = new TextField() {
label = "Foo",
value = "Bar",
};
ui.extensionContainer.Add(textField);
ui.extensionContainer.Add(new ObjectField("HogeObj") { objectType = typeof(GameObject) });
Button button = new Button() { text = "Increase Param", };
button.clicked += () => { ++m_node.param; };
ui.extensionContainer.Add(button);
}
private MyCustomNode m_node = null;
}