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.
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 );
}
}