Rendering/SpriteFillOverlay.cs

A component that draws a bottom-up alpha-masked fill over a live sprite frame. It tracks a SpriteRenderer source, computes a clipped quad based on Fill and sprite orientation, builds vertex geometry and issues a custom render call each update.

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

namespace BlockParty;

/// <summary>Draws a bottom-up, alpha-masked fill over a live sprite frame.</summary>
public sealed class SpriteFillOverlay : Component
{
	const float DEPTH_OFFSET = SpriteLayer.LAYER_Z_STEP * 0.5f;

	readonly RenderAttributes _attributes = new();
	readonly List<Vertex> _vertices = new( 6 );
	SceneCustomObject _sceneObject;
	Material _material;
	SpriteRenderer _source;
	float _fill;

	public Color Tint { get; set; } = Color.White;

	public float Fill
	{
		get => _fill;
		set => _fill = Math.Clamp( value, 0f, 1f );
	}

	public void Setup( SpriteRenderer source, float fill, Color tint )
	{
		_source = source;
		Fill = fill;
		Tint = tint;
	}

	protected override void OnEnabled()
	{
		_material = Material.FromShader( "shaders/player_ammo_overlay.shader" );
		_sceneObject = new SceneCustomObject( Scene.SceneWorld )
		{
			Transform = global::Transform.Zero,
			RenderLayer = SceneRenderLayer.OverlayWithDepth,
			RenderOverride = OnRender,
		};
		_sceneObject.Flags.IsOpaque = false;
		_sceneObject.Flags.IsTranslucent = true;
		_sceneObject.Flags.CastShadows = false;
	}

	protected override void OnDisabled()
	{
		_sceneObject?.Delete();
		_sceneObject = null;
	}

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

	void RebuildGeometry()
	{
		_vertices.Clear();
		if ( _fill <= 0f || _source is null || !_source.Enabled || !_source.GameObject.Active ) return;

		Texture texture = _source.Texture;
		if ( texture is null || texture == Texture.Transparent ) return;
		texture.MarkUsed( ushort.MaxValue );
		_attributes.Set( "MainTexture", texture );

		float fill = MathF.Floor( _fill * texture.Height ) / Math.Max( 1, texture.Height );
		if ( fill <= 0f ) return;

		Vector3 center = _source.GameObject.WorldPosition;
		Vector3 scale = _source.GameObject.WorldScale;
		float aspect = texture.Width / (float)Math.Max( 1, texture.Height );
		float width = MathF.Abs( _source.Size.x * scale.x * MathF.Min( aspect, 1f ) );
		float height = MathF.Abs( _source.Size.y * scale.y / MathF.Max( aspect, 1f ) );
		float left = center.x - width * 0.5f;
		float right = center.x + width * 0.5f;
		float fullBottom = center.y - height * 0.5f;
		float fullTop = center.y + height * 0.5f;
		float bottom = _source.FlipVertical ? fullTop - height * fill : fullBottom;
		float top = _source.FlipVertical ? fullTop : fullBottom + height * fill;
		float z = center.z + DEPTH_OFFSET;

		float leftU = _source.FlipHorizontal ? 1f : 0f;
		float rightU = _source.FlipHorizontal ? 0f : 1f;
		float bottomV = _source.FlipVertical ? 1f - fill : 1f;
		float topV = _source.FlipVertical ? 1f : 1f - fill;
		Color color = Tint;

		Vertex V( float x, float y, float u, float v )
			=> new( new Vector3( x, y, z ), new Vector4( u, v, 0f, 0f ), color );

		_vertices.Add( V( left, bottom, leftU, bottomV ) );
		_vertices.Add( V( right, bottom, rightU, bottomV ) );
		_vertices.Add( V( right, top, rightU, topV ) );
		_vertices.Add( V( left, bottom, leftU, bottomV ) );
		_vertices.Add( V( right, top, rightU, topV ) );
		_vertices.Add( V( left, top, leftU, topV ) );
	}

	void OnRender( SceneObject sceneObject )
	{
		if ( _vertices.Count == 0 || _material is null || _source is null || !_source.Enabled ) return;
		Graphics.Draw( _vertices, _vertices.Count, _material, _attributes, Graphics.PrimitiveType.Triangles );
	}
}