Code/utils/ParticleAttributesSetter.cs

Utility that sets renderer attributes for particles. ParticleAttributesSetter evaluates various typed particle parameters (floats, vectors, colors, textures, combos) for a given Particle and writes them into a RenderAttributes instance; AnimatedAttributes records which attributes vary over a particle's lifetime and builds a compact list of entries for per-frame updates.

Native Interop
using System;
using System.Collections.Generic;

namespace Sandbox;

/**
 * Simple class designed to set render attributes accounting for particule timings
 */
public class ParticleAttributesSetter
{
	private RenderAttributes _renderAttributes;
	private Particle _particle;
	
	public Dictionary<String, ParticleGradient> Colors { get; set; }
	public Dictionary<String, ParticleFloat> Floats { get; set; }
	public Dictionary<String, ParticleVector2> Floats2 { get; set; }
	public Dictionary<String, ParticleVector3> Floats3 { get; set; }
	public Dictionary<String, Vector4> Floats4 { get; set; }
	public Dictionary<String, Texture> Textures { get; set; }
	public Dictionary<String, int> DynamicCombos { get; set; }

	/// <summary>
	/// The renderer's attributes that change over a particle's life, shared by all of its particles.
	/// </summary>
	public AnimatedAttributes Animated { get; set; }
	
	public ParticleAttributesSetter(RenderAttributes renderAttributes, Particle particle)
	{
		_renderAttributes = renderAttributes;
		_particle = particle;
	}

	/// <summary>
	/// Sets every attribute. Call once when the particle spawns.
	/// </summary>
	public void SetAttributes()
	{
		foreach ( var dynamicCombo in DynamicCombos )
			_renderAttributes.SetCombo( dynamicCombo.Key, dynamicCombo.Value );

		foreach ( var pair in Floats )
			_renderAttributes.Set( pair.Key, EvaluateFloat( pair.Value ) );

		foreach ( var pair in Colors )
			_renderAttributes.Set( pair.Key, EvaluateColor( pair.Value ) );

		foreach ( var pair in Floats2 )
			_renderAttributes.Set( pair.Key, EvaluateFloat2( pair.Value ) );

		foreach ( var pair in Floats3 )
			_renderAttributes.Set( pair.Key, EvaluateFloat3( pair.Value ) );

		foreach ( var float4 in Floats4 )
			_renderAttributes.Set( float4.Key, float4.Value );

		foreach ( var texture in Textures )
			_renderAttributes.Set( texture.Key, texture.Value );
	}

	/// <summary>
	/// Sets only the attributes whose value changes over the particle's life. The scene object keeps the
	/// others from <see cref="SetAttributes"/>.
	/// </summary>
	public void SetAnimatedAttributes()
	{
		if ( Animated is null ) return;

		var entries = Animated.Entries;
		for ( int i = 0; i < entries.Count; i++ )
		{
			var a = entries[i];
			switch ( a.Kind )
			{
				case AnimatedAttributes.Kind.Float: _renderAttributes.Set( a.Token, EvaluateFloat( a.Float ) ); break;
				case AnimatedAttributes.Kind.Float2: _renderAttributes.Set( a.Token, EvaluateFloat2( a.Float2 ) ); break;
				case AnimatedAttributes.Kind.Float3: _renderAttributes.Set( a.Token, EvaluateFloat3( a.Float3 ) ); break;
				case AnimatedAttributes.Kind.Color: _renderAttributes.Set( a.Token, EvaluateColor( a.Color ) ); break;
			}
		}
	}

	// One call site per type: the per-particle random seed depends on the calling line, so spawn and
	// per-frame evaluation have to go through the same one to agree.
	private float EvaluateFloat( in ParticleFloat value ) => value.Evaluate( _particle, 6211 );
	private Vector2 EvaluateFloat2( ParticleVector2 value ) => value.Evaluate( _particle, 6211 );
	private Vector3 EvaluateFloat3( in ParticleVector3 value ) => value.Evaluate( _particle, 6211 );
	private Color EvaluateColor( in ParticleGradient value ) => value.Evaluate( _particle, 6211 );
}

/// <summary>
/// The attributes of a renderer that change over a particle's life, with their names already converted to
/// tokens. Rebuilt by the renderer on the main thread before each particle step (so inspector edits apply
/// straight away), then only read by the particles during the parallel step.
/// </summary>
public sealed class AnimatedAttributes
{
	public enum Kind : byte { Float, Float2, Float3, Color }

	public struct Entry
	{
		public Kind Kind;
		public StringToken Token;
		public ParticleFloat Float;
		public ParticleVector2 Float2;
		public ParticleVector3 Float3;
		public ParticleGradient Color;
	}

	public List<Entry> Entries { get; } = new();

	public void Rebuild( Dictionary<String, ParticleFloat> floats, Dictionary<String, ParticleVector2> floats2,
		Dictionary<String, ParticleVector3> floats3, Dictionary<String, ParticleGradient> colors )
	{
		Entries.Clear();

		if ( floats is not null )
			foreach ( var pair in floats )
				if ( Varies( pair.Value ) ) Entries.Add( new Entry { Kind = Kind.Float, Token = pair.Key, Float = pair.Value } );

		if ( colors is not null )
			foreach ( var pair in colors )
				if ( Varies( pair.Value ) ) Entries.Add( new Entry { Kind = Kind.Color, Token = pair.Key, Color = pair.Value } );

		if ( floats2 is not null )
			foreach ( var pair in floats2 )
				if ( Varies( pair.Value.X ) || Varies( pair.Value.Y ) ) Entries.Add( new Entry { Kind = Kind.Float2, Token = pair.Key, Float2 = pair.Value } );

		if ( floats3 is not null )
			foreach ( var pair in floats3 )
				if ( Varies( pair.Value.X ) || Varies( pair.Value.Y ) || Varies( pair.Value.Z ) ) Entries.Add( new Entry { Kind = Kind.Float3, Token = pair.Key, Float3 = pair.Value } );
	}

	// Constant values, and values picked once per particle from its seed, don't change after spawn
	private static bool Varies( in ParticleFloat value ) =>
		value.Type != ParticleFloat.ValueType.Constant && value.Evaluation != ParticleFloat.EvaluationType.Seed;

	private static bool Varies( in ParticleGradient value ) =>
		value.Type != ParticleGradient.ValueType.Constant && value.Evaluation != ParticleGradient.EvaluationType.Particle;
}