Rendering/TwinAbilityEyeTint.cs

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.

Native Interop
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 );
	}
}