Skip to content

Attribute System

PPG's attribute system is the mechanism by which per-point data beyond position and rotation is stored, accessed, and passed between nodes. It has three layers:

  1. PPGMetadata — the actual storage (column-oriented, typed attribute arrays).
  2. PPGAttributeSelector — a serializable, type-safe reference to either a built-in point property or a named metadata attribute.
  3. PPGAttributeAccessor — static helpers that dispatch reads/writes through a PPGAttributeSelector.

Two interfaces, IPPGAttributeProvider and IPPGAttributeConsumer, let nodes declare at the Settings level which attributes they produce or consume.


PPGMetadata

Each PPGPointData owns a PPGMetadata instance. Metadata is column-oriented: each named attribute is a typed column, and each point holds an long entry key that indexes into those columns.

Creating and populating metadata

csharp
var metadata = new PPGMetadata();

// Create typed attribute columns
metadata.CreateAttribute<float>("Density");
metadata.CreateAttribute<Unity.Mathematics.float3>("WindDirection");
metadata.CreateAttribute<int>("VariantIndex");

// Allocate an entry for each point and set values
for (var i = 0; i < points.Length; i++)
{
    var entryKey = metadata.AddEntry();   // returns a long key
    points[i].metadataEntry = entryKey;  // link the point to its entry

    metadata.SetValue(entryKey, "Density",       rng.NextFloat());
    metadata.SetValue(entryKey, "WindDirection",  new float3(1, 0, 0));
    metadata.SetValue(entryKey, "VariantIndex",   rng.NextInt(0, 4));
}

Reading metadata directly

csharp
if (metadata.HasAttribute("Density"))
{
    var density = metadata.GetValue<float>(point.metadataEntry, "Density");
    Debug.Log($"Density: {density}");
}

// Check attribute type before reading
var attrType = metadata.GetAttributeType("WindDirection");  // returns System.Type

Key methods

MethodDescription
AddEntry()Allocates a new entry key (long). Call once per point.
CreateAttribute<T>(name, defaultValue?)Creates a new typed column. No-op if the name already exists.
HasAttribute(name)Returns true if the named column exists.
GetAttributeType(name)Returns the System.Type of the named column, or null.
GetAttributeTypeEnum(name)Returns the PPGAttributeType enum value of the named column.
SetValue<T>(entryKey, name, value)Writes a value to the specified entry. Creates the column if it does not exist.
GetValue<T>(entryKey, name)Reads a value from the specified entry. Returns default(T) if not found.
Clone()Deep-copies the metadata, including all columns and all entry values.

PPGAttributeSelector

PPGAttributeSelector is a serializable struct used in node Settings classes wherever a user-selectable attribute reference is needed. It encodes three things in one struct:

  • Whether the target is a built-in PPGPointProperty (Position, Rotation, Scale, Density, …) or a custom metadata attribute name.
  • An optional component index for vector properties (e.g., Position.X → index 0).
  • Validation logic (IsValid, IsPointProperty, IsCustomAttribute).

Creating selectors in code

csharp
// Built-in property (e.g., Density)
var selector = PPGAttributeSelector.BuiltIn(PPGPointProperty.Density);

// Sub-component of a built-in property (Position.X)
var xSelector = PPGAttributeSelector.BuiltInComponent(PPGPointProperty.Position, 0);

// Custom metadata attribute
var customSelector = PPGAttributeSelector.Custom("WindDirection");

// Sub-component of a custom attribute (WindDirection.Y)
var windY = PPGAttributeSelector.CustomComponent("WindDirection", 1);

// Parse from a legacy string name (e.g., from older serialized data)
var legacySelector = PPGAttributeSelector.FromLegacyName("Position.X");

Component index mapping

IndexXYZ aliasRGBA alias
0XR
1YG
2ZB
3WA

Selector properties

PropertyTypeDescription
IsValidbooltrue if the selector has a usable target.
IsPointPropertybooltrue if targeting a built-in PPGPointProperty.
IsCustomAttributebooltrue if targeting a named metadata column.
HasComponentbooltrue if a sub-component index is set.
ComponentIndexintSub-component index (0–3). Meaningful only when HasComponent is true.
PointPropertyPPGPointPropertyThe built-in property, or None for custom attributes.
CustomNamestringThe metadata attribute name, or null for built-in properties.
DisplayNamestringHuman-readable label (e.g., "Position.X", "WindDirection").
ResolvedTypePPGAttributeTypeReturns Float when a sub-component is selected; otherwise the natural attribute type.

Using a selector in a Settings class

csharp
[Serializable]
[PPGNodeInfo("Remap Attribute", "Attributes", "Remaps one attribute's value range")]
[PPGInputPort("In",  PPGDataType.Point, Required = true)]
[PPGOutputPort("Out", PPGDataType.Point)]
[PPGElement(typeof(RemapAttributeElement))]
public sealed class RemapAttributeSettings : PPGSettings
{
    // Shown as a dropdown in the Inspector — picks from upstream attributes
    public PPGAttributeSelector SourceAttribute = PPGAttributeSelector.BuiltIn(PPGPointProperty.Density);
    public float InputMin  = 0f;
    public float InputMax  = 1f;
    public float OutputMin = 0f;
    public float OutputMax = 1f;
}

