Appearance
Creating Custom Nodes
A PPG node consists of two C# classes that always travel together:
- A Settings class (
PPGSettingssubclass) — serialized, holds inspector fields, declares ports via attributes. - An Element class (
IPPGElementimplementation) — not serialized, contains the execution logic.
The two classes are linked by [PPGElement(typeof(YourElement))] placed on the Settings class.
Minimal Example
The built-in Merge node is the clearest reference for the pattern:
csharp
using System;
using UnityEngine;
namespace YourNamespace
{
[Serializable]
[PPGNodeInfo("Merge", "Point Operations", "Combines multiple point sets into one")]
[PPGInputPort("In", PPGDataType.Point, AllowMultiple = true)]
[PPGOutputPort("Out", PPGDataType.Point)]
[PPGElement(typeof(PPGMergeElement))]
public sealed class PPGMergeSettings : PPGSettings
{
public override string NodeCategory => "Point Operations";
public override Color NodeColor => new(0.8f, 0.6f, 0.2f, 1f);
}
internal sealed class PPGMergeElement : IPPGElement
{
public bool Execute(PPGContext context)
{
var inputs = context.GetInputPointData("In");
if (inputs.Count == 0) return true;
var merged = new PPGPointData { Metadata = inputs[0].Metadata.Clone() };
foreach (var input in inputs)
merged.Points.AddRange(input.Points.AsArray());
context.AddOutputData("Out", merged);
return true;
}
}
}Return true from Execute on success, false on a recoverable failure. Throw an exception for unrecoverable errors — the executor will catch it and record it in the result.
Step-by-Step: Building a Custom Node
1. Declare the Settings class
csharp
[Serializable]
[PPGNodeInfo("Scale Points", "Point Operations", "Multiplies each point's scale attribute")]
[PPGInputPort("In", PPGDataType.Point, Required = true)]
[PPGOutputPort("Out", PPGDataType.Point)]
[PPGElement(typeof(ScalePointsElement))]
public sealed class ScalePointsSettings : PPGSettings
{
// Inspector fields — serialized automatically via [SerializeReference] on PPGNode
public float ScaleFactor = 2f;
public override string NodeCategory => "Point Operations";
public override Color NodeColor => new(0.2f, 0.6f, 0.8f, 1f);
}2. Implement the Element class
csharp
internal sealed class ScalePointsElement : IPPGElement
{
public bool Execute(PPGContext context)
{
var settings = context.GetSettings<ScalePointsSettings>();
var input = context.GetFirstInputPointData("In");
if (input == null) return true;
var output = new PPGPointData { Metadata = input.Metadata?.Clone() };
var points = input.Points.AsArray();
for (var i = 0; i < points.Length; i++)
{
var p = points[i];
// Scale is embedded in the transform matrix columns
var factor = settings.ScaleFactor;
p.transform.c0 *= new float4(factor, factor, factor, 1f);
p.transform.c1 *= new float4(factor, factor, factor, 1f);
p.transform.c2 *= new float4(factor, factor, factor, 1f);
output.Points.Add(p);
}
context.AddOutputData("Out", output);
return true;
}
}3. (Optional) Add seed support
Override UseSeed in the Settings class and read the seed from the context:
csharp
public override bool UseSeed => true;
// Inside Execute:
var rng = new Unity.Mathematics.Random((uint)context.Seed);
var offset = rng.NextFloat3Direction() * settings.ScaleFactor;PPGSettings — Virtual Members
Override these in your Settings class to control editor appearance and behaviour.
| Member | Default | Purpose |
|---|---|---|
NodeTitle | Class name minus PPG prefix and Settings suffix | Header text in the graph editor |
NodeCategory | "Uncategorized" | Group in the node search menu |
NodeColor | Color(0.3, 0.3, 0.3, 1) | Header background tint |
UseSeed | false | Shows a Seed field on the node; seed is injected into PPGContext.Seed |
SupportsParameterExpose | true | Whether fields can be promoted to graph-level parameters |
InputPorts() | Reads [PPGInputPort] attributes | Override for dynamic port lists |
OutputPorts() | Reads [PPGOutputPort] attributes | Override for dynamic port lists |
CreateElement() | Reads [PPGElement] attribute | Override to pass constructor arguments to the element |
Port Attributes
[PPGInputPort]
Applied at class level, one attribute per port. Order in source determines display order.
csharp
[PPGInputPort("Points", PPGDataType.Point)]
[PPGInputPort("Mask", PPGDataType.Point, Required = false, AllowMultiple = false)]| Property | Type | Default | Description |
|---|---|---|---|
Name | string | — | Port label; used as the key when reading PPGContext.InputData |
DataType | PPGDataType | — | Wire type (Point, Surface, Spline, etc.) |
Required | bool | false | Validator warns if no connection is made |
AllowMultiple | bool | false | Allows more than one upstream connection to this port |
[PPGOutputPort]
csharp
[PPGOutputPort("Out", PPGDataType.Point)]| Property | Type | Description |
|---|---|---|
Name | string | Port label; used as the key when writing to PPGContext.OutputData |
DataType | PPGDataType | Wire type |
[PPGNodeInfo] Attribute
Provides static editor metadata; takes priority over the virtual NodeTitle / NodeCategory properties when present.
csharp
[PPGNodeInfo(
displayName: "Scale Points",
category: "Point Operations",
description: "Multiplies each point's scale by a constant factor")]| Parameter | Type | Required | Description |
|---|---|---|---|
displayName | string | yes | Node header label |
category | string | yes | Group in the Add Node search menu |
description | string | no (default "") | Tooltip in the search menu |
PPGContext — Reading Inputs and Writing Outputs
PPGContext is passed to IPPGElement.Execute. Use its helpers rather than accessing the raw InputData / OutputData collections directly.
| Method / Property | Description |
|---|---|
GetSettings<T>() | Returns the node's Settings cast to T |
GetInputPointData(pinLabel) | Returns all PPGPointData connected to the named input port |
GetFirstInputPointData(pinLabel) | Returns the first (or only) PPGPointData on the named port |
AddOutputData(pinLabel, data) | Adds a PPGData item to the named output port |
GetComponentTransform() | Returns (Vector3 position, Quaternion rotation) of the driving PPGComponent |
CheckCancellation(nodeName, progress) | Throws OperationCanceledException if the user has requested cancellation. Call periodically in long loops. |
Seed | int — per-execution seed (injected from PPGComponent.Seed) |
SourceComponent | The PPGComponent driving execution |
Variables | Shared Dictionary<string, PPGDataCollection> for Set/Get Variable nodes |
Cancellation pattern for long-running nodes
csharp
public bool Execute(PPGContext context)
{
var points = context.GetInputPointData("In");
for (var i = 0; i < points.Count; i++)
{
// Report progress roughly every 5 % to avoid call overhead
if (i % 50 == 0)
context.CheckCancellation("MyHeavyNode", (float)i / points.Count);
// ... heavy work ...
}
return true;
}Dynamic Ports
For nodes whose port count depends on serialized data (e.g., a blend node with a variable number of inputs), override InputPorts() or OutputPorts() on the Settings class:
csharp
[Serializable]
[PPGNodeInfo("Blend", "Blend", "Blends N inputs by weight")]
[PPGOutputPort("Out", PPGDataType.Point)]
[PPGElement(typeof(BlendElement))]
public sealed class BlendSettings : PPGSettings
{
public int InputCount = 2;
public override IReadOnlyList<PPGPort> InputPorts()
{
var ports = new PPGPort[this.InputCount];
for (var i = 0; i < this.InputCount; i++)
ports[i] = PPGPort.CreateInput($"In{i}", PPGDataType.Point);
return ports;
}
}Note: The base InputPorts() and OutputPorts() implementations cache results by type using ConcurrentDictionary. When you override these methods and the result depends on instance data (as above), the caching is bypassed automatically because you are providing the full implementation.