using Sandbox.Rendering; using System.Numerics; namespace Sandbox; /// /// A scene object which is used to draw lines /// public class SceneLineObject : SceneCustomObject { public enum CapStyle { None = 0, [Icon( "signal_cellular_4_bar" )] Triangle = 1, [Icon( "label_important" )] Arrow = 2, [Icon( "fiber_smart_record" )] Rounded = 3 } public enum FaceMode { [Icon( "settings_ethernet" )] Camera = 0, [Icon( "rotate_90_degrees_cw" )] Normal = 1, [Icon( "circle" )] Cylinder = 2, } [Obsolete( "Use Material property instead", false )] public Texture LineTexture { get => Attributes.GetTexture( "BaseTexture" ); set => Attributes.Set( "BaseTexture", value ?? Texture.White ); } public Material Material = Material.FromShader( "line" ); public CapStyle StartCap { get => (CapStyle)Attributes.GetInt( "StartCap" ); set => Attributes.Set( "StartCap", (int)value ); } public CapStyle EndCap { get => (CapStyle)Attributes.GetInt( "EndCap" ); set => Attributes.Set( "EndCap", (int)value ); } public FaceMode Face { get => (FaceMode)_cs.Attributes.GetInt( "FaceMode" ); set => _cs.Attributes.Set( "FaceMode", (int)value ); } public bool Wireframe { get => Attributes.GetComboBool( "D_WIREFRAME" ); set => Attributes.SetCombo( "D_WIREFRAME", value ); } public bool Lighting { get => Attributes.GetComboBool( "D_ENABLE_LIGHTING" ); set => Attributes.SetCombo( "D_ENABLE_LIGHTING", value ); } public bool Clamped { get => _clamped; set { _clamped = value; if ( _clamped ) { Attributes.Set( "SamplerIndex", SamplerState.GetBindlessIndex( ClampSampler ) ); } else { Attributes.Set( "SamplerIndex", SamplerState.GetBindlessIndex( WrapSampler ) ); } } } private bool _clamped = false; public int Smoothness { get => Attributes.GetInt( "Smoothness" ); set => Attributes.Set( "Smoothness", value ); } public bool Opaque { set { Attributes.SetCombo( "D_OPAQUE", value ? 1 : 0 ); Flags.IsOpaque = value; Flags.IsTranslucent = !value; } } private int _roundedCapSegments = 8; private int _tessellationLevel = 1; // Number of subdivisions between left and right sides of each segment /// /// Number of tessellation subdivisions across the width of each line segment. /// 1 = no tessellation (just left and right), 2 = one subdivision in the middle, etc. /// Higher values create smoother curves and more detailed geometry but use more vertices. /// public int TessellationLevel { get => _tessellationLevel; set => _tessellationLevel = Math.Max( 1, value ); } private struct LinePoint( uint offset, in Vector3 position, in Vector3 normal, Color color, float width, float textureCoord ) { public uint Offset = offset; public Vector3 Position = position; public Vector3 Normal = normal; public Color Color = color; public float Width = width; public float TextureCoord = textureCoord; } private struct LineVertex() { [VertexLayout.Position] public Vector3 Position = default; [VertexLayout.Normal] public Vector3 Normal = default; [VertexLayout.Tangent] public Vector3 Tangent = default; [VertexLayout.Color] public Color Color = default; [VertexLayout.TexCoord] public Vector2 TextureCoord = default; } private readonly ComputeShader _cs = new( "line_cs" ); private GpuBuffer _pointBuffer; private GpuBuffer _vertexBuffer; private GpuBuffer _indexBuffer; private static readonly SamplerState WrapSampler = new() { Filter = FilterMode.Anisotropic, MaxAnisotropy = 8, AddressModeU = TextureAddressMode.Wrap, AddressModeV = TextureAddressMode.Wrap }; private static readonly SamplerState ClampSampler = new() { Filter = FilterMode.Anisotropic, MaxAnisotropy = 8, AddressModeU = TextureAddressMode.Clamp, AddressModeV = TextureAddressMode.Clamp }; private readonly List _points = []; private BBox _bounds; private int _pointCount; private int _pointCapacity; private int _indexCapacity; private int _vertexCapacity; public SceneLineObject( SceneWorld sceneWorld ) : base( sceneWorld ) { #pragma warning disable CS0618 LineTexture = Texture.White; #pragma warning restore CS0618 managedNative.ExecuteOnMainThread = false; } public void StartLine() { _points.Clear(); _pointCount = 0; _bounds = BBox.FromPositionAndSize( Transform.Position, 10 ); } public void AddLinePoint( in Vector3 pos, Color color, float width ) { AddLinePoint( pos, color, width, _pointCount / 20.0f ); } public void AddLinePoint( in Vector3 pos, Color color, float width, float textureCoord ) { AddLinePoint( pos, Vector3.Up, color, width, textureCoord ); } public void AddLinePoint( in Vector3 pos, in Vector3 normal, Color color, float width, float textureCoord ) { if ( _pointCount > 0 && _points[^1].Position.DistanceSquared( pos ) < 0.01f ) return; var point = new LinePoint( 0, pos, normal, color, width, textureCoord ); _points.Add( point ); _pointCount++; _bounds = _bounds.AddPoint( pos ); } /// /// Calculate the number of vertices needed for a specific cap type /// private int GetCapVertexCount( CapStyle capStyle ) { return capStyle switch { CapStyle.None => 0, CapStyle.Triangle => 1, // 1 additional vertex (center) CapStyle.Arrow => 3, // Additional triangle CapStyle.Rounded => _roundedCapSegments + 1 + 1, // Multiple vertices for the rounded cap _ => 0 }; } /// /// Calculate the number of indices needed for a specific cap type /// private int GetCapIndexCount( CapStyle capStyle ) { return capStyle switch { CapStyle.None => 0, CapStyle.Triangle => 3, // One triangle (3 indices) CapStyle.Arrow => 3, // One triangle (3 indices) CapStyle.Rounded => _roundedCapSegments * 3, // Multiple triangles (3 indices each) _ => 0 }; } public void EndLine() { Bounds = _bounds; if ( _pointCount == 0 ) return; var startCap = StartCap; var endCap = EndCap; if ( _points.Count > _pointCapacity ) { MainThread.QueueDispose( _pointBuffer ); _pointCapacity = (int)(_points.Count * 1.5); _pointBuffer = new GpuBuffer( _pointCapacity, GpuBuffer.UsageFlags.Structured ); } if ( CalculateIndexCount() > _indexCapacity ) { MainThread.QueueDispose( _indexBuffer ); _indexCapacity = (int)(CalculateIndexCount() * 1.5); _indexBuffer = new GpuBuffer( _indexCapacity, GpuBuffer.UsageFlags.Index | GpuBuffer.UsageFlags.Structured ); } if ( CalculateVertexCount() > _vertexCapacity ) { MainThread.QueueDispose( _vertexBuffer ); _vertexCapacity = (int)(CalculateVertexCount() * 1.5); _vertexBuffer = new GpuBuffer( _vertexCapacity, GpuBuffer.UsageFlags.Vertex | GpuBuffer.UsageFlags.Structured ); } if ( _pointBuffer.IsValid() ) { _pointBuffer.SetData( _points ); } if ( Face == FaceMode.Cylinder ) { // Unsupported on Cylinder _cs.Attributes.Set( "StartCap", 0 ); _cs.Attributes.Set( "EndCap", 0 ); } else { _cs.Attributes.Set( "StartCap", (int)startCap ); _cs.Attributes.Set( "EndCap", (int)endCap ); } _cs.Attributes.Set( "TessellationLevel", _tessellationLevel ); _cs.Attributes.Set( "RoundedCapSegments", _roundedCapSegments ); } public void Clear() { _points.Clear(); _pointCount = 0; _pointCapacity = 0; _vertexCapacity = 0; _indexCapacity = 0; MainThread.QueueDispose( _pointBuffer ); MainThread.QueueDispose( _vertexBuffer ); MainThread.QueueDispose( _indexBuffer ); _pointBuffer = default; _vertexBuffer = default; _indexBuffer = default; } private int CalculateIndexCount() { // With tessellation, each segment creates tessellationLevel * 2 triangles // (tessellationLevel quads, each quad = 2 triangles, each triangle = 3 indices) int indicesPerSegment = _tessellationLevel * 2 * 3; int lineIndices = (_pointCount - 1) * indicesPerSegment; // Add indices for caps int capIndices = GetCapIndexCount( StartCap ) + GetCapIndexCount( EndCap ); return lineIndices + capIndices; } private int CalculateVertexCount() { // Basic line vertices (tessellationLevel + 1 vertices per point to create tessellationLevel segments across width) int lineVertices = _pointCount * (_tessellationLevel + 1); // Add vertices for caps int capVertices = GetCapVertexCount( StartCap ) + GetCapVertexCount( EndCap ); return lineVertices + capVertices; } public override void RenderSceneObject() { base.RenderSceneObject(); // If we have less than a quad fuck off if ( CalculateIndexCount() < 6 ) return; if ( !_pointBuffer.IsValid() ) return; if ( !_vertexBuffer.IsValid() ) return; if ( !_indexBuffer.IsValid() ) return; _cs.Attributes.Set( "PointBuffer", _pointBuffer ); _cs.Attributes.Set( "VertexBuffer", _vertexBuffer ); _cs.Attributes.Set( "IndexBuffer", _indexBuffer ); _cs.Attributes.Set( "PointCount", _pointCount ); _cs.Dispatch( _pointCount, 1, 1 ); Graphics.ResourceBarrierTransition( _vertexBuffer, ResourceState.UnorderedAccess, ResourceState.VertexOrIndexBuffer ); Graphics.ResourceBarrierTransition( _indexBuffer, ResourceState.UnorderedAccess, ResourceState.VertexOrIndexBuffer ); if ( Lighting ) { Graphics.SetupLighting( this, Attributes ); } Graphics.Draw( _vertexBuffer, _indexBuffer, Material, 0, CalculateIndexCount(), Attributes ); } }