EmitterModelAttributes.cs

Component that drives a ShaderParticleModelRenderer's shader attributes from a ParticleEmitter over the emitter's lifetime. It samples float curves, gradients, vectors, textures and dynamic combo ints each frame and writes them into the renderer so all live particles share the same values.

File Access
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sandbox;

[Title( "EmitterModelAttributes" )]
[Category( "Shaders" )]
[Description(
	"Drives the shader render attributes of a ShaderParticleModelRenderer over the lifetime of a ParticleEmitter. " +
	"The animated values are sampled from the emitter's normalized time and pushed into the renderer so every live particle shares them." )]
public sealed class EmitterModelAttributes : Component, Component.ExecuteInEditor
{
	/// <summary>
	/// The renderer whose shader attribute dictionaries get overwritten each frame.
	/// </summary>
	[Property] public ShaderParticleModelRenderer Renderer { get; set; }

	/// <summary>
	/// The emitter whose lifetime (Duration + Delay) drives the normalized time used to sample the curves.
	/// </summary>
	[Property] public ParticleEmitter Emitter { get; set; }

	[Property, Group( "Floats1" )] public Dictionary<String, ParticleFloat> Floats { get; set; }
	[Property, Group( "Colors" )] public Dictionary<String, Gradient> Colors { get; set; }
	[Property, Group( "Floats2" )] public Dictionary<String, Vector2> Floats2 { get; set; }
	[Property, Group( "Floats4" )] public Dictionary<String, Vector4> Floats4 { get; set; }
	[Property, Group( "Textures" )] public Dictionary<String, Texture> Textures { get; set; }
	[Property, Group( "DynamicCombos" )] public Dictionary<String, int> DynamicCombos { get; set; }

	/// <summary>
	/// When true, the emitter's Delay is included so the time stays at 0 during the delay.
	/// </summary>
	[Property] public bool IncludeDelay { get; set; } = true;

	// Looping is driven by the emitter, never by a local flag. In the editor we always
	// loop so the animation previews continuously.
	private bool ShouldLoop => Scene.IsEditor || (Emitter.IsValid() && Emitter.Loop);

	protected override void OnStart()
	{
		Floats ??= new Dictionary<string, ParticleFloat>();
		Colors ??= new Dictionary<string, Gradient>();
		Floats2 ??= new Dictionary<string, Vector2>();
		Floats4 ??= new Dictionary<string, Vector4>();
		Textures ??= new Dictionary<string, Texture>();
		DynamicCombos ??= new Dictionary<string, int>();

		ApplyAttributes();
	}

	protected override void OnUpdate()
	{
		ApplyAttributes();
	}

	/// <summary>
	/// Normalized emitter time in [0,1] accounting for delay and looping.
	/// Driven entirely off the emitter's own playback clock so it can never drift out of
	/// sync, including on restart, playback scrubbing or time scaling.
	/// </summary>
	private float ComputeTime()
	{
		if ( !Emitter.IsValid() )
			return 0f;

		var duration = Emitter.Duration.Evaluate( 0f, 0f );
		if ( duration <= 0f )
			return 0f;

		var delay = IncludeDelay ? Emitter.Delay.Evaluate( 0f, 0f ) : 0f;

		// Emitter.time spans the whole [0, delay + duration] lifetime; the animation only
		// starts once the delay has elapsed.
		var elapsed = Emitter.time - delay;
		if ( elapsed <= 0f )
			return 0f;

		var raw = elapsed / duration;
		return ShouldLoop ? raw % 1f : raw.Clamp( 0f, 1f );
	}

	private void ApplyAttributes()
	{
		if ( !Renderer.IsValid() )
			return;

		// The renderer initializes its dictionaries in its own OnStart; component start
		// order isn't guaranteed, so make sure they exist before we write into them.
		Renderer.Floats ??= new Dictionary<string, ParticleFloat>();
		Renderer.Colors ??= new Dictionary<string, ParticleGradient>();
		Renderer.Floats2 ??= new Dictionary<string, ParticleVector2>();
		Renderer.Floats4 ??= new Dictionary<string, Vector4>();
		Renderer.Textures ??= new Dictionary<string, Texture>();
		Renderer.DynamicCombos ??= new Dictionary<string, int>();

		var t = ComputeTime();

		SetFloatAttributes( t );
		SetColorAttributes( t );
		SetFloat2Attributes();
		SetFloat4Attributes();
		SetTextureAttributes();
		SetDynamicCombos();
	}

	private void SetFloatAttributes( float t )
	{
		foreach ( var pair in Floats )
		{
			Renderer.Floats[pair.Key] = new ParticleFloat { ConstantValue = pair.Value.Evaluate( t, 6211 ) };
		}
	}

	private void SetColorAttributes( float t )
	{
		foreach ( var pair in Colors )
		{
			// Color implicitly converts to a constant ParticleGradient.
			Renderer.Colors[pair.Key] = pair.Value.Evaluate( t );
		}
	}

	private void SetFloat2Attributes()
	{
		foreach ( var pair in Floats2 )
			Renderer.Floats2[pair.Key] = pair.Value;
	}

	private void SetFloat4Attributes()
	{
		foreach ( var pair in Floats4 )
			Renderer.Floats4[pair.Key] = pair.Value;
	}

	private void SetTextureAttributes()
	{
		foreach ( var pair in Textures )
			Renderer.Textures[pair.Key] = pair.Value;
	}

	private void SetDynamicCombos()
	{
		foreach ( var pair in DynamicCombos )
			Renderer.DynamicCombos[pair.Key] = pair.Value;
	}
}