Appearance
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:
PPGMetadata— the actual storage (column-oriented, typed attribute arrays).PPGAttributeSelector— a serializable, type-safe reference to either a built-in point property or a named metadata attribute.PPGAttributeAccessor— static helpers that dispatch reads/writes through aPPGAttributeSelector.
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.TypeKey methods
| Method | Description |
|---|---|
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
| Index | XYZ alias | RGBA alias |
|---|---|---|
| 0 | X | R |
| 1 | Y | G |
| 2 | Z | B |
| 3 | W | A |
Selector properties
| Property | Type | Description |
|---|---|---|
IsValid | bool | true if the selector has a usable target. |
IsPointProperty | bool | true if targeting a built-in PPGPointProperty. |
IsCustomAttribute | bool | true if targeting a named metadata column. |
HasComponent | bool | true if a sub-component index is set. |
ComponentIndex | int | Sub-component index (0–3). Meaningful only when HasComponent is true. |
PointProperty | PPGPointProperty | The built-in property, or None for custom attributes. |
CustomName | string | The metadata attribute name, or null for built-in properties. |
DisplayName | string | Human-readable label (e.g., "Position.X", "WindDirection"). |
ResolvedType | PPGAttributeType | Returns 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 type | ReadFloat result | ReadInt result |
|---|---|---|
Float | direct | cast to int |
Int | cast to float | direct |
Bool | 1f / 0f | 1 / 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] |
String | 0f | 0 |
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
| Value | C# Type | Description |
|---|---|---|
Float | float | Single-precision scalar |
Int | int | Integer scalar |
Bool | bool | Boolean |
Float2 | Unity.Mathematics.float2 | 2-component vector |
Float3 | Unity.Mathematics.float3 | 3-component vector |
Float4 | Unity.Mathematics.float4 | 4-component vector / color |
Quaternion | Unity.Mathematics.quaternion | Rotation |
String | string | Arbitrary text |