mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-05 04:48:19 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
536 lines
15 KiB
C#
536 lines
15 KiB
C#
namespace Sandbox.Rendering;
|
|
|
|
using System.Buffers;
|
|
using System.Runtime.InteropServices;
|
|
|
|
/// <summary>
|
|
/// This object renders every sprite registered to it in a single draw call. It takes care of sorting, sampling, and the whole pipeline regarding sprites.
|
|
/// The SceneSpriteSystem is responsible for pushing sprites into this object depending on its properties.
|
|
/// </summary>
|
|
internal sealed class SpriteBatchSceneObject : SceneCustomObject
|
|
{
|
|
internal readonly record struct SpriteGroup( SpriteData[] SharedSprites, int Offset, int Count );
|
|
public bool Sorted { get; set; } = false;
|
|
public bool Filtered { get; set; } = false;
|
|
public bool Additive { get; set; } = false;
|
|
|
|
internal Dictionary<Guid, SpriteRenderer> Components = new();
|
|
|
|
private readonly ComputeShader SpriteComputeShader = new( "sprite/sprite_cs" );
|
|
private readonly ComputeShader SortComputeShader = new( "sort_cs" );
|
|
private readonly GpuBuffer<uint> SpriteAtomicCounter;
|
|
|
|
private readonly Material SpriteMaterial;
|
|
|
|
internal Dictionary<Guid, SpriteGroup> SpriteGroups = [];
|
|
|
|
internal readonly SamplerState sampler = new()
|
|
{
|
|
AddressModeU = TextureAddressMode.Clamp,
|
|
AddressModeV = TextureAddressMode.Clamp,
|
|
};
|
|
|
|
// GPU Resident representation of a sprite
|
|
[StructLayout( LayoutKind.Sequential, Pack = 16 )]
|
|
public struct SpriteData
|
|
{
|
|
public Vector3 Position;
|
|
public Vector3 Rotation;
|
|
public Vector2 Scale;
|
|
public uint TintColor; // Packed RGBA8
|
|
public uint OverlayColor; // Packed RGBA8
|
|
public int TextureHandle;
|
|
public int RenderFlags;
|
|
public uint BillboardMode;
|
|
public float FogStrength;
|
|
public uint Lighting;
|
|
public float DepthFeather;
|
|
public int SamplerIndex;
|
|
public int Splots = 0;
|
|
public int Sequence = 0;
|
|
public float SequenceTime = 0;
|
|
public float RotationOffset = -1.0f;
|
|
public Vector4 MotionBlur;
|
|
public Vector3 Velocity = Vector3.Zero;
|
|
public Vector4 BlendSheetUV;
|
|
public Vector2 Offset;
|
|
public SpriteData()
|
|
{
|
|
|
|
}
|
|
|
|
// Helper method to pack Color to RGBA8
|
|
internal static uint PackColor( Color color )
|
|
{
|
|
byte r = (byte)(color.r * 255f);
|
|
byte g = (byte)(color.g * 255f);
|
|
byte b = (byte)(color.b * 255f);
|
|
byte a = (byte)(color.a * 255f);
|
|
return (uint)(r | (g << 8) | (b << 16) | (a << 24));
|
|
}
|
|
}
|
|
struct SpriteVertex
|
|
{
|
|
public Vector4 position;
|
|
public Vector4 normal;
|
|
public Vector2 uv;
|
|
|
|
public SpriteVertex( Vector3 pos, Vector3 norm, Vector2 inUv )
|
|
{
|
|
position = new Vector4( pos, 1.0f );
|
|
normal = new Vector4( norm, 0.0f );
|
|
uv = inUv;
|
|
}
|
|
}
|
|
|
|
const int DefaultBufferSize = 16;
|
|
int CurrentBufferSize = DefaultBufferSize;
|
|
|
|
int _splotCount = 0;
|
|
int SplotCount
|
|
{
|
|
get
|
|
{
|
|
if ( _splotCount != 0 )
|
|
{
|
|
return _splotCount;
|
|
}
|
|
|
|
// Use precomputed splot counts to avoid iteration
|
|
int splotCount = 0;
|
|
foreach ( var group in SpriteGroups )
|
|
{
|
|
if ( _precomputedSplotCounts.TryGetValue( group.Key, out int precomputedCount ) )
|
|
{
|
|
splotCount += precomputedCount;
|
|
}
|
|
else
|
|
{
|
|
// Fallback
|
|
var spriteGroup = group.Value;
|
|
|
|
for ( int i = 0; i < spriteGroup.Count; i++ )
|
|
{
|
|
int index = spriteGroup.Offset + i;
|
|
int leading = spriteGroup.SharedSprites[index].MotionBlur.x > 1 ? 2 : 1;
|
|
splotCount += spriteGroup.SharedSprites[index].Splots * leading;
|
|
}
|
|
}
|
|
}
|
|
|
|
_splotCount = splotCount;
|
|
|
|
return _splotCount;
|
|
}
|
|
}
|
|
|
|
int SpriteCount
|
|
{
|
|
get
|
|
{
|
|
int sum = Components.Count;
|
|
foreach ( var group in SpriteGroups )
|
|
{
|
|
sum += group.Value.Count;
|
|
}
|
|
|
|
return sum;
|
|
}
|
|
}
|
|
|
|
bool GPUUploadQueued = false;
|
|
|
|
GpuBuffer<SpriteData> SpriteBuffer;
|
|
GpuBuffer<SpriteData> SpriteBufferOut;
|
|
|
|
GpuBuffer<SpriteVertex> VertexBuffer;
|
|
GpuBuffer<int> IndexBuffer;
|
|
GpuBuffer<uint> GPUSortingBuffer;
|
|
GpuBuffer<float> GPUDistanceBuffer;
|
|
|
|
SpriteData[] SpriteDataBuffer = null!;
|
|
bool SpriteDataBufferRented = false;
|
|
|
|
public SpriteBatchSceneObject( Scene scene ) : base( scene.SceneWorld )
|
|
{
|
|
SpriteMaterial = Material.FromShader( "sprite/sprite_ps.shader" );
|
|
|
|
InitializeSpriteMesh();
|
|
|
|
// GPU buffers
|
|
SpriteBuffer = new( CurrentBufferSize );
|
|
SpriteBufferOut = new( CurrentBufferSize );
|
|
GPUSortingBuffer = new( CurrentBufferSize );
|
|
GPUDistanceBuffer = new( CurrentBufferSize );
|
|
SpriteAtomicCounter = new( 1 );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create the initialize sprite mesh that will be instanced
|
|
/// </summary>
|
|
private void InitializeSpriteMesh()
|
|
{
|
|
// Vertex pulling buffer
|
|
const float spriteSize = 10f;
|
|
SpriteVertex[] vertices =
|
|
[
|
|
new ( new ( -spriteSize, -spriteSize, 0 ), Vector3.Forward, new ( 0, 0 ) ),
|
|
new ( new ( spriteSize, -spriteSize, 0 ), Vector3.Forward, new ( 1, 0 ) ),
|
|
new ( new ( spriteSize, spriteSize, 0 ), Vector3.Forward, new ( 1, 1 ) ),
|
|
new ( new ( -spriteSize, spriteSize, 0 ), Vector3.Forward, new ( 0, 1 ) ),
|
|
];
|
|
VertexBuffer = new( 4 );
|
|
VertexBuffer.SetData( vertices.AsSpan() );
|
|
|
|
// Index buffer
|
|
int[] indices = { 0, 1, 2, 0, 2, 3 };
|
|
IndexBuffer = new( 6, GpuBuffer.UsageFlags.Index );
|
|
IndexBuffer.SetData( indices );
|
|
}
|
|
|
|
~SpriteBatchSceneObject()
|
|
{
|
|
if ( SpriteDataBufferRented )
|
|
{
|
|
ArrayPool<SpriteData>.Shared.Return( SpriteDataBuffer, clearArray: false );
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Resizes GPU buffers to the nearest power of 2
|
|
/// </summary>
|
|
public void ResizeBuffers()
|
|
{
|
|
int allocationSize = SplotCount + SpriteCount;
|
|
if ( allocationSize <= CurrentBufferSize )
|
|
{
|
|
return;
|
|
}
|
|
|
|
ResizeBuffers( allocationSize );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resizes GPU buffers to accommodate the specified allocation size
|
|
/// </summary>
|
|
private void ResizeBuffers( int allocationSize )
|
|
{
|
|
CurrentBufferSize = (int)System.Numerics.BitOperations.RoundUpToPowerOf2( (uint)allocationSize );
|
|
|
|
SpriteBuffer?.Dispose();
|
|
GPUSortingBuffer?.Dispose();
|
|
GPUDistanceBuffer?.Dispose();
|
|
|
|
SpriteBuffer = new( CurrentBufferSize );
|
|
SpriteBufferOut = new( CurrentBufferSize );
|
|
GPUSortingBuffer = new( CurrentBufferSize );
|
|
GPUDistanceBuffer = new( CurrentBufferSize );
|
|
}
|
|
|
|
private readonly Dictionary<Guid, int> _precomputedSplotCounts = [];
|
|
|
|
// Pre-allocated buffer to avoid GC allocations in hot path
|
|
private SpriteRenderer[] _componentBuffer = new SpriteRenderer[16];
|
|
|
|
public void RegisterSprite( Guid ownerId, SpriteData[] sharedSprites, int offset, int count, int splotCount )
|
|
{
|
|
SpriteGroups[ownerId] = new( sharedSprites, offset, count );
|
|
_precomputedSplotCounts[ownerId] = splotCount;
|
|
OnChanged();
|
|
}
|
|
|
|
public void RegisterSprite( Guid id, SpriteRenderer component )
|
|
{
|
|
Components[id] = component;
|
|
OnChanged();
|
|
}
|
|
|
|
public void UnregisterSprite( Guid id )
|
|
{
|
|
Components.Remove( id );
|
|
OnChanged();
|
|
}
|
|
|
|
public void UnregisterSpriteGroup( Guid ownerId )
|
|
{
|
|
if ( SpriteGroups.Remove( ownerId ) )
|
|
{
|
|
// Clean up precomputed splot count
|
|
_precomputedSplotCounts.Remove( ownerId );
|
|
|
|
OnChanged();
|
|
}
|
|
}
|
|
|
|
public void UpdateSprite( Guid id, SpriteRenderer component )
|
|
{
|
|
if ( Components.ContainsKey( id ) )
|
|
{
|
|
Components[id] = component;
|
|
OnChanged();
|
|
}
|
|
}
|
|
|
|
public bool ContainsSprite( Guid id )
|
|
{
|
|
return Components.ContainsKey( id );
|
|
}
|
|
|
|
public void OnChanged()
|
|
{
|
|
int requiredSize = SplotCount + SpriteCount;
|
|
|
|
// Clear cached splot count to force recalculation
|
|
_splotCount = 0;
|
|
|
|
// Only resize if we actually need more space
|
|
if ( requiredSize > CurrentBufferSize )
|
|
{
|
|
ResizeBuffers( requiredSize );
|
|
}
|
|
|
|
GPUUploadQueued = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copy host buffers onto GPU
|
|
/// </summary>
|
|
public void UploadOnHost()
|
|
{
|
|
if ( !GPUUploadQueued && !Sorted )
|
|
{
|
|
return;
|
|
}
|
|
|
|
int spriteCount = SpriteCount;
|
|
|
|
if ( SpriteDataBuffer == null || SpriteDataBuffer.Length < spriteCount )
|
|
{
|
|
if ( SpriteDataBufferRented )
|
|
{
|
|
ArrayPool<SpriteData>.Shared.Return( SpriteDataBuffer, clearArray: false );
|
|
}
|
|
|
|
SpriteDataBuffer = ArrayPool<SpriteData>.Shared.Rent( spriteCount );
|
|
SpriteDataBufferRented = true;
|
|
}
|
|
|
|
// Upload sprites
|
|
int componentCount = Components.Count;
|
|
if ( componentCount > 0 )
|
|
{
|
|
// Use pre-allocated buffer to avoid GC allocation
|
|
if ( _componentBuffer.Length < componentCount )
|
|
{
|
|
_componentBuffer = new SpriteRenderer[componentCount * 2];
|
|
}
|
|
|
|
int index = 0;
|
|
foreach ( var component in Components.Values )
|
|
{
|
|
_componentBuffer[index++] = component;
|
|
}
|
|
|
|
Parallel.For( 0, componentCount, i =>
|
|
{
|
|
var c = _componentBuffer[i];
|
|
var transform = c.WorldTransform;
|
|
var spriteSize = c.Size;
|
|
var rotation = c.WorldRotation.Angles().AsVector3();
|
|
|
|
if ( c.Billboard == SpriteRenderer.BillboardMode.Always || c.Billboard == SpriteRenderer.BillboardMode.YOnly )
|
|
{
|
|
// We only care about roll in this case
|
|
rotation.x = 0;
|
|
rotation.y = 0;
|
|
}
|
|
|
|
spriteSize = spriteSize.Abs();
|
|
|
|
// Adjust for aspect ratio
|
|
var aspectRatio = (c.Texture?.Width ?? 1) / (float)(c.Texture?.Height ?? 1);
|
|
var size = spriteSize / 2f;
|
|
var pos = transform.Position;
|
|
var scale = new Vector3( transform.Scale.x * size.x, transform.Scale.y, transform.Scale.z * size.y );
|
|
if ( aspectRatio < 1f )
|
|
scale *= new Vector3( aspectRatio, 1f, 1f );
|
|
else
|
|
scale *= new Vector3( 1f, 1f, 1f / aspectRatio );
|
|
|
|
pos = pos.RotateAround( transform.Position, transform.Rotation );
|
|
transform = transform.WithScale( scale ).WithPosition( pos );
|
|
|
|
var flipFlags = SpriteRenderer.FlipFlags.None;
|
|
if ( c.FlipHorizontal ) flipFlags |= SpriteRenderer.FlipFlags.FlipX;
|
|
if ( c.FlipVertical ) flipFlags |= SpriteRenderer.FlipFlags.FlipY;
|
|
|
|
var rgbe = c.Color.ToRgbe();
|
|
var alpha = (byte)(c.Color.a.Clamp( 0.0f, 1.0f ) * 255.0f);
|
|
var tintColor = new Color32( rgbe.r, rgbe.g, rgbe.b, alpha );
|
|
|
|
var overlayRgbe = c.OverlayColor.ToRgbe();
|
|
var overlayAlpha = (byte)(c.OverlayColor.a.Clamp( 0.0f, 1.0f ) * 255.0f);
|
|
var overlayColor = new Color32( overlayRgbe.r, overlayRgbe.g, overlayRgbe.b, overlayAlpha );
|
|
|
|
int lightingFlag = c.Lighting ? 1 : 0;
|
|
uint packedExponent = (uint)(((byte)lightingFlag) | rgbe.a << 16);
|
|
SpriteDataBuffer[i] = new SpriteData
|
|
{
|
|
Position = transform.Position,
|
|
Rotation = new( rotation.x, rotation.y, rotation.z ),
|
|
Scale = new( transform.Scale.x, transform.Scale.z ),
|
|
TextureHandle = c.Texture is null ? Texture.Invalid.Index : c.Texture.Index,
|
|
TintColor = tintColor.RawInt,
|
|
OverlayColor = overlayColor.RawInt,
|
|
RenderFlags = (int)flipFlags,
|
|
BillboardMode = (uint)c.Billboard,
|
|
FogStrength = c.FogStrength,
|
|
Lighting = packedExponent,
|
|
DepthFeather = c.DepthFeather,
|
|
SamplerIndex = SamplerState.GetBindlessIndex( sampler with { Filter = c.TextureFilter } ),
|
|
Offset = c.Pivot
|
|
};
|
|
} );
|
|
}
|
|
|
|
// Upload components to GPU first
|
|
if ( Components.Count > 0 )
|
|
{
|
|
SpriteBuffer.SetData( SpriteDataBuffer );
|
|
}
|
|
|
|
// Upload each particle group directly to GPU with offset
|
|
int currentOffset = Components.Count;
|
|
foreach ( var spriteGroup in SpriteGroups.Values )
|
|
{
|
|
unsafe
|
|
{
|
|
var sourceSpan = spriteGroup.SharedSprites.AsSpan( spriteGroup.Offset, spriteGroup.Count );
|
|
|
|
// Upload directly to GPU at the correct offset
|
|
SpriteBuffer.SetData( sourceSpan, currentOffset );
|
|
currentOffset += spriteGroup.Count;
|
|
}
|
|
}
|
|
|
|
GPUUploadQueued = false;
|
|
}
|
|
|
|
private const int GroupSize = 256;
|
|
private const int MaxDimGroups = 1024;
|
|
private const int MaxDimThreads = GroupSize * MaxDimGroups;
|
|
|
|
private void PreSort()
|
|
{
|
|
if ( SpriteCount < 2 ) return;
|
|
|
|
// First we clear the buffers to prepare for sorting
|
|
SortComputeShader.Attributes.SetCombo( "D_CLEAR", 1 );
|
|
SortComputeShader.Attributes.Set( "SortBuffer", GPUSortingBuffer );
|
|
SortComputeShader.Attributes.Set( "DistanceBuffer", GPUDistanceBuffer );
|
|
SortComputeShader.Attributes.Set( "Count", CurrentBufferSize );
|
|
SortComputeShader.Dispatch( CurrentBufferSize, 1, 1 );
|
|
|
|
Graphics.ResourceBarrierTransition( GPUSortingBuffer, ResourceState.UnorderedAccess, ResourceState.UnorderedAccess );
|
|
Graphics.ResourceBarrierTransition( GPUDistanceBuffer, ResourceState.UnorderedAccess, ResourceState.UnorderedAccess );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs a GPU bitonic sort
|
|
/// </summary>
|
|
private void Sort()
|
|
{
|
|
// Distance buffer is already filled by GPU compute shader, no need to update from CPU
|
|
Graphics.ResourceBarrierTransition( GPUDistanceBuffer, Sandbox.Rendering.ResourceState.Common );
|
|
|
|
// Sort
|
|
SortComputeShader.Attributes.SetCombo( "D_CLEAR", 0 );
|
|
|
|
var x = Math.Min( CurrentBufferSize, MaxDimThreads );
|
|
var y = (CurrentBufferSize + MaxDimThreads - 1) / MaxDimThreads;
|
|
var z = 1;
|
|
|
|
for ( var dim = 2; dim <= CurrentBufferSize; dim <<= 1 )
|
|
{
|
|
SortComputeShader.Attributes.Set( "Dim", dim );
|
|
|
|
for ( var block = dim >> 1; block > 0; block >>= 1 )
|
|
{
|
|
SortComputeShader.Attributes.Set( "Block", block );
|
|
SortComputeShader.Dispatch( x, y, z );
|
|
|
|
// Make sure sort buffer is ready to use
|
|
Graphics.ResourceBarrierTransition( GPUSortingBuffer, ResourceState.UnorderedAccess, ResourceState.UnorderedAccess );
|
|
Graphics.ResourceBarrierTransition( GPUDistanceBuffer, ResourceState.UnorderedAccess, ResourceState.UnorderedAccess );
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rendering logic of the sprites
|
|
/// </summary>
|
|
public override void RenderSceneObject()
|
|
{
|
|
base.RenderSceneObject();
|
|
|
|
if ( SpriteCount == 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( Sorted )
|
|
{
|
|
PreSort();
|
|
}
|
|
|
|
// Generate trails and UVs (this is mainly for particles)
|
|
SpriteAtomicCounter.SetData( [0] ); // Reset atomic counter
|
|
Graphics.ResourceBarrierTransition( SpriteAtomicCounter, ResourceState.Common );
|
|
Graphics.ResourceBarrierTransition( SpriteBuffer, ResourceState.Common );
|
|
Graphics.ResourceBarrierTransition( SpriteBufferOut, ResourceState.Common );
|
|
Graphics.ResourceBarrierTransition( GPUDistanceBuffer, ResourceState.Common );
|
|
|
|
var attributes = RenderAttributes.Pool.Get();
|
|
|
|
attributes.Set( "Sprites", SpriteBuffer );
|
|
attributes.Set( "SpriteBufferOut", SpriteBufferOut );
|
|
|
|
attributes.Set( "SpriteCount", SpriteCount );
|
|
attributes.Set( "AtomicCounter", SpriteAtomicCounter );
|
|
|
|
// Sorting
|
|
attributes.Set( "DistanceBuffer", GPUDistanceBuffer );
|
|
attributes.Set( "CameraPosition", Graphics.CameraPosition );
|
|
|
|
SpriteComputeShader.DispatchWithAttributes( attributes, SpriteCount, 1, 1 );
|
|
|
|
RenderAttributes.Pool.Return( attributes );
|
|
|
|
// Barried for the new sprites generated
|
|
Graphics.ResourceBarrierTransition( SpriteAtomicCounter, ResourceState.Common );
|
|
Graphics.ResourceBarrierTransition( SpriteBufferOut, ResourceState.Common );
|
|
|
|
Graphics.Attributes.SetCombo( "D_BLEND", Additive ? 1 : 0 );
|
|
|
|
// Sort
|
|
if ( Sorted )
|
|
{
|
|
Sort();
|
|
}
|
|
|
|
// Draw the sprites
|
|
Graphics.Attributes.Set( "IsSorted", Sorted ? 1 : 0 );
|
|
Graphics.Attributes.Set( "SpriteCount", SpriteCount + SplotCount );
|
|
|
|
Graphics.Attributes.Set( "Filtered", Filtered );
|
|
Graphics.Attributes.Set( "Sprites", SpriteBufferOut );
|
|
Graphics.Attributes.Set( "SortLUT", GPUSortingBuffer ); // Always bind even if not used
|
|
|
|
// Vertex Pulling
|
|
Graphics.Attributes.Set( "Vertices", VertexBuffer );
|
|
Graphics.Attributes.Set( "g_bNonDirectionalDiffuseLighting", true );
|
|
Graphics.DrawIndexedInstanced( IndexBuffer, SpriteMaterial, SpriteCount + SplotCount );
|
|
}
|
|
}
|