Creating Custom Nodes for Visual Compositor
Create the node class.
Custom node classes need to inherit from the Unity.VisualCompositor.CompositorNode
base class.
This class is an abstract base class, which requires a single method to be implemented:
using Unity.VisualCompositor;
public class MyCustomNode : CompositorNode {
public override void Render() { }
}
Visual Compositor will call this method when executing its Compositor Graph asset.
UI Controls
To construct controls for the UI of the custom node, declare fields with [ExposeField]
attributes.
For example:
[ExposeField] public float param;
This will create a FloatField
in the node as parameters.
We can use almost any serializable type, where each will create its corresponding Unity UI.
Node Inputs/Outputs
Node Inputs
In order to get input into the custom node from the outputs of other nodes,
we decorate its fields with [InputPort]
special attribute.
When the Render()
method is called on the custom node,
these fields will have values in them ready to be used.
For example:
[InputPort(name: "input")] private RenderTexture m_input;
This declares a field named m_input
, which is labeled as input
and
able to receive a RenderTexture from another node.
There can be as many input nodes defined with any types.
The Selection Group node for example,
outputs a value of type HashSet<GameObject>
.
Node Outputs
Visual Compositor will automatically copy the values of all output fields to any connected nodes.
These output fields are fields decorated with [OutputPort]
attributes and
we can set their values inside the Render()
method of the custom node.
For example:
[OutputPort(name: "output")] private RenderTexture m_output;
This declares an output port named m_output
, which is labeled as output
and
able to send a RenderTexture to any connected node.
Node UI Extension
A custom node may be decorated with a [CompositorNode]
attribute
with the following parameters for further customization.
No | Name | Description | Required |
---|---|---|---|
1 | title | The name used in the create node menu. | ✔️ |
2 | tooltip | The documentation string (tooltip) for the node. | ✔️ |
3 | w | The width of the node widget in the editor. | |
4 | userCreatable | If true, allows users to create the node through the 'Create Node' menu. | |
5 | icon | The path to a custom icon for the node. |
We can also extend the UI of a custom node by writing a class
inheriting from CustomCompositorNodeEditor
with [CompositorNodeEditor]
attribute.
For example:
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.
}
}
Complete Example
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;
}