Skip to content

Creating Custom Nodes

A PPG node consists of two C# classes that always travel together:

  • A Settings class (PPGSettings subclass) — serialized, holds inspector fields, declares ports via attributes.
  • An Element class (IPPGElement implementation) — 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.

MemberDefaultPurpose
NodeTitleClass name minus PPG prefix and Settings suffixHeader text in the graph editor
NodeCategory"Uncategorized"Group in the node search menu
NodeColorColor(0.3, 0.3, 0.3, 1)Header background tint
UseSeedfalseShows a Seed field on the node; seed is injected into PPGContext.Seed
SupportsParameterExposetrueWhether fields can be promoted to graph-level parameters
InputPorts()Reads [PPGInputPort] attributesOverride for dynamic port lists
OutputPorts()Reads [PPGOutputPort] attributesOverride for dynamic port lists
CreateElement()Reads [PPGElement] attributeOverride 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)]
PropertyTypeDefaultDescription
NamestringPort label; used as the key when reading PPGContext.InputData
DataTypePPGDataTypeWire type (Point, Surface, Spline, etc.)
RequiredboolfalseValidator warns if no connection is made
AllowMultipleboolfalseAllows more than one upstream connection to this port

[PPGOutputPort]

csharp
[PPGOutputPort("Out", PPGDataType.Point)]
PropertyTypeDescription
NamestringPort label; used as the key when writing to PPGContext.OutputData
DataTypePPGDataTypeWire 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")]
ParameterTypeRequiredDescription
displayNamestringyesNode header label
categorystringyesGroup in the Add Node search menu
descriptionstringno (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 / PropertyDescription
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.
Seedint — per-execution seed (injected from PPGComponent.Seed)
SourceComponentThe PPGComponent driving execution
VariablesShared 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.

Procedural Placement Graph for Unity