Rendering/PlayfieldPatternRenderer.cs

A component that renders the playfield pattern as a single procedural quad in the world. It creates a SceneCustomObject, configures a material/shader and per-instance render attributes, computes a 2-triangle quad covering a rect, and encodes an 8x8 custom pattern into shader attributes.

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

namespace BlockParty;

/// <summary>Draws the arena's repeating two-colour pattern as one procedural world quad.</summary>
public sealed class PlayfieldPatternRenderer : Component
{
	private SceneCustomObject _sceneObject;
	private Material _material;
	private readonly RenderAttributes _attributes = new();
	private readonly List<Vertex> _vertices = new( 6 );

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

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

	public void Setup( PlayfieldPattern pattern, int cellScale, IReadOnlyList<string> customRows, Color light, Color dark,
		RectF? bounds = null, int depthOrder = 0 )
	{
		if ( _sceneObject is null ) return;

		_attributes.Set( "Pattern", (int)pattern );
		_attributes.Set( "CellScale", (float)Math.Clamp( cellScale, 1, 8 ) );
		_attributes.Set( "LightColor", light );
		_attributes.Set( "DarkColor", dark );

		int width = customRows is { Count: > 0 } ? Math.Clamp( customRows[0]?.Length ?? 0, 1, 8 ) : 2;
		int height = customRows is { Count: > 0 } ? Math.Clamp( customRows.Count, 1, 8 ) : 2;
		_attributes.Set( "PatternWidth", width );
		_attributes.Set( "PatternHeight", height );
		for ( int row = 0; row < 8; row++ )
		{
			int bits = 0;
			string source = customRows is not null && row < customRows.Count ? customRows[row] : null;
			for ( int column = 0; column < width && column < (source?.Length ?? 0); column++ )
				if ( source[column] == '1' ) bits |= 1 << column;
			_attributes.Set( $"PatternRow{row}", bits );
		}

		RectF rect = bounds ?? new RectF( 0f, 0f, Arena.WIDTH, Arena.HEIGHT );
		float z = Globals.DepthToZ( Globals.DEPTH_PLAYFIELD, depthOrder );
		var color = Color.White;
		Vertex V( float x, float y ) => new( new Vector3( x, y, z ), color );
		_vertices.Clear();
		_vertices.Add( V( rect.Left, rect.Bottom ) );
		_vertices.Add( V( rect.Right, rect.Bottom ) );
		_vertices.Add( V( rect.Right, rect.Top ) );
		_vertices.Add( V( rect.Left, rect.Bottom ) );
		_vertices.Add( V( rect.Right, rect.Top ) );
		_vertices.Add( V( rect.Left, rect.Top ) );
	}

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