A rendering component that tints a rectangular eye region on a player sprite while an ability is recharging. It computes pixel-aligned UVs and quad geometry from a SpriteRenderer, sets a tint color and custom shader/material, and draws a small quad into an overlay scene object each frame.
using System.Collections.Generic;
using Sandbox;
namespace BlockParty;
/// <summary>Tints the bright pixels in a configured eye patch while an ability is recharging.</summary>
public sealed class TwinAbilityEyeTint : Component
{
const float DEPTH_OFFSET = SpriteLayer.LAYER_Z_STEP;
readonly RenderAttributes _attributes = new();
readonly List<Vertex> _vertices = new( 6 );
SceneCustomObject _sceneObject;
Material _material;
Player _player;
bool _adjustTwinOneWalk;
bool _tintDarkPixels;
int _eyeLeft = 5;
int _eyeTop = 3;
int _eyeWidth = 3;
int _eyeHeight = 3;
string _animation = "idle";
Color _tint = Color.Transparent;
public Color Tint
{
get => _tint;
set => _tint = value;
}
public void Setup( Player player, bool adjustTwinOneWalk, int eyeLeft = 5, int eyeTop = 3,
int eyeWidth = 3, int eyeHeight = 3, bool tintDarkPixels = false )
{
_player = player;
_adjustTwinOneWalk = adjustTwinOneWalk;
_tintDarkPixels = tintDarkPixels;
_eyeLeft = eyeLeft;
_eyeTop = eyeTop;
_eyeWidth = eyeWidth;
_eyeHeight = eyeHeight;
}
public void SetAnimation( string animation )
{
_animation = animation;
}
protected override void OnEnabled()
{
_material = Material.FromShader( "shaders/twin_ability_eye_tint.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();
SpriteRenderer body = _player?.BodySpriteRenderer;
if ( _tint.a <= 0f || body is null || !body.Enabled || !body.GameObject.Active ) return;
Texture texture = body.Texture;
if ( texture is null || texture == Texture.Transparent ) return;
texture.MarkUsed( ushort.MaxValue );
_attributes.Set( "MainTexture", texture );
_attributes.Set( "TintDarkPixels", _tintDarkPixels ? 1f : 0f );
Vector3 center = body.GameObject.WorldPosition;
Vector3 scale = body.GameObject.WorldScale;
float aspect = texture.Width / (float)Math.Max( 1, texture.Height );
float width = MathF.Abs( body.Size.x * scale.x * MathF.Min( aspect, 1f ) );
float height = MathF.Abs( body.Size.y * scale.y / MathF.Max( aspect, 1f ) );
float pixelWidth = width / Math.Max( 1, texture.Width );
float pixelHeight = height / Math.Max( 1, texture.Height );
float fullLeft = center.x - width * 0.5f;
float fullRight = center.x + width * 0.5f;
float fullBottom = center.y - height * 0.5f;
float fullTop = center.y + height * 0.5f;
int eyeTop = _eyeTop - (IsRaisedTwinOneWalkFrame( body ) ? 1 : 0);
int eyeRight = _eyeLeft + _eyeWidth;
int eyeBottom = eyeTop + _eyeHeight;
float left;
float right;
float leftU;
float rightU;
if ( body.FlipHorizontal )
{
left = fullRight - eyeRight * pixelWidth;
right = fullRight - _eyeLeft * pixelWidth;
leftU = eyeRight / (float)texture.Width;
rightU = _eyeLeft / (float)texture.Width;
}
else
{
left = fullLeft + _eyeLeft * pixelWidth;
right = fullLeft + eyeRight * pixelWidth;
leftU = _eyeLeft / (float)texture.Width;
rightU = eyeRight / (float)texture.Width;
}
float bottom;
float top;
float bottomV;
float topV;
if ( body.FlipVertical )
{
bottom = fullBottom + eyeTop * pixelHeight;
top = fullBottom + eyeBottom * pixelHeight;
bottomV = eyeTop / (float)texture.Height;
topV = eyeBottom / (float)texture.Height;
}
else
{
bottom = fullTop - eyeBottom * pixelHeight;
top = fullTop - eyeTop * pixelHeight;
bottomV = eyeBottom / (float)texture.Height;
topV = eyeTop / (float)texture.Height;
}
float z = center.z + DEPTH_OFFSET;
Vertex V( float x, float y, float u, float v )
=> new( new Vector3( x, y, z ), new Vector4( u, v, 0f, 0f ), _tint );
_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 ) );
}
bool IsRaisedTwinOneWalkFrame( SpriteRenderer body )
{
if ( !_adjustTwinOneWalk || _animation != "walk" ) return false;
return body.CurrentFrameIndex is 0 or 1 or 3 or 4;
}
void OnRender( SceneObject sceneObject )
{
SpriteRenderer body = _player?.BodySpriteRenderer;
if ( _vertices.Count == 0 || _material is null || body is null || !body.Enabled ) return;
Graphics.Draw( _vertices, _vertices.Count, _material, _attributes, Graphics.PrimitiveType.Triangles );
}
}