Code/LineShaderAttributes.cs

Component that drives shader/render attributes on a LineRenderer. It stores dictionaries of float, float2, float4, color and texture parameters, evaluates time-based ParticleFloat values (with looping and randomized sample per instance), and applies them each update or on start; it can also read shader attributes from a material shader file.

File Access
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sandbox;
[Title("LineShaderAttributes")]
[Category("Shaders")]
[Description("Let's you set the render attributes of the scene object of a LineRenderer")]
public sealed class LineShaderAttributes : Component, Component.ExecuteInEditor, IResetable
{
	[Property] private LineRenderer LineRenderer { get; set; }

	/// <summary>
	/// The LineRenderer this component drives. Exposed so an owning
	/// <see cref="ChildrenLineAttributes"/> can drive shared properties (e.g. width).
	/// </summary>
	public LineRenderer Line => LineRenderer;
	[Property, Group( "Floats1" )] public Dictionary<String, ParticleFloat> Floats { get; set; }
	[Property, Group("Floats2")] public Dictionary<String, Vector2> Floats2 { get; set; }
	[Property, Group("Colors")] public Dictionary<String, Color> Colors {get; set;}
	[Property, Group("Textures")] public Dictionary<String, Texture> Textures { get; set; }

	[Property, Group("Floats4")] public Dictionary<String, Vector4> Floats4 { get; set; }

	[Property, Group("DynamicCombos")] public Dictionary<String, int> DynamicCombos { get; set; }

	[Property] public ParticleFloat Duration { get; set; } = new ParticleFloat { ConstantValue = 1.0f };
	[Property] public bool Loop { get; set; } = false;

	// When false, the effective duration is driven externally (e.g. by a
	// ChildrenLineAttributes parent) instead of being evaluated from Duration.
	[Property] public bool AutoEvaluateDuration { get; set; } = true;

	public float Delta { get; set; }

	// Effective duration, evaluated once from the ParticleFloat on start.
	float IResetable.Duration { get => _effectiveDuration; set => _effectiveDuration = value; }

	/// <summary>
	/// Overrides the current effective duration. Used when the timing is driven
	/// externally; pair with <see cref="AutoEvaluateDuration"/> set to false.
	/// </summary>
	public void SetEffectiveDuration( float duration ) => _effectiveDuration = duration;

	private TimeSince _elapsed;
	private float _effectiveDuration = 1.0f;

	// Per-instance random draw (0..1) used to sample ranged ParticleFloats.
	// Evaluated once so each spawned instance gets a stable value within its
	// range instead of the same constant every frame.
	private float _randomSample = Random.Shared.Float( 0f, 1f );


	protected override void OnStart()
	{
		Floats = Floats == null ? new Dictionary<string, ParticleFloat>() : Floats;
		Floats2 = Floats2 == null ? new Dictionary<string, Vector2>() : Floats2;
		Floats4 = Floats4 == null ? new Dictionary<string, Vector4>() : Floats4;
		Textures = Textures == null ? new Dictionary<string, Texture>() : Textures;
		Colors = Colors == null ? new Dictionary<String, Color>() : Colors;
		DynamicCombos = DynamicCombos == null ? new Dictionary<string, int>() : DynamicCombos;
		if ( AutoEvaluateDuration )
			_effectiveDuration = EvaluateDuration();
		_elapsed = 0;
		_randomSample = Random.Shared.Float( 0f, 1f );
		ApplyAttributes();

	}

	public void ResetTime()
	{
		_elapsed = 0;
	}

	[Button]
	private void ReadAttributes()
	{
		if ( LineRenderer.Texturing.Material == null || !FileSystem.Mounted.FileExists( LineRenderer.Texturing.Material.Shader.ResourcePath )) return;
		var parsedFloats = new Dictionary<string, float>();
		AttributesParser<float,Color> parser = new AttributesParser<float,Color>(new NativeAttributeTypeSet());
		parser.Floats = parsedFloats;
		parser.Floats2 = Floats2;
		parser.Floats4 = Floats4;
		parser.Textures = Textures;
		parser.DynamicCombos = DynamicCombos;
		parser.Colors = Colors;
		parser.ParseAttributes( LineRenderer.Texturing.Material.Shader.ResourcePath );

		foreach (var pair in parsedFloats)
		{
			if (Floats.ContainsKey(pair.Key))
				continue;

			Floats.Add(pair.Key, new ParticleFloat { ConstantValue = pair.Value });
		}

	}
	protected override void OnUpdate()
	{
		_elapsed += Time.Delta;
		ApplyAttributes();
	}

	private void ApplyAttributes()
	{
		if ( LineRenderer == null )
			return;

		SetColorAttributes();
		SetFloatAttributes();
		SetFloat2Attributes();
		SetTexturesAttributes();
		SetFloat4Attributes();
		SetDynamicCombos();
	}

	private void SetDynamicCombos()
	{
		foreach ( var dynamicCombo in DynamicCombos )
		{
			LineRenderer.Attributes.SetCombo( dynamicCombo.Key, dynamicCombo.Value );
		}
	}

	private void SetFloat4Attributes()
	{
		foreach ( var float4Attribute in Floats4 )
		{
			LineRenderer.Attributes.Set( float4Attribute.Key, float4Attribute.Value );
		}
	}

	private void SetTexturesAttributes()
	{
		foreach ( var textureAttribute in Textures )
		{
			LineRenderer.Attributes.Set( textureAttribute.Key, textureAttribute.Value );
		}
	}


	private void SetColorAttributes()
	{
		foreach ( var colorAttribute in Colors )
		{
			LineRenderer.Attributes.Set( colorAttribute.Key, colorAttribute.Value );
		}
	}

	private float EvaluateDuration()
	{
		return Duration.Evaluate( 0f, Random.Shared.Float( 0f, 1f ) );
	}

	private void SetFloatAttributes()
	{
		float t = 0f;
		if ( _effectiveDuration > 0f )
		{
			bool looping = Loop ;
			// Re-evaluate the duration each time a loop cycle completes so a
			// randomized ParticleFloat produces a fresh duration per loop.
			// Skipped when the duration is driven externally.
			if ( looping && AutoEvaluateDuration )
			{
				while ( _elapsed >= _effectiveDuration )
				{
					_elapsed -= _effectiveDuration;
					_effectiveDuration = EvaluateDuration();
					_randomSample = Random.Shared.Float( 0f, 1f );
					if ( _effectiveDuration <= 0f )
						break;
				}
			}

			if ( _effectiveDuration > 0f )
			{
				var raw = (float)_elapsed / _effectiveDuration;
				t = looping ? raw % 1f : raw.Clamp( 0f, 1f );
			}
		}
		foreach ( var float1 in Floats )
		{
			LineRenderer.Attributes.Set( float1.Key, float1.Value.Evaluate( t, _randomSample ) );
		}
	}

	private void SetFloat2Attributes()
	{
		foreach ( var float2 in Floats2 )
		{
			LineRenderer.Attributes.Set( float2.Key, float2.Value );
		}
	}
}