PPGAttributeAccessor

PPGAttributeAccessor is a static class that provides unified reads and writes through a PPGAttributeSelector. Use it inside Element Execute methods instead of writing your own dispatch logic.

Reading float values

csharp
// Inside IPPGElement.Execute:
var settings = context.GetSettings<RemapAttributeSettings>();
var input    = context.GetFirstInputPointData("In");
var output   = new PPGPointData { Metadata = input.Metadata?.Clone() };

var points = input.Points.AsArray();
for (var i = 0; i < points.Length; i++)
{
    var point = points[i];

    // Read from built-in or custom attribute transparently
    var raw     = PPGAttributeAccessor.ReadFloat(settings.SourceAttribute, point, input.Metadata);
    var remapped = Mathf.Lerp(settings.OutputMin, settings.OutputMax,
                              Mathf.InverseLerp(settings.InputMin, settings.InputMax, raw));

    // Write back — WriteFloat returns the mutated point struct
    point = PPGAttributeAccessor.WriteFloat(settings.SourceAttribute, point, output.Metadata, remapped);
    output.Points.Add(point);
}

context.AddOutputData("Out", output);

Reading int values

csharp
var variantIndex = PPGAttributeAccessor.ReadInt(selector, point, metadata);

Type coercion rules

PPGAttributeAccessor coerces between attribute types automatically:

Source typeReadFloat resultReadInt result
Floatdirectcast to int
Intcast to floatdirect
Bool1f / 0f1 / 0
Float2/3/4 (no component).x component(int).x component
Float2/3/4 (with component)indexed component(int) indexed component
Quaternion (no component).value.x(int).value.x
Quaternion (with component).value[index](int).value[index]
String0f0

When the selector points to a built-in property, the read dispatches directly to the PPGPoint struct field — no metadata lookup occurs.


IPPGAttributeProvider

Implement this interface on your Settings class when your node creates custom metadata attributes. The editor uses the information to populate the attribute selector dropdown for downstream nodes.

csharp
[Serializable]
[PPGNodeInfo("Tag By Slope", "Attributes", "Adds a SlopeFactor float attribute to each point")]
[PPGInputPort("In",  PPGDataType.Point)]
[PPGOutputPort("Out", PPGDataType.Point)]
[PPGElement(typeof(TagBySlopeElement))]
public sealed class TagBySlopeSettings : PPGSettings, IPPGAttributeProvider
{
    public string AttributeName = "SlopeFactor";

    public IEnumerable<(string name, PPGAttributeType type)> GetProvidedAttributes()
    {
        yield return (this.AttributeName, PPGAttributeType.Float);
    }
}

The interface has a single method:

csharp
IEnumerable<(string name, PPGAttributeType type)> GetProvidedAttributes();

Return one tuple per attribute column your node writes to the output metadata.


IPPGAttributeConsumer

Implement this interface on your Settings class when your node reads custom metadata attributes by name. The graph validator uses this information to warn when a referenced attribute does not exist in any upstream IPPGAttributeProvider.

csharp
public sealed class RemapAttributeSettings : PPGSettings,
    IPPGAttributeConsumer
{
    public PPGAttributeSelector SourceAttribute;

    public IEnumerable<string> GetConsumedCustomAttributes()
    {
        // Only report custom attributes; built-in properties are always available
        if (this.SourceAttribute.IsCustomAttribute)
            yield return this.SourceAttribute.CustomName;
    }
}

The interface has a single method:

csharp
IEnumerable<string> GetConsumedCustomAttributes();

Return the names of all metadata columns your node reads. Do not include built-in PPGPointProperty names here.


Combining Provider and Consumer

A node can implement both interfaces — for example, a node that reads one attribute and writes a derived attribute:

csharp
public sealed class NormalizeAttributeSettings : PPGSettings,
    IPPGAttributeProvider,
    IPPGAttributeConsumer
{
    public string SourceName = "RawWeight";
    public string OutputName = "NormalizedWeight";

    public IEnumerable<(string, PPGAttributeType)> GetProvidedAttributes()
    {
        yield return (this.OutputName, PPGAttributeType.Float);
    }

    public IEnumerable<string> GetConsumedCustomAttributes()
    {
        yield return this.SourceName;
    }
}

PPGAttributeType Enum

ValueC# TypeDescription
FloatfloatSingle-precision scalar
IntintInteger scalar
BoolboolBoolean
Float2Unity.Mathematics.float22-component vector
Float3Unity.Mathematics.float33-component vector
Float4Unity.Mathematics.float44-component vector / color
QuaternionUnity.Mathematics.quaternionRotation
StringstringArbitrary text

Procedural Placement Graph for Unity