Component that drives shader attributes on a ShaderParticleModelRenderer from a ParticleEmitter over its lifetime. It samples configured float, color, vector and texture maps at a normalized emitter time and writes them into the renderer each frame.
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;
}
}