├── .gitignore ├── .editorconfig ├── Assets ├── textures │ └── facepunch_sdf.png ├── layers │ └── examples │ │ ├── background.sdflayer │ │ ├── scorch.sdflayer │ │ └── checkerboard.sdflayer ├── shaders │ ├── sdf2d │ │ ├── shared.hlsl │ │ └── scorched.shader │ └── sdf3d │ │ ├── shared.hlsl │ │ ├── triplanar.hlsl │ │ ├── triplanar.shader │ │ └── triplanar_scorch.shader └── materials │ └── sdf2d │ └── examples │ └── checkerboard.vmat ├── .gitattributes ├── README.md ├── ProjectSettings └── Collision.config ├── .sbproj ├── Code ├── Pooled.cs ├── 3D │ ├── Sdf3DVolume.cs │ ├── Sdf3DChunk.cs │ ├── Sdf3DWorld.cs │ ├── Sdf3DMeshWriter.Types.cs │ ├── Noise │ │ └── Cellular.cs │ ├── Sdf3DArray.cs │ └── Sdf3DMeshWriter.cs ├── Helpers.cs ├── facepunch.libpolygon │ ├── PolygonMeshBuilder.Edge.cs │ ├── PolygonModelRenderer.cs │ ├── PolygonMeshBuilder.Validate.cs │ ├── Helpers.cs │ ├── PolygonMeshBuilder.SVG.cs │ ├── PolygonMeshBuilder.Fill.cs │ ├── PolygonMeshBuilder.cs │ └── PolygonMeshBuilder.Bevel.cs ├── WorldQuality.cs ├── 2D │ ├── Sdf2DWorld.cs │ ├── Noise │ │ └── Cellular.cs │ ├── Sdf2DChunk.cs │ ├── Sdf2DLayer.cs │ ├── Sdf2DMeshWriter.Types.cs │ ├── Transform2D.cs │ ├── Sdf2DArray.cs │ ├── Sdf2DMeshWriter.SourceEdges.cs │ └── Sdf2DMeshWriter.cs ├── SdfWorld.Network.cs ├── SdfResource.cs ├── SdfArray.cs └── SdfChunk.cs ├── LICENSE └── scripts └── codegen.py /.gitignore: -------------------------------------------------------------------------------- 1 | obj/ 2 | launchSettings.json 3 | *.csproj 4 | *_c 5 | .idea 6 | *.sln 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # C# files 2 | [*.{cs,razor}] 3 | indent_style = tab 4 | indent_size = 4 5 | tab_size = 4 6 | -------------------------------------------------------------------------------- /Assets/textures/facepunch_sdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Facepunch/sbox-sdf/HEAD/Assets/textures/facepunch_sdf.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | \*.png filter=lfs diff=lfs merge=lfs -text 2 | \*.jpg filter=lfs diff=lfs merge=lfs -text 3 | \*.psd filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /Assets/layers/examples/background.sdflayer: -------------------------------------------------------------------------------- 1 | { 2 | "CollisionTags": "solid", 3 | "Depth": 16, 4 | "Offset": -24, 5 | "FrontFaceMaterial": "materials/dev/dev_measuregeneric01b.vmat", 6 | "BackFaceMaterial": null, 7 | "CutFaceMaterial": "materials/dev/dev_measuregeneric01b.vmat", 8 | "Quality": "Low" 9 | } -------------------------------------------------------------------------------- /Assets/layers/examples/scorch.sdflayer: -------------------------------------------------------------------------------- 1 | { 2 | "IsTextureSourceOnly": true, 3 | "CollisionTags": "solid", 4 | "Depth": 64, 5 | "Offset": 0, 6 | "TexCoordSize": 256, 7 | "FrontFaceMaterial": null, 8 | "BackFaceMaterial": null, 9 | "CutFaceMaterial": null, 10 | "LayerTextures": null, 11 | "QualityLevel": "Medium", 12 | "ChunkResolution": 16, 13 | "ChunkSize": 256, 14 | "MaxDistance": 64 15 | } -------------------------------------------------------------------------------- /Assets/layers/examples/checkerboard.sdflayer: -------------------------------------------------------------------------------- 1 | { 2 | "IsTextureSourceOnly": false, 3 | "CollisionTags": "solid", 4 | "Depth": 32, 5 | "Offset": 0, 6 | "TexCoordSize": 256, 7 | "FrontFaceMaterial": "materials/sdf2d/examples/checkerboard.vmat", 8 | "BackFaceMaterial": null, 9 | "CutFaceMaterial": "materials/sdf2d/examples/checkerboard.vmat", 10 | "LayerTextures": [ 11 | { 12 | "TargetAttribute": "ScorchLayer", 13 | "SourceLayer": "layers/examples/scorch.sdflayer" 14 | } 15 | ], 16 | "QualityLevel": "Medium", 17 | "ChunkResolution": 16, 18 | "ChunkSize": 256, 19 | "MaxDistance": 64 20 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sbox-sdf 2 | Allows you to define a 2D / 3D field of signed distances to a virtual surface, perform operations with primitive shapes, then generate a 3D mesh of the surface in real time. 3 | 4 | ![20230602134828_1](https://github.com/Facepunch/sbox-sdf/assets/1110904/87cd8c4c-8ff3-4782-b6b0-7ebde4e52aef) 5 | 6 | ## Packages 7 | * [`facepunch.libsdf`](https://asset.party/facepunch/libsdf) - Main library package that you can reference in your own projects 8 | * [`facepunch.blobtool`](https://asset.party/facepunch/blobtool) - Tech demo tool addon for [`facepunch.sandbox`](https://asset.party/facepunch/sandbox) 9 | 10 | ## Tutorials 11 | * [Getting Started in 2D](https://github.com/Facepunch/sbox-sdf/wiki/Getting-Started-in-2D) 12 | -------------------------------------------------------------------------------- /Assets/shaders/sdf2d/shared.hlsl: -------------------------------------------------------------------------------- 1 | #ifndef SDF2D_SHARED_H 2 | #define SDF2D_SHARED_H 3 | 4 | #define STRING( A ) #A 5 | 6 | #define CreateSdfLayerTexture( attribName ) \ 7 | CreateTexture2D( g_t##attribName ) < \ 8 | Attribute( #attribName ); \ 9 | SrgbRead( false ); \ 10 | Filter( BILINEAR ); \ 11 | AddressU( CLAMP ); \ 12 | AddressV( CLAMP ); \ 13 | >; \ 14 | float4 g_fl##attribName##_Params < \ 15 | Default4( 0.0, 0.0, 1.0, 1.0 ); \ 16 | Attribute( STRING( attribName##_Params ) ); \ 17 | > 18 | 19 | #define SdfLayerTex( attribName, positionOs ) \ 20 | ((Tex2D( g_t##attribName, positionOs.xy * g_fl##attribName##_Params.z + g_fl##attribName##_Params.xx ) - 0.5) * g_fl##attribName##_Params.w) 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /Assets/shaders/sdf3d/shared.hlsl: -------------------------------------------------------------------------------- 1 | #ifndef SDF3D_SHARED_H 2 | #define SDF3D_SHARED_H 3 | 4 | #define STRING( A ) #A 5 | 6 | #define CreateSdfLayerTexture( attribName ) \ 7 | CreateTexture3D( g_t##attribName ) < \ 8 | Attribute( #attribName ); \ 9 | SrgbRead( false ); \ 10 | Filter( BILINEAR ); \ 11 | AddressU( CLAMP ); \ 12 | AddressV( CLAMP ); \ 13 | AddressW( CLAMP ); \ 14 | >; \ 15 | float4 g_fl##attribName##_Params < \ 16 | Default4( 0.0, 0.0, 1.0, 1.0 ); \ 17 | Attribute( STRING( attribName##_Params ) ); \ 18 | > 19 | 20 | #define SdfLayerTex( attribName, positionOs ) \ 21 | ((Tex3D( g_t##attribName, positionOs.xyz * g_fl##attribName##_Params.z + g_fl##attribName##_Params.xxx ) - 0.5) * g_fl##attribName##_Params.w) 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /ProjectSettings/Collision.config: -------------------------------------------------------------------------------- 1 | { 2 | "Defaults": { 3 | "solid": "Collide", 4 | "trigger": "Trigger", 5 | "ladder": "Ignore", 6 | "water": "Trigger" 7 | }, 8 | "Pairs": [ 9 | { 10 | "a": "solid", 11 | "b": "solid", 12 | "r": "Collide" 13 | }, 14 | { 15 | "a": "trigger", 16 | "b": "playerclip", 17 | "r": "Ignore" 18 | }, 19 | { 20 | "a": "trigger", 21 | "b": "solid", 22 | "r": "Trigger" 23 | }, 24 | { 25 | "a": "solid", 26 | "b": "trigger", 27 | "r": "Collide" 28 | }, 29 | { 30 | "a": "playerclip", 31 | "b": "solid", 32 | "r": "Collide" 33 | } 34 | ], 35 | "__guid": "cb87b295-60b0-46dc-98ca-1ed35ffff3cb", 36 | "__schema": "configdata", 37 | "__type": "CollisionRules", 38 | "__version": 1 39 | } -------------------------------------------------------------------------------- /.sbproj: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "SDF Mesh Library", 3 | "Type": "library", 4 | "Org": "facepunch", 5 | "Ident": "libsdf", 6 | "Schema": 1, 7 | "IncludeSourceFiles": false, 8 | "Resources": "/shaders/*.hlsl\n", 9 | "PackageReferences": [], 10 | "EditorReferences": null, 11 | "Mounts": null, 12 | "IsStandaloneOnly": false, 13 | "Metadata": { 14 | "ProjectTemplate": null, 15 | "CsProjName": "", 16 | "Summary": "Marching cubes & squares mesh generation", 17 | "Description": "* Sdf2DWorld for 2D layers\n* Sdf3DWorld for 3D volumes", 18 | "Public": true, 19 | "Compiler": { 20 | "RootNamespace": "Sandbox", 21 | "DefineConstants": "SANDBOX;ADDON;DEBUG", 22 | "NoWarn": "1701;1702;1591;", 23 | "References": [], 24 | "DistinctReferences": [] 25 | }, 26 | "ReplaceTags": "procgen mesh 3d 2d" 27 | } 28 | } -------------------------------------------------------------------------------- /Assets/materials/sdf2d/examples/checkerboard.vmat: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED 2 | 3 | Layer0 4 | { 5 | shader "shaders/sdf2d/scorched.shader_c" 6 | 7 | //---- Fog ---- 8 | g_bFogEnabled "1" 9 | 10 | //---- Material ---- 11 | g_flTintColor "[1.000000 1.000000 1.000000 0.000000]" 12 | TextureAmbientOcclusion "materials/default/default_ao.tga" 13 | TextureColor "materials/dev/simple/simple_tile_color.png" 14 | TextureMetalness "materials/default/default_metal.tga" 15 | TextureNormal "materials/default/default_normal.tga" 16 | TextureRoughness "materials/dev/simple/simple_tile_rough.png" 17 | TextureTintMask "materials/default/default.tga" 18 | TextureTranslucency "materials/default/default_trans.tga" 19 | 20 | //---- Scorch ---- 21 | ScorchAmbientOcclusion "materials/nature/leaves_ao.png" 22 | ScorchBlendMask "materials/nature/leaves_height.png" 23 | ScorchColor "materials/nature/leaves_color.png" 24 | ScorchMetalness "materials/default/default_metal.tga" 25 | ScorchNormal "materials/nature/leaves_normal.png" 26 | ScorchRoughness "materials/nature/leaves_rough.png" 27 | } -------------------------------------------------------------------------------- /Code/Pooled.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Sandbox; 5 | 6 | public abstract class Pooled : IDisposable 7 | where T : Pooled, new() 8 | { 9 | #pragma warning disable SB3000 10 | private const int MaxPoolCount = 64; 11 | private static List Pool { get; } = new(); 12 | #pragma warning restore SB3000 13 | 14 | public static T Rent() 15 | { 16 | lock ( Pool ) 17 | { 18 | if ( Pool.Count <= 0 ) return new T(); 19 | 20 | var writer = Pool[^1]; 21 | Pool.RemoveAt( Pool.Count - 1 ); 22 | 23 | writer._isInPool = false; 24 | writer.Reset(); 25 | 26 | return writer; 27 | } 28 | } 29 | 30 | public void Return() 31 | { 32 | lock ( Pool ) 33 | { 34 | if ( _isInPool ) throw new InvalidOperationException( "Already returned." ); 35 | 36 | Reset(); 37 | 38 | _isInPool = true; 39 | 40 | if ( Pool.Count < MaxPoolCount ) Pool.Add( (T)this ); 41 | } 42 | } 43 | 44 | private bool _isInPool; 45 | 46 | public abstract void Reset(); 47 | 48 | public void Dispose() 49 | { 50 | Return(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Code/3D/Sdf3DVolume.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Sandbox.Sdf; 5 | 6 | /// 7 | /// Controls the appearance and physical properties of a volume in a . 8 | /// 9 | [GameResource( "SDF 3D Volume", "sdfvol", $"Properties of a volume in a Sdf3DWorld", Icon = "view_in_ar" )] 10 | public class Sdf3DVolume : SdfResource 11 | { 12 | /// 13 | /// Material used to render this volume. 14 | /// 15 | [HideIf( nameof( IsTextureSourceOnly ), true )] 16 | public Material Material { get; set; } 17 | 18 | internal override WorldQuality GetQualityFromPreset( WorldQualityPreset preset ) 19 | { 20 | switch ( preset ) 21 | { 22 | case WorldQualityPreset.Low: 23 | return new( 8, 512f, 96f ); 24 | 25 | case WorldQualityPreset.Medium: 26 | return new( 16, 512f, 48f ); 27 | 28 | case WorldQualityPreset.High: 29 | return new( 32, 512f, 24f ); 30 | 31 | case WorldQualityPreset.Extreme: 32 | return new( 16, 256f, 24f ); 33 | 34 | default: 35 | throw new NotImplementedException(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Facepunch Studios 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Code/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Sandbox.Sdf; 4 | 5 | internal static class Helpers 6 | { 7 | public static Vector2 NormalizeSafe( in Vector2 vec ) 8 | { 9 | var length = vec.Length; 10 | 11 | if ( length > 9.9999997473787516E-06 ) 12 | { 13 | return vec / length; 14 | } 15 | else 16 | { 17 | return 0f; 18 | } 19 | } 20 | 21 | public static Vector2 Rotate90( Vector2 v ) 22 | { 23 | return new Vector2( v.y, -v.x ); 24 | } 25 | 26 | public static Vector3 RotateNormal( Vector3 oldNormal, float sin, float cos ) 27 | { 28 | var normal2d = new Vector2( oldNormal.x, oldNormal.y ); 29 | 30 | if ( normal2d.LengthSquared <= 0.000001f ) 31 | { 32 | return oldNormal; 33 | } 34 | 35 | normal2d = NormalizeSafe( normal2d ); 36 | 37 | return new Vector3( normal2d.x * cos, normal2d.y * cos, sin ).Normal; 38 | } 39 | 40 | public static float GetEpsilon( Vector2 vec, float frac = 0.0001f ) 41 | { 42 | return Math.Max( Math.Abs( vec.x ), Math.Abs( vec.y ) ) * frac; 43 | } 44 | 45 | public static float GetEpsilon( Vector2 a, Vector2 b, float frac = 0.0001f ) 46 | { 47 | return Math.Max( GetEpsilon( a, frac ), GetEpsilon( b, frac ) ); 48 | } 49 | 50 | public static int NextPowerOf2( int value ) 51 | { 52 | var po2 = 1; 53 | while ( po2 < value ) po2 <<= 1; 54 | 55 | return po2; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Code/facepunch.libpolygon/PolygonMeshBuilder.Edge.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Sandbox.Polygons; 3 | 4 | partial class PolygonMeshBuilder 5 | { 6 | private struct Edge 7 | { 8 | public int Index { get; } 9 | 10 | public Vector2 Origin { get; } 11 | public Vector2 Tangent { get; } 12 | public Vector2 Normal { get; } 13 | 14 | public Vector2 Velocity { get; set; } 15 | 16 | public int PrevEdge { get; set; } 17 | public int NextEdge { get; set; } 18 | 19 | public float Distance { get; set; } 20 | public float MaxDistance { get; set; } 21 | 22 | public (int Prev, int Next) Vertices { get; set; } 23 | 24 | public int Twin { get; } 25 | 26 | public Edge( int index, Vector2 origin, Vector2 tangent, float distance, int twin = -1 ) 27 | { 28 | Index = index; 29 | 30 | Origin = origin; 31 | Tangent = tangent; 32 | Normal = Helpers.Rotate90( tangent ); 33 | 34 | Velocity = Vector2.Zero; 35 | 36 | PrevEdge = -1; 37 | NextEdge = -1; 38 | 39 | Vertices = (-1, -1); 40 | 41 | Distance = distance; 42 | MaxDistance = float.PositiveInfinity; 43 | 44 | Twin = twin; 45 | } 46 | 47 | public readonly Vector2 Project( float distance ) 48 | { 49 | return Origin + Velocity * (distance - Distance); 50 | } 51 | 52 | public override string ToString() 53 | { 54 | return $"[{Index}]"; 55 | } 56 | 57 | public bool Equals( Edge other ) 58 | { 59 | return Index == other.Index; 60 | } 61 | 62 | public override bool Equals( object obj ) 63 | { 64 | return obj is Edge other && Equals( other ); 65 | } 66 | 67 | public override int GetHashCode() 68 | { 69 | return Index; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Code/WorldQuality.cs: -------------------------------------------------------------------------------- 1 | namespace Sandbox.Sdf; 2 | 3 | /// 4 | /// Preset quality settings for . 5 | /// 6 | public enum WorldQualityPreset 7 | { 8 | /// 9 | /// Cheap and cheerful, suitable for frequent (per-frame) edits. 10 | /// 11 | Low, 12 | 13 | /// 14 | /// Recommended quality for most cases. 15 | /// 16 | Medium, 17 | 18 | /// 19 | /// More expensive to update and network, but a much smoother result. 20 | /// 21 | High, 22 | 23 | /// 24 | /// Only use this for small, detailed objects! 25 | /// 26 | Extreme, 27 | 28 | /// 29 | /// Manually tweak quality parameters. 30 | /// 31 | Custom = -1 32 | } 33 | 34 | /// 35 | /// Quality settings for . 36 | /// 37 | public record struct WorldQuality( int ChunkResolution, float ChunkSize, float MaxDistance ) 38 | { 39 | /// 40 | /// Distance between samples in one axis. 41 | /// 42 | public float UnitSize => ChunkSize / ChunkResolution; 43 | 44 | /// 45 | /// Read an instance of from a er. 46 | /// 47 | public static WorldQuality Read( ref ByteStream net ) 48 | { 49 | return new WorldQuality( net.Read(), 50 | net.Read(), 51 | net.Read() ); 52 | } 53 | 54 | /// 55 | /// Write this instance to a er. Can be read with . 56 | /// 57 | public void Write( ref ByteStream net ) 58 | { 59 | net.Write( ChunkResolution ); 60 | net.Write( ChunkSize ); 61 | net.Write( MaxDistance ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Code/2D/Sdf2DWorld.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Sandbox.Sdf; 5 | 6 | /// 7 | /// Main entity for creating a set of 2D surfaces that can be added to and subtracted from. 8 | /// Each surface is aligned to the same plane, but can have different offsets, depths, and materials. 9 | /// 10 | [Title( "SDF 2D World" )] 11 | public partial class Sdf2DWorld : SdfWorld 12 | { 13 | /// 14 | public override int Dimensions => 2; 15 | 16 | private (int MinX, int MinY, int MaxX, int MaxY) GetChunkRange( Rect bounds, WorldQuality quality ) 17 | { 18 | var unitSize = quality.UnitSize; 19 | 20 | var min = (bounds.TopLeft - quality.MaxDistance - unitSize) / quality.ChunkSize; 21 | var max = (bounds.BottomRight + quality.MaxDistance + unitSize) / quality.ChunkSize; 22 | 23 | var minX = (int)MathF.Floor( min.x ); 24 | var minY = (int)MathF.Floor( min.y ); 25 | 26 | var maxX = (int)MathF.Ceiling( max.x ); 27 | var maxY = (int)MathF.Ceiling( max.y ); 28 | 29 | return (minX, minY, maxX, maxY); 30 | } 31 | 32 | /// 33 | protected override IEnumerable<(int X, int Y)> GetAffectedChunks( T sdf, WorldQuality quality ) 34 | { 35 | var (minX, minY, maxX, maxY) = GetChunkRange( sdf.Bounds, quality ); 36 | 37 | for ( var y = minY; y < maxY; ++y ) 38 | for ( var x = minX; x < maxX; ++x ) 39 | { 40 | yield return (x, y); 41 | } 42 | } 43 | 44 | protected override bool AffectsChunk( T sdf, WorldQuality quality, (int X, int Y) chunkKey ) 45 | { 46 | var (minX, minY, maxX, maxY) = GetChunkRange( sdf.Bounds, quality ); 47 | 48 | return chunkKey.X >= minX && chunkKey.X < maxX 49 | && chunkKey.Y >= minY && chunkKey.Y < maxY; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Code/facepunch.libpolygon/PolygonModelRenderer.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace Sandbox.Polygons; 6 | 7 | public class PolygonModelRenderer : ModelRenderer 8 | { 9 | private Mesh _mesh; 10 | 11 | private string _svg; 12 | private bool _meshDirty; 13 | 14 | /// 15 | /// Scalable Vector Graphics source string for this model. 16 | /// 17 | [Property] 18 | public string Svg 19 | { 20 | get => _svg; 21 | set 22 | { 23 | _svg = value; 24 | _meshDirty = true; 25 | } 26 | } 27 | 28 | private int _lastHash = 0; 29 | 30 | protected override void OnEnabled() 31 | { 32 | base.OnEnabled(); 33 | 34 | UpdateModel(); 35 | } 36 | 37 | protected override void OnValidate() 38 | { 39 | base.OnValidate(); 40 | 41 | _meshDirty = true; 42 | } 43 | 44 | private void UpdateModel() 45 | { 46 | if ( !_meshDirty ) 47 | { 48 | return; 49 | } 50 | 51 | var hash = Svg?.FastHash() ?? 0; 52 | if ( _lastHash == hash ) 53 | { 54 | return; 55 | } 56 | 57 | if ( Model?.IsProcedural is not true ) 58 | { 59 | Model = null; 60 | } 61 | 62 | _lastHash = hash; 63 | 64 | if ( !string.IsNullOrEmpty( Svg ) ) 65 | { 66 | using var builder = PolygonMeshBuilder.Rent(); 67 | 68 | builder.MaxSmoothAngle = 33f.DegreeToRadian(); 69 | 70 | builder.AddSvg( _svg, new AddSvgOptions 71 | { 72 | ThrowIfNotSupported = true 73 | }, new Rect( -128f, -128f, 256f, 256f ) ); 74 | builder.Extrude( 8f ); 75 | builder.Arc( 2f, 2 ); 76 | builder.Fill(); 77 | builder.Mirror(); 78 | 79 | _mesh ??= new Mesh( Material.Load( "materials/default/white.vmat" ) ); 80 | _mesh.UpdateMesh( PolygonMeshBuilder.Vertex.Layout, builder.Vertices, builder.Indices ); 81 | 82 | Model ??= new ModelBuilder() 83 | .AddMesh( _mesh ) 84 | .Create(); 85 | } 86 | else 87 | { 88 | _mesh?.SetIndexRange( 0, 0 ); 89 | } 90 | } 91 | 92 | protected override void OnUpdate() 93 | { 94 | UpdateModel(); 95 | 96 | base.OnUpdate(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Code/2D/Noise/Cellular.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Sandbox.Sdf.Noise 6 | { 7 | public record struct CellularNoiseSdf2D( int Seed, Vector2 CellSize, float DistanceOffset, Vector2 InvCellSize ) : ISdf2D 8 | { 9 | public CellularNoiseSdf2D( int seed, Vector2 cellSize, float distanceOffset ) 10 | : this( seed, cellSize, distanceOffset, new Vector2( 1f / cellSize.x, 1f / cellSize.y ) ) 11 | { 12 | } 13 | 14 | public Rect Bounds => default; 15 | 16 | public float this[ Vector2 pos ] 17 | { 18 | get 19 | { 20 | var localPos = pos * InvCellSize; 21 | var cell = ( 22 | X: (int)MathF.Floor( localPos.x ), 23 | Y: (int)MathF.Floor( localPos.y )); 24 | 25 | var cellPos = new Vector2( cell.X, cell.Y ) * CellSize; 26 | var cellLocalPos = pos - cellPos; 27 | 28 | var minDistSq = float.PositiveInfinity; 29 | 30 | foreach ( var offset in PointOffsets ) 31 | { 32 | var feature = GetFeature( cell.X + offset.X, cell.Y + offset.Y ) + new Vector2( offset.X, offset.Y ) * CellSize; 33 | var distSq = (feature - cellLocalPos).LengthSquared; 34 | 35 | minDistSq = Math.Min( minDistSq, distSq ); 36 | } 37 | 38 | return MathF.Sqrt( minDistSq ) - DistanceOffset; 39 | } 40 | } 41 | 42 | Vector2 GetFeature( int x, int y ) 43 | { 44 | var hashX = HashCode.Combine( Seed, x, y ); 45 | var hashY = HashCode.Combine( y, Seed, x ); 46 | 47 | return new Vector2( (hashX & 0xffff) / 65536f, (hashY & 0xffff) / 65536f ) * CellSize; 48 | } 49 | 50 | private static (int X, int Y)[] PointOffsets { get; } = Enumerable.Range( -1, 3 ) 51 | .SelectMany( y => Enumerable.Range( -1, 3 ) 52 | .Select( x => (x, y) ) ).ToArray(); 53 | 54 | public void WriteRaw( ref ByteStream writer, Dictionary sdfTypes ) 55 | { 56 | writer.Write( Seed ); 57 | writer.Write( CellSize ); 58 | writer.Write( DistanceOffset ); 59 | } 60 | 61 | public static CellularNoiseSdf2D ReadRaw( ref ByteStream reader, IReadOnlyDictionary> sdfTypes ) 62 | { 63 | return new CellularNoiseSdf2D( reader.Read(), reader.Read(), reader.Read() ); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Assets/shaders/sdf3d/triplanar.hlsl: -------------------------------------------------------------------------------- 1 | #include "common/pixel.hlsl" 2 | 3 | float3 RnmBlendUnpacked(float3 n1, float3 n2) 4 | { 5 | n1 += float3( 0, 0, 1); 6 | n2 *= float3(-1, -1, 1); 7 | return n1*dot(n1, n2)/n1.z - n2; 8 | } 9 | 10 | Material ToMaterialTriplanar( in PixelInput i, in Texture2D tColor, in Texture2D tNormal, in Texture2D tRma ) 11 | { 12 | #ifdef TRIPLANAR_OBJECT_SPACE 13 | float3 worldPos = i.vPositionOs.xyz / 256.0; 14 | #else 15 | float3 worldPos = (i.vPositionWithOffsetWs.xyz + g_vCameraPositionWs.xyz) / 256.0; 16 | #endif 17 | 18 | float2 uvX = worldPos.zy; 19 | float2 uvY = worldPos.xz; 20 | float2 uvZ = worldPos.xy; 21 | 22 | float3 triblend = saturate(pow(abs(i.vNormalOs.xyz), 4)); 23 | triblend /= max(dot(triblend, half3(1,1,1)), 0.0001); 24 | 25 | half3 absVertNormal = abs(i.vNormalOs); 26 | half3 axisSign = i.vNormalOs < 0 ? -1 : 1; 27 | 28 | uvX.x *= axisSign.x; 29 | uvY.x *= axisSign.y; 30 | uvZ.x *= -axisSign.z; 31 | 32 | float4 colX = Tex2DS( tColor, TextureFiltering, uvX ); 33 | float4 colY = Tex2DS( tColor, TextureFiltering, uvY ); 34 | float4 colZ = Tex2DS( tColor, TextureFiltering, uvZ ); 35 | float4 col = colX * triblend.x + colY * triblend.y + colZ * triblend.z; 36 | 37 | float3 tnormalX = DecodeNormal(Tex2DS( tNormal, TextureFiltering, uvX ).xyz); 38 | float3 tnormalY = DecodeNormal(Tex2DS( tNormal, TextureFiltering, uvY ).xyz); 39 | float3 tnormalZ = DecodeNormal(Tex2DS( tNormal, TextureFiltering, uvZ ).xyz); 40 | 41 | tnormalX.x *= axisSign.x; 42 | tnormalY.x *= axisSign.y; 43 | tnormalZ.x *= -axisSign.z; 44 | 45 | tnormalX = half3(tnormalX.xy + i.vNormalWs.zy, i.vNormalWs.x); 46 | tnormalY = half3(tnormalY.xy + i.vNormalWs.xz, i.vNormalWs.y); 47 | tnormalZ = half3(tnormalZ.xy + i.vNormalWs.xy, i.vNormalWs.z); 48 | 49 | // Triblend normals and add to world normal 50 | float3 norm = normalize( 51 | tnormalX.zyx * triblend.x + 52 | tnormalY.xzy * triblend.y + 53 | tnormalZ.xyz * triblend.z + 54 | i.vNormalWs 55 | ); 56 | 57 | float4 rmaX = Tex2DS( tRma, TextureFiltering, uvX ); 58 | float4 rmaY = Tex2DS( tRma, TextureFiltering, uvY ); 59 | float4 rmaZ = Tex2DS( tRma, TextureFiltering, uvZ ); 60 | float4 rma = rmaX * triblend.x + rmaY * triblend.y + rmaZ * triblend.z; 61 | 62 | Material m = Material::From( i, col, float4( 0.5, 0.5, 1.0, 1.0 ), rma, g_flTintColor ); 63 | 64 | m.Normal = norm; 65 | 66 | return m; 67 | } 68 | -------------------------------------------------------------------------------- /Assets/shaders/sdf3d/triplanar.shader: -------------------------------------------------------------------------------- 1 | //========================================================================================================================= 2 | // Optional 3 | //========================================================================================================================= 4 | HEADER 5 | { 6 | CompileTargets = ( IS_SM_50 && ( PC || VULKAN ) ); 7 | Description = "Shader for geometry without texture coordinates"; 8 | } 9 | 10 | //========================================================================================================================= 11 | // Optional 12 | //========================================================================================================================= 13 | FEATURES 14 | { 15 | #include "common/features.hlsl" 16 | } 17 | 18 | //========================================================================================================================= 19 | COMMON 20 | { 21 | #include "common/shared.hlsl" 22 | } 23 | 24 | //========================================================================================================================= 25 | 26 | struct VertexInput 27 | { 28 | #include "common/vertexinput.hlsl" 29 | }; 30 | 31 | //========================================================================================================================= 32 | 33 | struct PixelInput 34 | { 35 | #include "common/pixelinput.hlsl" 36 | 37 | float3 vPositionOs : TEXCOORD15; 38 | float3 vNormalOs : TEXCOORD16; 39 | }; 40 | 41 | //========================================================================================================================= 42 | 43 | VS 44 | { 45 | #include "common/vertex.hlsl" 46 | 47 | // 48 | // Main 49 | // 50 | PixelInput MainVs( VertexInput i ) 51 | { 52 | PixelInput o = ProcessVertex( i ); 53 | 54 | o.vPositionOs = i.vPositionOs; 55 | o.vNormalOs = i.vNormalOs.xyz; 56 | 57 | return FinalizeVertex( o ); 58 | } 59 | } 60 | 61 | //========================================================================================================================= 62 | 63 | PS 64 | { 65 | #define TRIPLANAR_OBJECT_SPACE 66 | 67 | #include "sdf3d/triplanar.hlsl" 68 | 69 | // 70 | // Main 71 | // 72 | float4 MainPs( PixelInput i ) : SV_Target0 73 | { 74 | Material m = ToMaterialTriplanar( i, g_tColor, g_tNormal, g_tRma ); 75 | 76 | return ShadingModelStandard::Shade( i, m ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Code/SdfWorld.Network.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Sandbox.Sdf; 4 | 5 | public partial class SdfWorld : Component 6 | { 7 | private Dictionary ConnectionStates { get; } = new(); 8 | 9 | private record struct ConnectionState( int clearCount, int modificationCount, TimeSince lastMessage ); 10 | 11 | private const float HeartbeatPeriod = 2f; 12 | 13 | private void SendModifications( Connection conn ) 14 | { 15 | if ( !ConnectionStates.TryGetValue( conn, out var state ) ) 16 | state = new ConnectionState( 0, 0, 0f ); 17 | 18 | if ( state.clearCount != ClearCount ) 19 | state = state with { clearCount = ClearCount, modificationCount = 0 }; 20 | else if ( state.modificationCount >= ModificationCount && state.lastMessage < HeartbeatPeriod ) 21 | return; 22 | 23 | state = state with { lastMessage = 0f }; 24 | 25 | var byteStream = ByteStream.Create( 512 ); 26 | var count = Write( ref byteStream, state.modificationCount ); 27 | 28 | ConnectionStates[conn] = state with { modificationCount = state.modificationCount + count }; 29 | 30 | using ( Rpc.FilterInclude( conn ) ) 31 | { 32 | Rpc_SendModifications( byteStream.ToArray() ); 33 | } 34 | 35 | byteStream.Dispose(); 36 | } 37 | 38 | [Rpc.Broadcast] 39 | private void Rpc_RequestMissing( int clearCount, int modificationCount ) 40 | { 41 | var conn = Rpc.Caller; 42 | 43 | if ( !ConnectionStates.TryGetValue( conn, out var state ) ) 44 | { 45 | Log.Info( $"Can't find connection state for {conn.DisplayName}" ); 46 | return; 47 | } 48 | 49 | if (state.clearCount != clearCount || state.modificationCount <= modificationCount) 50 | return; 51 | 52 | ConnectionStates[conn] = state with { modificationCount = modificationCount }; 53 | } 54 | 55 | private TimeSince _notifiedMissingModifications = float.PositiveInfinity; 56 | 57 | [Rpc.Broadcast] 58 | private void Rpc_SendModifications( byte[] bytes ) 59 | { 60 | var byteStream = ByteStream.CreateReader( bytes ); 61 | if ( Read( ref byteStream ) ) 62 | { 63 | _notifiedMissingModifications = float.PositiveInfinity; 64 | return; 65 | } 66 | 67 | if ( _notifiedMissingModifications >= 0.5f ) 68 | { 69 | _notifiedMissingModifications = 0f; 70 | 71 | using ( Rpc.FilterInclude( Rpc.Caller ) ) 72 | { 73 | Rpc_RequestMissing( ClearCount, ModificationCount ); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Code/facepunch.libpolygon/PolygonMeshBuilder.Validate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Sandbox.Polygons; 5 | 6 | partial class PolygonMeshBuilder 7 | { 8 | [ThreadStatic] 9 | private static List Validate_EdgeList; 10 | 11 | private void Validate() 12 | { 13 | if ( _validated ) 14 | { 15 | return; 16 | } 17 | 18 | // Check active edge loops: 19 | // * Referenced edges must also be active 20 | // * Make sure references are correct in both directions 21 | // * Edges can't reference themselves 22 | 23 | foreach ( var edgeIndex in _activeEdges ) 24 | { 25 | ref var edge = ref _allEdges[edgeIndex]; 26 | 27 | if ( !_activeEdges.Contains( edge.NextEdge ) ) 28 | { 29 | throw InvalidPolygonException(); 30 | } 31 | 32 | if ( !_activeEdges.Contains( edge.PrevEdge ) ) 33 | { 34 | throw InvalidPolygonException(); 35 | } 36 | 37 | if ( edge.NextEdge == edge.Index ) 38 | { 39 | throw InvalidPolygonException(); 40 | } 41 | 42 | ref var next = ref _allEdges[edge.NextEdge]; 43 | 44 | if ( next.PrevEdge != edge.Index ) 45 | { 46 | throw InvalidPolygonException(); 47 | } 48 | } 49 | 50 | // Check for intersecting edges 51 | // TODO: Bentley–Ottmann? 52 | 53 | Validate_EdgeList ??= new List(); 54 | Validate_EdgeList.Clear(); 55 | Validate_EdgeList.AddRange( _activeEdges ); 56 | 57 | for ( var i = 0; i < Validate_EdgeList.Count; ++i ) 58 | { 59 | ref var edgeA0 = ref _allEdges[Validate_EdgeList[i]]; 60 | ref var edgeA1 = ref _allEdges[edgeA0.NextEdge]; 61 | 62 | var a0 = edgeA0.Origin; 63 | var a1 = edgeA1.Origin; 64 | 65 | var minA = Vector2.Min( a0 ,a1 ); 66 | var maxA = Vector2.Max( a0, a1 ); 67 | 68 | for ( var j = i + 1; j < Validate_EdgeList.Count; ++j ) 69 | { 70 | ref var edgeB0 = ref _allEdges[Validate_EdgeList[j]]; 71 | 72 | if ( edgeA0.NextEdge == edgeB0.Index || edgeA0.PrevEdge == edgeB0.Index ) 73 | { 74 | continue; 75 | } 76 | 77 | ref var edgeB1 = ref _allEdges[edgeB0.NextEdge]; 78 | 79 | var b0 = edgeA0.Origin; 80 | var b1 = edgeA1.Origin; 81 | 82 | var minB = Vector2.Min( b0, b1 ); 83 | var maxB = Vector2.Max( b0, b1 ); 84 | 85 | if ( minA.x >= maxB.x || minA.y >= maxB.y || minB.x >= maxA.x || minB.y >= maxA.y ) 86 | { 87 | continue; 88 | } 89 | 90 | if ( Helpers.LineSegmentsIntersect( a0, a1, b0, b1 ) ) 91 | { 92 | throw InvalidPolygonException(); 93 | } 94 | } 95 | } 96 | 97 | _validated = true; 98 | } 99 | 100 | private static Exception InvalidPolygonException() 101 | { 102 | return new Exception( "Invalid polygon" ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Code/3D/Sdf3DChunk.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Sandbox.Sdf; 7 | 8 | /// 9 | /// Represents chunks in a . 10 | /// Each chunk contains an SDF for a sub-region of one specific volume. 11 | /// 12 | [Hide] 13 | public partial class Sdf3DChunk : SdfChunk 14 | { 15 | public override Vector3 ChunkPosition 16 | { 17 | get 18 | { 19 | var quality = Resource.Quality; 20 | return new Vector3( Key.X * quality.ChunkSize, Key.Y * quality.ChunkSize, Key.Z * quality.ChunkSize ); 21 | } 22 | } 23 | 24 | private TranslatedSdf3D ToLocal( in T sdf ) 25 | where T : ISdf3D 26 | { 27 | return sdf.Translate( new Vector3( Key.X, Key.Y, Key.Z ) * -Resource.Quality.ChunkSize ); 28 | } 29 | 30 | /// 31 | protected override Task OnAddAsync( T sdf ) 32 | { 33 | return Data.AddAsync( ToLocal( sdf ) ); 34 | } 35 | 36 | /// 37 | protected override Task OnSubtractAsync( T sdf ) 38 | { 39 | return Data.SubtractAsync( ToLocal( sdf ) ); 40 | } 41 | 42 | protected override Task OnRebuildAsync( IEnumerable> modifications ) 43 | { 44 | return Data.RebuildAsync( modifications.Select( x => x with { Sdf = ToLocal( x.Sdf ) } )); 45 | } 46 | 47 | /// 48 | protected override async Task OnUpdateMeshAsync() 49 | { 50 | var enableRenderMesh = Resource.Material != null; 51 | var enableCollisionMesh = Resource.HasCollision; 52 | 53 | if ( !enableRenderMesh && !enableCollisionMesh ) 54 | { 55 | return; 56 | } 57 | 58 | using var writer = Sdf3DMeshWriter.Rent(); 59 | 60 | await Data.WriteToAsync( writer, Resource ); 61 | 62 | if ( !IsValid ) return; 63 | 64 | var renderTask = Task.CompletedTask; 65 | var collisionTask = Task.CompletedTask; 66 | 67 | if ( enableRenderMesh ) 68 | { 69 | renderTask = UpdateRenderMeshesAsync( new MeshDescription( writer, Resource.Material ) ); 70 | } 71 | 72 | if ( enableCollisionMesh ) 73 | { 74 | var scale = WorldScale.x; 75 | var offset = new Vector3( Key.X, Key.Y, Key.Z ) * Resource.Quality.ChunkSize; 76 | 77 | collisionTask = GameTask.RunInThreadAsync( async () => 78 | { 79 | // ReSharper disable AccessToDisposedClosure 80 | var vertices = writer.VertexPositions; 81 | 82 | for ( var i = 0; i < vertices.Count; ++i ) 83 | { 84 | vertices[i] += offset; 85 | vertices[i] *= scale; 86 | } 87 | 88 | await UpdateCollisionMeshAsync( writer.VertexPositions, writer.Indices ); 89 | // ReSharper restore AccessToDisposedClosure 90 | } ); 91 | } 92 | 93 | await GameTask.WhenAll( renderTask, collisionTask ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Code/3D/Sdf3DWorld.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Sandbox.Sdf; 5 | 6 | /// 7 | /// Main entity for creating a 3D surface that can be added to and subtracted from. 8 | /// Multiple volumes can be added to this entity with different materials. 9 | /// 10 | [Title( "SDF 3D World" )] 11 | public partial class Sdf3DWorld : SdfWorld 12 | { 13 | public override int Dimensions => 3; 14 | 15 | [Property] 16 | public bool IsFinite { get; set; } 17 | 18 | [Property, ShowIf( nameof(IsFinite), true )] 19 | public Vector3 Size { get; set; } = new Vector3( 1024, 1024, 1024 ); 20 | 21 | private ((int X, int Y, int Z) Min, (int X, int Y, int Z) Max) GetChunkRange( BBox bounds, WorldQuality quality ) 22 | { 23 | var unitSize = quality.UnitSize; 24 | 25 | var min = (bounds.Mins - quality.MaxDistance - unitSize) / quality.ChunkSize; 26 | var max = (bounds.Maxs + quality.MaxDistance + unitSize) / quality.ChunkSize; 27 | 28 | var minX = (int) MathF.Floor( min.x ); 29 | var minY = (int) MathF.Floor( min.y ); 30 | var minZ = (int) MathF.Floor( min.z ); 31 | 32 | var maxX = (int) MathF.Ceiling( max.x ); 33 | var maxY = (int) MathF.Ceiling( max.y ); 34 | var maxZ = (int) MathF.Ceiling( max.z ); 35 | 36 | if ( IsFinite ) 37 | { 38 | var chunksX = (int)MathF.Ceiling( Size.x / quality.ChunkSize ); 39 | var chunksY = (int)MathF.Ceiling( Size.y / quality.ChunkSize ); 40 | var chunksZ = (int)MathF.Ceiling( Size.z / quality.ChunkSize ); 41 | 42 | minX = Math.Max( 0, minX ); 43 | minY = Math.Max( 0, minY ); 44 | minZ = Math.Max( 0, minZ ); 45 | 46 | maxX = Math.Min( chunksX, maxX ); 47 | maxY = Math.Min( chunksY, maxY ); 48 | maxZ = Math.Min( chunksZ, maxZ ); 49 | } 50 | 51 | return ((minX, minY, minZ), (maxX, maxY, maxZ)); 52 | } 53 | 54 | private IEnumerable<(int X, int Y, int Z)> GetChunks( BBox bounds, WorldQuality quality ) 55 | { 56 | var ((minX, minY, minZ), (maxX, maxY, maxZ)) = GetChunkRange( bounds, quality ); 57 | 58 | for ( var z = minZ; z < maxZ; ++z ) 59 | for ( var y = minY; y < maxY; ++y ) 60 | for ( var x = minX; x < maxX; ++x ) 61 | { 62 | yield return (x, y, z); 63 | } 64 | } 65 | 66 | private BBox? DefaultBounds => IsFinite ? new BBox( 0f, Size ) : null; 67 | 68 | /// 69 | protected override IEnumerable<(int X, int Y, int Z)> GetAffectedChunks( T sdf, WorldQuality quality ) 70 | { 71 | if ( (sdf.Bounds ?? DefaultBounds) is not { } bounds ) 72 | { 73 | throw new Exception( "Can only make modifications with an SDF with Bounds != null" ); 74 | } 75 | 76 | return GetChunks( bounds, quality ); 77 | } 78 | 79 | protected override bool AffectsChunk( T sdf, WorldQuality quality, (int X, int Y, int Z) chunkKey ) 80 | { 81 | if ( (sdf.Bounds ?? DefaultBounds) is not { } bounds ) 82 | { 83 | throw new Exception( "Can only make modifications with an SDF with Bounds != null" ); 84 | } 85 | 86 | var ((minX, minY, minZ), (maxX, maxY, maxZ)) = GetChunkRange( bounds, quality ); 87 | return chunkKey.X >= minX && chunkKey.X < maxX 88 | && chunkKey.Y >= minY && chunkKey.Y < maxY 89 | && chunkKey.Z >= minZ && chunkKey.Z < maxZ; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Code/2D/Sdf2DChunk.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Sandbox.Sdf; 7 | 8 | /// 9 | /// Represents chunks in a . 10 | /// Each chunk contains an SDF for a sub-region of one specific layer. 11 | /// 12 | [Hide] 13 | public partial class Sdf2DChunk : SdfChunk 14 | { 15 | public override Vector3 ChunkPosition 16 | { 17 | get 18 | { 19 | var quality = Resource.Quality; 20 | return new Vector3( Key.X * quality.ChunkSize, Key.Y * quality.ChunkSize ); 21 | } 22 | } 23 | 24 | private TranslatedSdf2D ToLocal( in T sdf ) 25 | where T : ISdf2D 26 | { 27 | return sdf.Translate( new Vector2( Key.X, Key.Y ) * -Resource.Quality.ChunkSize ); 28 | } 29 | 30 | /// 31 | protected override Task OnAddAsync( T sdf ) 32 | { 33 | return Data.AddAsync( ToLocal( sdf ) ); 34 | } 35 | 36 | /// 37 | protected override Task OnSubtractAsync( T sdf ) 38 | { 39 | return Data.SubtractAsync( ToLocal( sdf ) ); 40 | } 41 | 42 | protected override Task OnRebuildAsync( IEnumerable> modifications ) 43 | { 44 | return Data.RebuildAsync( modifications.Select( x => x with { Sdf = ToLocal( x.Sdf ) } ) ); 45 | } 46 | 47 | /// 48 | protected override async Task OnUpdateMeshAsync() 49 | { 50 | var enableRenderMesh = (Resource.FrontFaceMaterial ?? Resource.BackFaceMaterial ?? Resource.CutFaceMaterial) is not null; 51 | var enableCollisionMesh = Resource.HasCollision && World.HasPhysics; 52 | 53 | if ( !IsValid || !enableRenderMesh && !enableCollisionMesh ) 54 | { 55 | return; 56 | } 57 | 58 | using var writer = Sdf2DMeshWriter.Rent(); 59 | 60 | await GameTask.WorkerThread(); 61 | 62 | writer.DebugOffset = ChunkPosition; 63 | writer.DebugScale = Data.Quality.UnitSize; 64 | 65 | try 66 | { 67 | Data.WriteTo( writer, Resource, enableRenderMesh, enableCollisionMesh ); 68 | } 69 | catch ( Exception e ) 70 | { 71 | if ( !IsValid ) return; 72 | 73 | Log.Error( e ); 74 | 75 | writer.Reset(); 76 | } 77 | 78 | var renderTask = Task.CompletedTask; 79 | var collisionTask = Task.CompletedTask; 80 | 81 | if ( enableRenderMesh ) 82 | { 83 | renderTask = UpdateRenderMeshesAsync( 84 | new MeshDescription( writer.FrontWriter, Resource.FrontFaceMaterial ), 85 | new MeshDescription( writer.BackWriter, Resource.BackFaceMaterial ), 86 | new MeshDescription( writer.CutWriter, Resource.CutFaceMaterial ) ); 87 | } 88 | 89 | if ( enableCollisionMesh ) 90 | { 91 | var scale = WorldScale.x; 92 | var offset = new Vector3( Key.X, Key.Y ) * Resource.Quality.ChunkSize; 93 | 94 | collisionTask = GameTask.RunInThreadAsync( async () => 95 | { 96 | // ReSharper disable AccessToDisposedClosure 97 | var vertices = writer.CollisionMesh.Vertices; 98 | 99 | for ( var i = 0; i < vertices.Count; ++i ) 100 | { 101 | vertices[i] += offset; 102 | vertices[i] *= scale; 103 | } 104 | 105 | await UpdateCollisionMeshAsync( writer.CollisionMesh.Vertices, writer.CollisionMesh.Indices ); 106 | // ReSharper restore AccessToDisposedClosure 107 | } ); 108 | } 109 | 110 | await GameTask.WhenAll( renderTask, collisionTask ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Code/3D/Sdf3DMeshWriter.Types.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Sandbox.Sdf; 5 | 6 | partial class Sdf3DMeshWriter 7 | { 8 | /// 9 | /// 10 | /// Z = 0 Z = 1 11 | /// c - d g - h 12 | /// | | | | 13 | /// a - b e - f 14 | /// 15 | /// 16 | [Flags] 17 | private enum CubeConfiguration : byte 18 | { 19 | None = 0, 20 | 21 | A = 1, 22 | B = 2, 23 | C = 4, 24 | D = 8, 25 | E = 16, 26 | F = 32, 27 | G = 64, 28 | H = 128 29 | } 30 | 31 | private enum NormalizedVertex : byte 32 | { 33 | A, 34 | AB, 35 | AC, 36 | AE 37 | } 38 | 39 | private enum CubeVertex : byte 40 | { 41 | A = 0, B = 4, C = 8, D = 12, 42 | E = 16, F = 20, G = 24, H = 28, 43 | 44 | AB = A | NormalizedVertex.AB, 45 | AC = A | NormalizedVertex.AC, 46 | AE = A | NormalizedVertex.AE, 47 | BD = B | NormalizedVertex.AC, 48 | BF = B | NormalizedVertex.AE, 49 | CD = C | NormalizedVertex.AB, 50 | CG = C | NormalizedVertex.AE, 51 | DH = D | NormalizedVertex.AE, 52 | EF = E | NormalizedVertex.AB, 53 | EG = E | NormalizedVertex.AC, 54 | FH = F | NormalizedVertex.AC, 55 | GH = G | NormalizedVertex.AB, 56 | 57 | OffsetMask = 0x03, 58 | BaseMask = 0xff ^ OffsetMask 59 | } 60 | 61 | private enum UvPlane 62 | { 63 | NegX, 64 | PosX, 65 | NegY, 66 | PosY, 67 | NegZ, 68 | PosZ 69 | } 70 | 71 | private record struct VertexKey( int X, int Y, int Z, NormalizedVertex Vertex ) 72 | { 73 | public static VertexKey Normalize( int x, int y, int z, CubeVertex vertex ) 74 | { 75 | // ReSharper disable BitwiseOperatorOnEnumWithoutFlags 76 | var baseVertex = vertex & CubeVertex.BaseMask; 77 | var offset = (NormalizedVertex) (vertex & CubeVertex.OffsetMask); 78 | // ReSharper enable BitwiseOperatorOnEnumWithoutFlags 79 | 80 | switch ( baseVertex ) 81 | { 82 | case CubeVertex.A: 83 | return new VertexKey( x + 0, y + 0, z + 0, offset ); 84 | case CubeVertex.B: 85 | return new VertexKey( x + 1, y + 0, z + 0, offset ); 86 | case CubeVertex.C: 87 | return new VertexKey( x + 0, y + 1, z + 0, offset ); 88 | case CubeVertex.D: 89 | return new VertexKey( x + 1, y + 1, z + 0, offset ); 90 | 91 | case CubeVertex.E: 92 | return new VertexKey( x + 0, y + 0, z + 1, offset ); 93 | case CubeVertex.F: 94 | return new VertexKey( x + 1, y + 0, z + 1, offset ); 95 | case CubeVertex.G: 96 | return new VertexKey( x + 0, y + 1, z + 1, offset ); 97 | case CubeVertex.H: 98 | return new VertexKey( x + 1, y + 1, z + 1, offset ); 99 | default: 100 | throw new NotImplementedException(); 101 | } 102 | } 103 | } 104 | 105 | private record struct Triangle( VertexKey V0, VertexKey V1, VertexKey V2 ) 106 | { 107 | public Triangle( int x, int y, int z, CubeVertex V0, CubeVertex V1, CubeVertex V2 ) 108 | : this( VertexKey.Normalize( x, y, z, V0 ), VertexKey.Normalize( x, y, z, V1 ), VertexKey.Normalize( x, y, z, V2 ) ) 109 | { 110 | } 111 | 112 | public Triangle Flipped => new( V0, V2, V1 ); 113 | } 114 | 115 | [StructLayout( LayoutKind.Sequential )] 116 | public record struct Vertex( Vector3 Position, Vector3 Normal, Vector4 Tangent, Vector2 TexCoord ) 117 | { 118 | public static VertexAttribute[] Layout { get; } = 119 | { 120 | new( VertexAttributeType.Position, VertexAttributeFormat.Float32 ), 121 | new( VertexAttributeType.Normal, VertexAttributeFormat.Float32 ), 122 | new( VertexAttributeType.Tangent, VertexAttributeFormat.Float32, 4 ), 123 | new( VertexAttributeType.TexCoord, VertexAttributeFormat.Float32, 2 ) 124 | }; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Code/2D/Sdf2DLayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Sandbox.Sdf; 5 | 6 | /// 7 | /// Options for how front / back faces connect to the cut face in a . 8 | /// 9 | public enum EdgeStyle 10 | { 11 | /// 12 | /// The two faces meet at a 90 degree angle. 13 | /// 14 | Sharp, 15 | 16 | /// 17 | /// The two faces are connected by a 45 degree bevel. 18 | /// 19 | Bevel, 20 | 21 | /// 22 | /// The two faces smoothly join with a rounded edge. 23 | /// 24 | Round 25 | } 26 | 27 | /// 28 | /// Controls the appearance and physical properties of a layer in a . 29 | /// 30 | [GameResource( "SDF 2D Layer", "sdflayer", $"Properties of a layer in a {nameof( Sdf2DWorld )}", Icon = "layers" )] 31 | public class Sdf2DLayer : SdfResource 32 | { 33 | /// 34 | /// How wide this layer is in the z-axis. This can help prevent 35 | /// z-fighting for overlapping layers. 36 | /// 37 | [HideIf( nameof( IsTextureSourceOnly ), true )] 38 | public float Depth { get; set; } = 64f; 39 | 40 | /// 41 | /// How far to offset this layer in the z-axis. 42 | /// Useful for things like background / foreground layers. 43 | /// 44 | [HideIf( nameof( IsTextureSourceOnly ), true )] 45 | public float Offset { get; set; } = 0f; 46 | 47 | /// 48 | /// How wide a single tile of the texture should be. 49 | /// 50 | [HideIf( nameof( IsTextureSourceOnly ), true )] 51 | public float TexCoordSize { get; set; } = 256f; 52 | 53 | /// 54 | /// Material used by the front face of this layer. 55 | /// 56 | [HideIf( nameof( IsTextureSourceOnly ), true )] 57 | public Material FrontFaceMaterial { get; set; } 58 | 59 | /// 60 | /// Material used by the back face of this layer. 61 | /// 62 | [HideIf( nameof( IsTextureSourceOnly ), true )] 63 | public Material BackFaceMaterial { get; set; } 64 | 65 | /// 66 | /// Material used by the cut face connecting the front and 67 | /// back of this layer. 68 | /// 69 | [HideIf( nameof( IsTextureSourceOnly ), true )] 70 | public Material CutFaceMaterial { get; set; } 71 | 72 | /// 73 | /// Options for how front / back faces connect to the cut face. 74 | /// 75 | public EdgeStyle EdgeStyle { get; set; } 76 | 77 | /// 78 | /// Angles below this will have smooth normals. 79 | /// 80 | public float MaxSmoothAngle { get; set; } = 180f; 81 | 82 | /// 83 | /// How wide the connecting edge should be between front / back faces and the cut face. 84 | /// 85 | [HideIf( nameof(EdgeStyle), EdgeStyle.Sharp )] 86 | public float EdgeRadius { get; set; } = 8f; 87 | 88 | /// 89 | /// How many faces to use for rounded connecting edges between front / back faces and the cut face. 90 | /// 91 | [ShowIf( nameof( EdgeStyle ), EdgeStyle.Round )] 92 | public int EdgeFaces { get; set; } = 3; 93 | 94 | internal override WorldQuality GetQualityFromPreset( WorldQualityPreset preset ) 95 | { 96 | switch ( preset ) 97 | { 98 | case WorldQualityPreset.Low: 99 | return new( 8, 256f, 48f ); 100 | 101 | case WorldQualityPreset.Medium: 102 | return new( 16, 256f, 24f ); 103 | 104 | case WorldQualityPreset.High: 105 | return new( 32, 256f, 12f ); 106 | 107 | case WorldQualityPreset.Extreme: 108 | return new( 32, 128f, 6f ); 109 | 110 | default: 111 | throw new NotImplementedException(); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Code/2D/Sdf2DMeshWriter.Types.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Sandbox.Sdf; 5 | 6 | partial class Sdf2DMeshWriter 7 | { 8 | /// 9 | /// 10 | /// c - d 11 | /// | | 12 | /// a - b 13 | /// 14 | /// 15 | [Flags] 16 | private enum SquareConfiguration : byte 17 | { 18 | None = 0, 19 | 20 | A = 1, 21 | B = 2, 22 | C = 4, 23 | D = 8, 24 | 25 | AB = A | B, 26 | AC = A | C, 27 | BD = B | D, 28 | CD = C | D, 29 | 30 | AD = A | D, 31 | BC = B | C, 32 | 33 | ABC = A | B | C, 34 | ABD = A | B | D, 35 | ACD = A | C | D, 36 | BCD = B | C | D, 37 | 38 | ABCD = A | B | C | D 39 | } 40 | 41 | private enum NormalizedVertex : byte 42 | { 43 | A, 44 | AB, 45 | AC 46 | } 47 | 48 | private enum SquareVertex : byte 49 | { 50 | A, 51 | B, 52 | C, 53 | D, 54 | 55 | AB, 56 | AC, 57 | BD, 58 | CD 59 | } 60 | 61 | private record struct SourceEdge( VertexKey V0, VertexKey V1 ) 62 | { 63 | public SourceEdge( int x, int y, SquareVertex V0, SquareVertex V1 ) 64 | : this( VertexKey.Normalize( x, y, V0 ), VertexKey.Normalize( x, y, V1 ) ) 65 | { 66 | } 67 | } 68 | 69 | private record struct VertexKey( int X, int Y, NormalizedVertex Vertex ) 70 | { 71 | public static VertexKey Normalize( int x, int y, SquareVertex vertex ) 72 | { 73 | switch ( vertex ) 74 | { 75 | case SquareVertex.A: 76 | return new VertexKey( x, y, NormalizedVertex.A ); 77 | 78 | case SquareVertex.AB: 79 | return new VertexKey( x, y, NormalizedVertex.AB ); 80 | 81 | case SquareVertex.AC: 82 | return new VertexKey( x, y, NormalizedVertex.AC ); 83 | 84 | 85 | case SquareVertex.B: 86 | return new VertexKey( x + 1, y, NormalizedVertex.A ); 87 | 88 | case SquareVertex.C: 89 | return new VertexKey( x, y + 1, NormalizedVertex.A ); 90 | 91 | case SquareVertex.D: 92 | return new VertexKey( x + 1, y + 1, NormalizedVertex.A ); 93 | 94 | 95 | case SquareVertex.BD: 96 | return new VertexKey( x + 1, y, NormalizedVertex.AC ); 97 | 98 | case SquareVertex.CD: 99 | return new VertexKey( x, y + 1, NormalizedVertex.AB ); 100 | 101 | 102 | default: 103 | throw new NotImplementedException(); 104 | } 105 | } 106 | } 107 | 108 | [StructLayout( LayoutKind.Sequential )] 109 | public record struct Vertex( Vector3 Position, Vector3 Normal, Vector4 Tangent, Vector2 TexCoord ) 110 | { 111 | public static VertexAttribute[] Layout { get; } = 112 | { 113 | new( VertexAttributeType.Position, VertexAttributeFormat.Float32 ), 114 | new( VertexAttributeType.Normal, VertexAttributeFormat.Float32 ), 115 | new( VertexAttributeType.Tangent, VertexAttributeFormat.Float32, 4 ), 116 | new( VertexAttributeType.TexCoord, VertexAttributeFormat.Float32, 2 ) 117 | }; 118 | } 119 | 120 | public readonly struct VertexHelper : IVertexHelper 121 | { 122 | public Vector3 GetPosition( in Vertex vertex ) 123 | { 124 | return vertex.Position; 125 | } 126 | 127 | private static Vector3 Slerp( Vector3 a, Vector3 b, float t ) 128 | { 129 | var omega = Vector3.GetAngle( a, b ) * MathF.PI / 180f; 130 | 131 | if ( Math.Abs( omega ) <= 0.001f ) 132 | { 133 | return Vector3.Lerp( a, b, t ); 134 | } 135 | 136 | return (MathF.Sin( (1f - t) * omega ) * a + MathF.Sin( t * omega ) * b) / MathF.Sin( omega ); 137 | } 138 | 139 | public Vertex Lerp( in Vertex a, in Vertex b, float t ) 140 | { 141 | var normal = Slerp( a.Normal, b.Normal, t ).Normal; 142 | var tangent = Slerp( a.Tangent, b.Tangent, t ); 143 | var binormal = Vector3.Cross( normal, tangent ); 144 | 145 | tangent = Vector3.Cross( binormal, normal ).Normal; 146 | 147 | return new Vertex( 148 | Vector3.Lerp( a.Position, b.Position, t ), 149 | normal, 150 | new Vector4( tangent, 1f ), 151 | Vector2.Lerp( a.TexCoord, b.TexCoord, t ) ); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Assets/shaders/sdf2d/scorched.shader: -------------------------------------------------------------------------------- 1 | //========================================================================================================================= 2 | // Optional 3 | //========================================================================================================================= 4 | HEADER 5 | { 6 | Description = "Simple shader for SDF 2D Layers with a scorch effect"; 7 | } 8 | 9 | //========================================================================================================================= 10 | // Optional 11 | //========================================================================================================================= 12 | FEATURES 13 | { 14 | #include "common/features.hlsl" 15 | } 16 | 17 | //========================================================================================================================= 18 | COMMON 19 | { 20 | #include "common/shared.hlsl" 21 | #include "sdf2d/shared.hlsl" 22 | } 23 | 24 | //========================================================================================================================= 25 | 26 | struct VertexInput 27 | { 28 | #include "common/vertexinput.hlsl" 29 | }; 30 | 31 | //========================================================================================================================= 32 | 33 | struct PixelInput 34 | { 35 | #include "common/pixelinput.hlsl" 36 | 37 | float3 vPositionOs : TEXCOORD15; 38 | }; 39 | 40 | //========================================================================================================================= 41 | 42 | VS 43 | { 44 | #include "common/vertex.hlsl" 45 | 46 | // 47 | // Main 48 | // 49 | PixelInput MainVs( INSTANCED_SHADER_PARAMS( VertexInput i ) ) 50 | { 51 | PixelInput o = ProcessVertex( i ); 52 | 53 | o.vPositionOs = i.vPositionOs; 54 | 55 | return FinalizeVertex( o ); 56 | } 57 | } 58 | 59 | //========================================================================================================================= 60 | 61 | PS 62 | { 63 | #include "common/pixel.hlsl" 64 | 65 | CreateSdfLayerTexture( ScorchLayer ); 66 | 67 | CreateInputTexture2D( ScorchColor, Srgb, 8, "", "_color", "Scorch,10/10", Default3( 1.0, 1.0, 1.0 ) ); 68 | CreateInputTexture2D( ScorchBlendMask, Linear, 8, "", "_height", "Scorch,10/20", Default( 1.0 ) ); 69 | CreateInputTexture2D( ScorchRoughness, Linear, 8, "", "_rough", "Scorch,10/30", Default( 0.5 ) ); 70 | CreateInputTexture2D( ScorchMetalness, Linear, 8, "", "_metal", "Scorch,10/40", Default( 1.0 ) ); 71 | CreateInputTexture2D( ScorchAmbientOcclusion, Linear, 8, "", "_ao", "Scorch,10/50", Default( 1.0 ) ); 72 | CreateInputTexture2D( ScorchNormal, Linear, 8, "NormalizeNormals", "_normal", "Scorch,10/60", Default3( 0.5, 0.5, 1.0 ) ); 73 | 74 | CreateTexture2D( g_tScorchColor ) < Channel( RGB, Box( ScorchColor ), Srgb ); OutputFormat( BC7 ); SrgbRead( true ); > ; 75 | CreateTexture2D( g_tScorchNormal ) < Channel( RGB, Box( ScorchNormal ), Linear ); Channel( A, Box( ScorchBlendMask ), Linear ); OutputFormat( DXT5 ); SrgbRead( false ); >; 76 | CreateTexture2D( g_tScorchRma ) < Channel( R, Box( ScorchRoughness ), Linear ); Channel( G, Box( ScorchMetalness ), Linear ); Channel( B, Box( ScorchAmbientOcclusion ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >; 77 | 78 | // 79 | // Main 80 | // 81 | float4 MainPs( PixelInput i ) : SV_Target0 82 | { 83 | Material m = Material::From( i ); 84 | 85 | float signedDist = SdfLayerTex( ScorchLayer, i.vPositionOs.xy ).r; 86 | float3 scorchColor = Tex2D( g_tScorchColor, i.vTextureCoords.xy ).rgb; 87 | float3 scorchRma = Tex2D( g_tScorchRma, i.vTextureCoords.xy ).xyz; 88 | float4 scorchMat = Tex2D( g_tScorchNormal, i.vTextureCoords.xy ); 89 | float scorchMask = scorchMat.a; 90 | 91 | float scorch = 1.0 - clamp( (signedDist - scorchMask * 0.5) * 64.0, 0.0, 1.0 ); 92 | float3 scorchNormal = TransformNormal( i, DecodeNormal( scorchMat.xyz ) ); 93 | 94 | m.Albedo.rgb = lerp( m.Albedo, scorchColor, scorch ); 95 | m.Normal.xyz = normalize( lerp( m.Normal.xyz, scorchNormal, scorch ) ); 96 | m.Roughness = lerp( m.Roughness, scorchRma.x, scorch ); 97 | m.Metalness = lerp( m.Metalness, scorchRma.y, scorch ); 98 | m.AmbientOcclusion = lerp( m.AmbientOcclusion, scorchRma.z, scorch ); 99 | 100 | return ShadingModelStandard::Shade( i, m ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Assets/shaders/sdf3d/triplanar_scorch.shader: -------------------------------------------------------------------------------- 1 | //========================================================================================================================= 2 | // Optional 3 | //========================================================================================================================= 4 | HEADER 5 | { 6 | CompileTargets = ( IS_SM_50 && ( PC || VULKAN ) ); 7 | Description = "Shader for scorchable geometry without texture coordinates"; 8 | } 9 | 10 | //========================================================================================================================= 11 | // Optional 12 | //========================================================================================================================= 13 | FEATURES 14 | { 15 | #include "common/features.hlsl" 16 | } 17 | 18 | //========================================================================================================================= 19 | COMMON 20 | { 21 | #include "common/shared.hlsl" 22 | } 23 | 24 | //========================================================================================================================= 25 | 26 | struct VertexInput 27 | { 28 | #include "common/vertexinput.hlsl" 29 | }; 30 | 31 | //========================================================================================================================= 32 | 33 | struct PixelInput 34 | { 35 | #include "common/pixelinput.hlsl" 36 | 37 | float3 vPositionOs : TEXCOORD15; 38 | float3 vNormalOs : TEXCOORD16; 39 | }; 40 | 41 | //========================================================================================================================= 42 | 43 | VS 44 | { 45 | #include "common/vertex.hlsl" 46 | 47 | // 48 | // Main 49 | // 50 | PixelInput MainVs( VertexInput i ) 51 | { 52 | PixelInput o = ProcessVertex( i ); 53 | 54 | o.vPositionOs = i.vPositionOs; 55 | o.vNormalOs = i.vNormalOs.xyz; 56 | 57 | return FinalizeVertex( o ); 58 | } 59 | } 60 | 61 | //========================================================================================================================= 62 | 63 | PS 64 | { 65 | #define TRIPLANAR_OBJECT_SPACE 66 | 67 | #include "sdf3d/triplanar.hlsl" 68 | #include "sdf3d/shared.hlsl" 69 | 70 | CreateSdfLayerTexture( ScorchLayer ); 71 | 72 | CreateInputTexture2D( ScorchColor, Srgb, 8, "", "_color", "Scorch,10/10", Default3( 1.0, 1.0, 1.0 ) ); 73 | CreateInputTexture2D( ScorchBlendMask, Linear, 8, "", "_height", "Scorch,10/20", Default( 0.5 ) ); 74 | CreateInputTexture2D( ScorchRoughness, Linear, 8, "", "_rough", "Scorch,10/30", Default( 0.5 ) ); 75 | CreateInputTexture2D( ScorchMetalness, Linear, 8, "", "_metal", "Scorch,10/40", Default( 1.0 ) ); 76 | CreateInputTexture2D( ScorchAmbientOcclusion, Linear, 8, "", "_ao", "Scorch,10/50", Default( 1.0 ) ); 77 | CreateInputTexture2D( ScorchNormal, Linear, 8, "NormalizeNormals", "_normal", "Scorch,10/60", Default3( 0.5, 0.5, 1.0 ) ); 78 | 79 | CreateTexture2D( g_tScorchColor ) < Channel( RGB, Box( ScorchColor ), Srgb ); Channel( A, Box( ScorchBlendMask ), Linear ); OutputFormat( BC7 ); SrgbRead( true ); > ; 80 | CreateTexture2D( g_tScorchNormal ) < Channel( RGB, Box( ScorchNormal ), Linear ); OutputFormat( DXT5 ); SrgbRead( false ); >; 81 | CreateTexture2D( g_tScorchRma ) < Channel( R, Box( ScorchRoughness ), Linear ); Channel( G, Box( ScorchMetalness ), Linear ); Channel( B, Box( ScorchAmbientOcclusion ), Linear ); OutputFormat( BC7 ); SrgbRead( false ); >; 82 | 83 | // 84 | // Main 85 | // 86 | float4 MainPs( PixelInput i ) : SV_Target0 87 | { 88 | Material m = ToMaterialTriplanar( i, g_tColor, g_tNormal, g_tRma ); 89 | Material s = ToMaterialTriplanar( i, g_tScorchColor, g_tScorchNormal, g_tScorchRma ); 90 | 91 | float scorchMask = s.Opacity; 92 | 93 | float signedDist = SdfLayerTex( ScorchLayer, i.vPositionOs.xyz ).r; 94 | float scorch = 1.0 - clamp( signedDist * 4.0 - scorchMask * 64.0, 0.0, 1.0 ); 95 | 96 | m.Opacity = 1; 97 | m.Albedo = lerp( m.Albedo, s.Albedo, scorch ); 98 | m.Normal.xyz = normalize( lerp( m.Normal.xyz, s.Normal.xyz, scorch ) ); 99 | m.Roughness = lerp( m.Roughness, s.Roughness, scorch ); 100 | m.Metalness = lerp( m.Metalness, s.Metalness, scorch ); 101 | m.AmbientOcclusion = lerp( m.AmbientOcclusion, s.AmbientOcclusion, scorch ); 102 | 103 | return ShadingModelStandard::Shade( i, m ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Code/SdfResource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Sandbox.Sdf; 6 | 7 | /// 8 | /// References a layer or volume that will be used as a texture when rendering. 9 | /// 10 | public class TextureReference 11 | where T : SdfResource 12 | { 13 | /// 14 | /// Material attribute name to set for the materials used by this layer or volume. 15 | /// 16 | public string TargetAttribute { get; set; } 17 | 18 | /// 19 | /// Source layer or volume that will provide the texture. The texture will have a single channel, 20 | /// with 0 representing - of the source layer, 21 | /// and 1 representing +. 22 | /// 23 | public T Source { get; set; } 24 | } 25 | 26 | /// 27 | /// Base class for SDF volume / layer resources. 28 | /// 29 | /// 30 | public abstract class SdfResource : GameResource 31 | where T : SdfResource 32 | { 33 | #pragma warning disable SB3000 34 | // ReSharper disable once StaticMemberInGenericType 35 | private static char[] SplitChars { get; } = { ' ' }; 36 | #pragma warning restore SB3000 37 | 38 | /// 39 | /// If true, this layer or volume is only used as a texture source by other layers or volumes. 40 | /// This will disable collision shapes and render mesh generation for this layer or volume. 41 | /// 42 | public bool IsTextureSourceOnly { get; set; } 43 | 44 | /// 45 | /// Tags that physics shapes created by this layer or volume should have, separated by spaces. 46 | /// If empty, no physics shapes will be created. 47 | /// 48 | [Editor( "tags" )] 49 | [HideIf( nameof( IsTextureSourceOnly ), true )] 50 | public string CollisionTags { get; set; } = "solid"; 51 | 52 | /// 53 | /// Array of tags that physics shapes created by this layer or volume should have. 54 | /// If empty, no physics shapes will be created. 55 | /// 56 | [Hide] 57 | [JsonIgnore] 58 | public string[] SplitCollisionTags => IsTextureSourceOnly 59 | ? Array.Empty() 60 | : CollisionTags?.Split( SplitChars, StringSplitOptions.RemoveEmptyEntries ) ?? Array.Empty(); 61 | 62 | /// 63 | /// If true, this resource will have a collision mesh. True if has any items 64 | /// and is false. 65 | /// 66 | [Hide] 67 | [JsonIgnore] 68 | public bool HasCollision => !IsTextureSourceOnly && !string.IsNullOrWhiteSpace( CollisionTags ); 69 | 70 | /// 71 | /// Controls mesh visual quality, affecting performance and networking costs. 72 | /// 73 | public WorldQualityPreset QualityLevel { get; set; } = WorldQualityPreset.Medium; 74 | 75 | /// 76 | /// How many rows / columns of samples are stored per chunk. 77 | /// Higher means more needs to be sent over the network, and more work for the mesh generator. 78 | /// Medium quality is 16 for 2D layers. 79 | /// 80 | [ShowIf( nameof( QualityLevel ), WorldQualityPreset.Custom )] 81 | public int ChunkResolution { get; set; } = 16; 82 | 83 | /// 84 | /// How wide / tall a chunk is in world space. If you'll always make small 85 | /// edits to this layer, you can reduce this to add detail. 86 | /// Medium quality is 256 for 2D layers. 87 | /// 88 | [ShowIf( nameof( QualityLevel ), WorldQualityPreset.Custom )] 89 | public float ChunkSize { get; set; } = 256f; 90 | 91 | /// 92 | /// Largest absolute value stored in a chunk's SDF. 93 | /// Higher means more samples are written to when doing modifications. 94 | /// I'd arbitrarily recommend ChunkSize / ChunkResolution * 4. 95 | /// 96 | [ShowIf( nameof( QualityLevel ), WorldQualityPreset.Custom )] 97 | public float MaxDistance { get; set; } = 64f; 98 | 99 | /// 100 | /// References to layers or volumes that will be used as textures when rendering this layer or volume. 101 | /// All referenced layers or volumes must have the same chunk size as this layer or volume. 102 | /// 103 | [HideIf( nameof( IsTextureSourceOnly ), true )] 104 | public List> ReferencedTextures { get; set; } 105 | 106 | [Hide] 107 | [JsonIgnore] 108 | internal WorldQuality Quality => QualityLevel switch 109 | { 110 | WorldQualityPreset.Custom => new WorldQuality( ChunkResolution, ChunkSize, MaxDistance ), 111 | _ => GetQualityFromPreset( QualityLevel ) 112 | }; 113 | 114 | internal abstract WorldQuality GetQualityFromPreset( WorldQualityPreset preset ); 115 | 116 | [Hide] 117 | [JsonIgnore] 118 | internal int ChangeCount { get; private set; } 119 | 120 | protected override void PostReload() 121 | { 122 | base.PostReload(); 123 | 124 | ++ChangeCount; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /scripts/codegen.py: -------------------------------------------------------------------------------- 1 | output = [] 2 | output.append("//") 3 | output.append("// PLEASE DO NOT EDIT!!") 4 | output.append("// This file was generated by Scripts/codegen.py, go edit that instead!") 5 | output.append("//") 6 | output.append("") 7 | output.append("namespace Sandbox.Sdf;") 8 | output.append("") 9 | output.append("partial class Sdf3DMeshWriter") 10 | output.append("{") 11 | output.append("\tpartial void AddTriangles( in Sdf3DArrayData data, int x, int y, int z )") 12 | output.append("\t{") 13 | output.append("\t\tvar aRaw = data[x + 0, y + 0, z + 0];") 14 | output.append("\t\tvar bRaw = data[x + 1, y + 0, z + 0];") 15 | output.append("\t\tvar cRaw = data[x + 0, y + 1, z + 0];") 16 | output.append("\t\tvar dRaw = data[x + 1, y + 1, z + 0];") 17 | output.append("") 18 | output.append("\t\tvar eRaw = data[x + 0, y + 0, z + 1];") 19 | output.append("\t\tvar fRaw = data[x + 1, y + 0, z + 1];") 20 | output.append("\t\tvar gRaw = data[x + 0, y + 1, z + 1];") 21 | output.append("\t\tvar hRaw = data[x + 1, y + 1, z + 1];") 22 | output.append("") 23 | output.append("\t\tvar a = aRaw < 128 ? CubeConfiguration.A : 0;") 24 | output.append("\t\tvar b = bRaw < 128 ? CubeConfiguration.B : 0;") 25 | output.append("\t\tvar c = cRaw < 128 ? CubeConfiguration.C : 0;") 26 | output.append("\t\tvar d = dRaw < 128 ? CubeConfiguration.D : 0;") 27 | output.append("") 28 | output.append("\t\tvar e = eRaw < 128 ? CubeConfiguration.E : 0;") 29 | output.append("\t\tvar f = fRaw < 128 ? CubeConfiguration.F : 0;") 30 | output.append("\t\tvar g = gRaw < 128 ? CubeConfiguration.G : 0;") 31 | output.append("\t\tvar h = hRaw < 128 ? CubeConfiguration.H : 0;") 32 | output.append("") 33 | output.append("\t\tvar config = a | b | c | d | e | f | g | h;") 34 | output.append("") 35 | output.append("\t\tswitch ( config )") 36 | output.append("\t\t{") 37 | 38 | A = 0 39 | B = 1 40 | C = 2 41 | D = 3 42 | 43 | E = 4 44 | F = 5 45 | G = 6 46 | H = 7 47 | 48 | faces: list[list[int]] = [ 49 | [A, B, D, C], 50 | [E, G, H, F], 51 | [A, E, F, B], 52 | [A, C, G, E], 53 | [C, D, H, G], 54 | [B, F, H, D] 55 | ] 56 | 57 | names = [chr(65 + i) for i in range(8)] 58 | 59 | def sort_cut(cut: tuple[int, int]): 60 | if cut[0] > cut[1]: 61 | return (cut[1], cut[0]) 62 | else: 63 | return cut 64 | 65 | def cut_to_vertex(cut: tuple[int, int]): 66 | cut = sort_cut(cut) 67 | return f"CubeVertex.{names[cut[0]]}{names[cut[1]]}" 68 | 69 | for i in range(256): 70 | solid = [(i & (1 << j)) != 0 for j in range(8)] 71 | 72 | cases = "" 73 | 74 | keys = [names[i] for i, x in enumerate(solid) if x] 75 | if len(keys) == 0: 76 | cases += "CubeConfiguration.None" 77 | else: 78 | for key in keys: 79 | if len(cases) > 0: 80 | cases += " | " 81 | cases += f"CubeConfiguration.{key}" 82 | output.append(f"\t\t\tcase {cases}:") 83 | 84 | edges: list[tuple[tuple[int, int], tuple[int, int]]] = [] 85 | 86 | for face in faces: 87 | prev = face[3] 88 | cuts: list[tuple[int, int]] = [] 89 | 90 | for next in face: 91 | if solid[prev] != solid[next]: 92 | cuts.append((prev, next)) 93 | prev = next 94 | 95 | if len(cuts) == 0: 96 | continue 97 | 98 | if solid[cuts[0][1]]: 99 | cuts.append(cuts[0]) 100 | cuts = cuts[1:] 101 | 102 | for j in range(len(cuts) // 2): 103 | cut_a = cuts[j * 2 + 0] 104 | cut_b = cuts[j * 2 + 1] 105 | 106 | edges.append((cut_a, cut_b)) 107 | prev = cut_b 108 | 109 | first_face = True 110 | 111 | while len(edges) > 0: 112 | if not first_face: 113 | output.append("") 114 | 115 | first_face = False 116 | 117 | prev_edge = edges.pop() 118 | 119 | face_edges = [prev_edge] 120 | 121 | found = True 122 | while found: 123 | found = False 124 | 125 | for edge in edges: 126 | if edge[0][0] != prev_edge[1][1] or edge[0][1] != prev_edge[1][0]: 127 | continue 128 | 129 | found = True 130 | edges.remove(edge) 131 | face_edges.append(edge) 132 | prev_edge = edge 133 | break 134 | 135 | for i in range(0, len(face_edges) - 2): 136 | cut_a = face_edges[0][0] 137 | cut_b, cut_c = face_edges[i + 1] 138 | output.append(f"\t\t\t\tAddTriangle( x, y, z, {cut_to_vertex(cut_a)}, {cut_to_vertex(cut_b)}, {cut_to_vertex(cut_c)} );") 139 | 140 | output.append("\t\t\t\treturn;") 141 | output.append("") 142 | 143 | output.append("\t\t}") 144 | output.append("\t}") 145 | output.append("}") 146 | 147 | with open("../Code/3D/Sdf3DMeshWriter.Generated.cs", "tw", newline="\r\n") as f: 148 | f.writelines(line + "\n" for line in output) 149 | -------------------------------------------------------------------------------- /Code/2D/Transform2D.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Sandbox.Sdf; 4 | 5 | /// 6 | /// Represents a 2D rotation around the Z axis. 7 | /// 8 | /// Cosine of the rotation angle. 9 | /// Sine of the rotation angle. 10 | public record struct Rotation2D( float Cos, float Sin ) 11 | { 12 | /// 13 | /// Converts an angle in degrees to a . 14 | /// 15 | /// Angle in degrees. 16 | /// Rotation corresponding to the given angle. 17 | public static implicit operator Rotation2D( float degrees ) 18 | { 19 | return new Rotation2D( degrees ); 20 | } 21 | 22 | /// 23 | /// Converts a rotation to its vector representation. 24 | /// 25 | public static implicit operator Vector2( Rotation2D rotation ) 26 | { 27 | return new Vector2( rotation.Cos, rotation.Sin ); 28 | } 29 | 30 | /// 31 | /// Converts a rotation from its vector representation. 32 | /// 33 | public static implicit operator Rotation2D( Vector2 vector ) 34 | { 35 | return new Rotation2D( vector.x, vector.y ); 36 | } 37 | 38 | /// 39 | /// Represents a rotation of 0 degrees. 40 | /// 41 | public static Rotation2D Identity { get; } = new( 1f, 0f ); 42 | 43 | /// 44 | /// Applies a rotation to a vector. 45 | /// 46 | /// Rotation to apply. 47 | /// Vector to rotate. 48 | /// A rotated vector. 49 | public static Vector2 operator *( Rotation2D rotation, Vector2 vector ) 50 | { 51 | return rotation.UnitX * vector.x + rotation.UnitY * vector.y; 52 | } 53 | 54 | /// 55 | /// Combines two rotations. 56 | /// 57 | /// First rotation. 58 | /// Second rotation. 59 | /// A combined rotation. 60 | public static Rotation2D operator *( Rotation2D lhs, Rotation2D rhs ) 61 | { 62 | return new Rotation2D( lhs.Cos * rhs.Cos - lhs.Sin * rhs.Sin, lhs.Sin * rhs.Cos + lhs.Cos * rhs.Sin ); 63 | } 64 | 65 | /// 66 | /// Result of rotating (1, 0) by this rotation. 67 | /// 68 | public Vector2 UnitX => new( Cos, -Sin ); 69 | 70 | /// 71 | /// Result of rotating (0, 1) by this rotation. 72 | /// 73 | public Vector2 UnitY => new( Sin, Cos ); 74 | 75 | /// 76 | /// Inverse of this rotation. 77 | /// 78 | public Rotation2D Inverse => this with { Sin = -Sin }; 79 | 80 | /// 81 | /// A normalized version of this rotation, in case ^2 + ^2 != 1. 82 | /// 83 | public Rotation2D Normalized 84 | { 85 | get 86 | { 87 | var length = MathF.Sqrt( Cos * Cos + Sin * Sin ); 88 | var scale = 1f / length; 89 | 90 | return new Rotation2D( Cos * scale, Sin * scale ); 91 | } 92 | } 93 | 94 | /// 95 | /// Represents a 2D rotation around the Z axis. 96 | /// 97 | /// Angle in degrees. 98 | public Rotation2D( float degrees ) 99 | : this( MathF.Cos( degrees * MathF.PI / 180f ), MathF.Sin( degrees * MathF.PI / 180f ) ) 100 | { 101 | } 102 | } 103 | 104 | /// 105 | /// Translation this transform will apply. 106 | /// Rotation this transform will apply. 107 | /// Scale this transform will apply. 108 | /// Inverse of . 109 | public record struct Transform2D( Vector2 Position, Rotation2D Rotation, float Scale, float InverseScale ) 110 | { 111 | /// 112 | /// Represents no transformation. 113 | /// 114 | public static Transform2D Identity { get; } = new( Vector2.Zero, Rotation2D.Identity ); 115 | 116 | /// 117 | /// Translation this transform will apply. 118 | /// Rotation this transform will apply. 119 | /// Scale this transform will apply. 120 | public Transform2D( Vector2? position = null, Rotation2D? rotation = null, float scale = 1f ) 121 | : this( position ?? Vector2.Zero, rotation ?? Rotation2D.Identity, scale, 1f / scale ) 122 | { 123 | } 124 | 125 | /// 126 | /// Apply this transformation to a position. 127 | /// 128 | /// Position to transform. 129 | /// Transformed position. 130 | public Vector2 TransformPoint( Vector2 pos ) 131 | { 132 | return Position + Rotation * (pos * Scale); 133 | } 134 | 135 | /// 136 | /// Apply the inverse of this transformation to a position. 137 | /// 138 | /// Position to transform. 139 | /// Transformed position. 140 | public Vector2 InverseTransformPoint( Vector2 pos ) 141 | { 142 | return InverseScale * (Rotation.Inverse * (pos - Position)); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Code/3D/Noise/Cellular.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Sandbox.Sdf.Noise 9 | { 10 | public record struct CellularNoiseSdf3D( int Seed, Vector3 CellSize, float DistanceOffset, Vector3 InvCellSize ) : ISdf3D 11 | { 12 | public CellularNoiseSdf3D( int seed, Vector3 cellSize, float distanceOffset ) 13 | : this( seed, cellSize, distanceOffset, new Vector3( 1f / cellSize.x, 1f / cellSize.y, 1f / cellSize.z ) ) 14 | { 15 | } 16 | 17 | public BBox? Bounds => null; 18 | 19 | public float this[ Vector3 pos ] 20 | { 21 | get 22 | { 23 | var localPos = pos * InvCellSize; 24 | var cell = ( 25 | X: (int)MathF.Floor( localPos.x ), 26 | Y: (int)MathF.Floor( localPos.y ), 27 | Z: (int)MathF.Floor( localPos.z )); 28 | 29 | var cellPos = new Vector3( cell.X, cell.Y, cell.Z ) * CellSize; 30 | var cellLocalPos = pos - cellPos; 31 | 32 | var minDistSq = float.PositiveInfinity; 33 | 34 | foreach ( var offset in PointOffsets ) 35 | { 36 | var feature = GetFeature( cell.X + offset.X, cell.Y + offset.Y, cell.Z + offset.Z ) + new Vector3( offset.X, offset.Y, offset.Z ) * CellSize; 37 | var distSq = (feature - cellLocalPos).LengthSquared; 38 | 39 | minDistSq = Math.Min( minDistSq, distSq ); 40 | } 41 | 42 | return MathF.Sqrt( minDistSq ) - DistanceOffset; 43 | } 44 | } 45 | 46 | Vector3 GetFeature( int x, int y, int z ) 47 | { 48 | var hashX = HashCode.Combine( Seed, x, y, z ); 49 | var hashY = HashCode.Combine( z, Seed, x, y ); 50 | var hashZ = HashCode.Combine( y, z, Seed, x ); 51 | 52 | return new Vector3( (hashX & 0xffff) / 65536f, (hashY & 0xffff) / 65536f, (hashZ & 0xffff) / 65536f ) * CellSize; 53 | } 54 | 55 | private static (int X, int Y, int Z)[] PointOffsets { get; } = Enumerable.Range( -1, 3 ).SelectMany( z => 56 | Enumerable.Range( -1, 3 ).SelectMany( y => Enumerable.Range( -1, 3 ).Select( x => (x, y, z) ) ) ).ToArray(); 57 | 58 | async Task ISdf3D.SampleRangeAsync( Transform transform, float[] output, (int X, int Y, int Z) outputSize ) 59 | { 60 | var localBounds = new BBox( 0f, new Vector3( outputSize.X, outputSize.Y, outputSize.Z ) ); 61 | var bounds = localBounds.Transform( transform ); 62 | var cellBounds = new BBox( bounds.Mins * InvCellSize, bounds.Maxs * InvCellSize ); 63 | 64 | var cellMin = ( 65 | X: (int)MathF.Floor( cellBounds.Mins.x ) - 1, 66 | Y: (int)MathF.Floor( cellBounds.Mins.y ) - 1, 67 | Z: (int)MathF.Floor( cellBounds.Mins.z ) - 1); 68 | 69 | var cellMax = ( 70 | X: (int) MathF.Floor( cellBounds.Maxs.x ) + 2, 71 | Y: (int) MathF.Floor( cellBounds.Maxs.y ) + 2, 72 | Z: (int) MathF.Floor( cellBounds.Maxs.z ) + 2); 73 | 74 | var cellCounts = ( 75 | X: cellMax.X - cellMin.X, 76 | Y: cellMax.Y - cellMin.Y, 77 | Z: cellMax.Z - cellMin.Z); 78 | 79 | var features = ArrayPool.Shared.Rent( cellCounts.X * cellCounts.Y * cellCounts.Z ); 80 | 81 | try 82 | { 83 | for ( var cellZ = 0; cellZ < cellCounts.Z; ++cellZ ) 84 | { 85 | for ( var cellY = 0; cellY < cellCounts.Y; ++cellY ) 86 | { 87 | for ( int cellX = 0, index = cellY * cellCounts.X + cellZ * cellCounts.X * cellCounts.Y; cellX < cellCounts.X; ++cellX, ++index ) 88 | { 89 | features[index] = GetFeature( cellX + cellMin.X, cellY + cellMin.Y, cellZ + cellMin.Z ); 90 | } 91 | } 92 | } 93 | 94 | var cellSize = CellSize; 95 | var invCellSize = InvCellSize; 96 | var distanceOffset = DistanceOffset; 97 | 98 | await GameTask.WorkerThread(); 99 | 100 | for ( var z = 0; z < outputSize.Z; ++z ) 101 | { 102 | for ( var y = 0; y < outputSize.Y; ++y ) 103 | { 104 | for ( int x = 0, index = (y + z * outputSize.Y) * outputSize.X; x < outputSize.X; ++x, ++index ) 105 | { 106 | var pos = transform.PointToWorld( new Vector3( x, y, z ) ); 107 | var localPos = pos * invCellSize; 108 | var cell = ( 109 | X: (int)MathF.Floor( localPos.x ), 110 | Y: (int)MathF.Floor( localPos.y ), 111 | Z: (int)MathF.Floor( localPos.z )); 112 | 113 | var cellPos = new Vector3( cell.X, cell.Y, cell.Z ) * cellSize; 114 | var cellLocalPos = pos - cellPos; 115 | 116 | var minDistSq = float.PositiveInfinity; 117 | 118 | foreach ( var offset in PointOffsets ) 119 | { 120 | var featureCell = ( 121 | X: cell.X + offset.X - cellMin.X, 122 | Y: cell.Y + offset.Y - cellMin.Y, 123 | Z: cell.Z + offset.Z - cellMin.Z); 124 | var featureIndex = featureCell.X + featureCell.Y * cellCounts.X + featureCell.Z * cellCounts.X * cellCounts.Y; 125 | 126 | var feature = features[featureIndex] + new Vector3( offset.X, offset.Y, offset.Z ) * cellSize; 127 | var distSq = (feature - cellLocalPos).LengthSquared; 128 | 129 | minDistSq = Math.Min( minDistSq, distSq ); 130 | } 131 | 132 | output[index] = MathF.Sqrt( minDistSq ) - distanceOffset; 133 | } 134 | } 135 | } 136 | } 137 | finally 138 | { 139 | ArrayPool.Shared.Return( features ); 140 | } 141 | } 142 | 143 | public void WriteRaw( ref ByteStream writer, Dictionary sdfTypes ) 144 | { 145 | writer.Write( Seed ); 146 | writer.Write( CellSize ); 147 | writer.Write( DistanceOffset ); 148 | } 149 | 150 | public static CellularNoiseSdf3D ReadRaw( ref ByteStream reader, IReadOnlyDictionary> sdfTypes ) 151 | { 152 | return new CellularNoiseSdf3D( reader.Read(), reader.Read(), reader.Read() ); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Code/facepunch.libpolygon/Helpers.cs: -------------------------------------------------------------------------------- 1 | using Sandbox.Sdf; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace Sandbox.Polygons; 9 | 10 | internal static class Helpers 11 | { 12 | public static Vector2 NormalizeSafe( in Vector2 vec ) 13 | { 14 | var length = vec.Length; 15 | 16 | if ( length > 9.9999997473787516E-06 ) 17 | { 18 | return vec / length; 19 | } 20 | else 21 | { 22 | return 0f; 23 | } 24 | } 25 | 26 | public static Vector2 Rotate90( Vector2 v ) 27 | { 28 | return new Vector2( v.y, -v.x ); 29 | } 30 | 31 | public static float Cross( Vector2 a, Vector2 b ) 32 | { 33 | return a.x * b.y - a.y * b.x; 34 | } 35 | 36 | public static bool LineSegmentsIntersect( Vector2 a0, Vector2 a1, Vector2 b0, Vector2 b1 ) 37 | { 38 | return Math.Sign( Cross( a0 - b0, b1 - b0 ) ) != Math.Sign( Cross( a1 - b0, b1 - b0 ) ) 39 | && Math.Sign( Cross( b0 - a0, a1 - a0 ) ) != Math.Sign( Cross( b1 - a0, a1 - a0 ) ); 40 | } 41 | 42 | public static Vector3 RotateNormal( Vector3 oldNormal, float sin, float cos ) 43 | { 44 | var normal2d = new Vector2( oldNormal.x, oldNormal.y ); 45 | 46 | if ( normal2d.LengthSquared <= 0.000001f ) 47 | { 48 | return oldNormal; 49 | } 50 | 51 | normal2d = NormalizeSafe( normal2d ); 52 | 53 | return new Vector3( normal2d.x * cos, normal2d.y * cos, sin ).Normal; 54 | } 55 | 56 | public static float GetEpsilon( Vector2 vec, float frac = 0.0001f ) 57 | { 58 | return Math.Max( Math.Abs( vec.x ), Math.Abs( vec.y ) ) * frac; 59 | } 60 | 61 | public static float GetEpsilon( Vector2 a, Vector2 b, float frac = 0.0001f ) 62 | { 63 | return Math.Max( GetEpsilon( a, frac ), GetEpsilon( b, frac ) ); 64 | } 65 | 66 | public static void UpdateMesh( this Mesh mesh, VertexAttribute[] layout, List vertices, List indices ) 67 | where T : unmanaged 68 | { 69 | if ( !mesh.HasIndexBuffer ) 70 | { 71 | mesh.CreateVertexBuffer( vertices.Count, layout, vertices ); 72 | mesh.CreateIndexBuffer( indices.Count, indices ); 73 | } 74 | else if ( indices.Count > 0 && vertices.Count > 0 ) 75 | { 76 | mesh.SetIndexBufferSize( indices.Count ); 77 | mesh.SetVertexBufferSize( vertices.Count ); 78 | 79 | mesh.SetVertexBufferData( vertices ); 80 | mesh.SetIndexBufferData( indices ); 81 | } 82 | 83 | mesh.SetIndexRange( 0, indices.Count ); 84 | } 85 | } 86 | 87 | public record SdfDataDump( 88 | string Samples, 89 | int BaseIndex, 90 | int Size, 91 | int RowStride ) 92 | { 93 | internal SdfDataDump( Sdf2DArrayData data ) 94 | : this( System.Convert.ToBase64String( data.Samples ), data.BaseIndex, data.Size, data.RowStride ) 95 | { 96 | 97 | } 98 | } 99 | 100 | public record DebugDump( 101 | string Exception, 102 | string EdgeLoops, 103 | SdfDataDump SdfData, 104 | EdgeStyle EdgeStyle, 105 | float EdgeWidth, 106 | int EdgeFaces ) 107 | { 108 | public static string SeriaizeEdgeLoops( IReadOnlyList> loops ) 109 | { 110 | var writer = new StringWriter(); 111 | 112 | foreach ( var loop in loops ) 113 | { 114 | foreach ( var vertex in loop ) 115 | { 116 | writer.Write( $"{vertex.x:R},{vertex.y:R};" ); 117 | } 118 | 119 | writer.Write( "\n" ); 120 | } 121 | 122 | return writer.ToString(); 123 | } 124 | 125 | private static Regex Pattern { get; } = new Regex( @"(?-?[0-9]+(?:\.[0-9]+)?),(?-?[0-9]+(?:\.[0-9]+)?);" ); 126 | 127 | public static IReadOnlyList> DeserializeEdgeLoops( string source ) 128 | { 129 | var loops = new List>(); 130 | 131 | foreach ( var line in source.Split( "\n" ) ) 132 | { 133 | var loop = new List(); 134 | 135 | foreach ( Match match in Pattern.Matches( line ) ) 136 | { 137 | var x = float.Parse( match.Groups["x"].Value ); 138 | var y = float.Parse( match.Groups["y"].Value ); 139 | 140 | loop.Add( new Vector2( x, y ) ); 141 | } 142 | 143 | if ( loop.Count == 0 ) 144 | { 145 | break; 146 | } 147 | 148 | loops.Add( loop ); 149 | } 150 | 151 | return loops; 152 | } 153 | 154 | public DebugDump Reduce() 155 | { 156 | var sourceLoops = DeserializeEdgeLoops( EdgeLoops ); 157 | var erroring = new List>(); 158 | 159 | foreach ( var sourceLoop in sourceLoops ) 160 | { 161 | var singleLoop = new[] { sourceLoop }; 162 | 163 | try 164 | { 165 | using var polyMeshBuilder = PolygonMeshBuilder.Rent(); 166 | 167 | Init( polyMeshBuilder, singleLoop ); 168 | Bevel( polyMeshBuilder ); 169 | Fill( polyMeshBuilder ); 170 | 171 | continue; 172 | } 173 | catch 174 | { 175 | // 176 | } 177 | 178 | if ( sourceLoop.Count < 4 ) 179 | { 180 | erroring.Add( sourceLoop ); 181 | } 182 | 183 | var loop = sourceLoop.ToList(); 184 | 185 | singleLoop[0] = loop; 186 | 187 | for ( var j = loop.Count - 1; j >= 0; --j ) 188 | { 189 | var removed = loop[j]; 190 | 191 | loop.RemoveAt( j ); 192 | 193 | try 194 | { 195 | using var polyMeshBuilder = PolygonMeshBuilder.Rent(); 196 | 197 | Init( polyMeshBuilder, singleLoop ); 198 | Bevel( polyMeshBuilder ); 199 | Fill( polyMeshBuilder ); 200 | } 201 | catch 202 | { 203 | continue; 204 | } 205 | 206 | loop.Insert( j, removed ); 207 | } 208 | 209 | erroring.Add( loop ); 210 | } 211 | 212 | return this with { EdgeLoops = SeriaizeEdgeLoops( erroring ) }; 213 | } 214 | 215 | public void Init( PolygonMeshBuilder meshBuilder ) 216 | { 217 | Init( meshBuilder, DeserializeEdgeLoops( EdgeLoops ) ); 218 | } 219 | 220 | private static void Init( PolygonMeshBuilder meshBuilder, IReadOnlyList> loops ) 221 | { 222 | foreach ( var loop in loops ) 223 | { 224 | meshBuilder.AddEdgeLoop( loop, 0, loop.Count ); 225 | } 226 | } 227 | 228 | public void Bevel( PolygonMeshBuilder meshBuilder, float? width = null ) 229 | { 230 | var w = width ?? EdgeWidth; 231 | var style = width is null ? EdgeStyle : EdgeStyle.Bevel; 232 | 233 | switch ( style ) 234 | { 235 | case EdgeStyle.Sharp: 236 | break; 237 | 238 | case EdgeStyle.Bevel: 239 | meshBuilder.Bevel( w, w ); 240 | break; 241 | 242 | case EdgeStyle.Round: 243 | meshBuilder.Arc( w, w, EdgeFaces ); 244 | break; 245 | } 246 | } 247 | 248 | public void Fill( PolygonMeshBuilder meshBuilder ) 249 | { 250 | meshBuilder.Fill(); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /Code/2D/Sdf2DArray.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace Sandbox.Sdf; 7 | 8 | internal record struct Sdf2DArrayData( byte[] Samples, int BaseIndex, int Size, int RowStride ) 9 | { 10 | public byte this[int x, int y] 11 | { 12 | get 13 | { 14 | if ( x < -1 || x > Size + 1 || y < -1 || y > Size + 1 ) 15 | { 16 | return 191; 17 | } 18 | 19 | return Samples[BaseIndex + x + y * RowStride]; 20 | } 21 | } 22 | } 23 | 24 | /// 25 | /// Array containing raw SDF samples for a . 26 | /// 27 | public partial class Sdf2DArray : SdfArray 28 | { 29 | /// 30 | /// Array containing raw SDF samples for a . 31 | /// 32 | public Sdf2DArray() 33 | : base( 2 ) 34 | { 35 | } 36 | 37 | /// 38 | protected override Texture CreateTexture() 39 | { 40 | return new Texture2DBuilder() 41 | .WithFormat( ImageFormat.I8 ) 42 | .WithSize( ArraySize, ArraySize ) 43 | .WithData( FrontBuffer ) 44 | .WithAnonymous( true ) 45 | .Finish(); 46 | } 47 | 48 | /// 49 | protected override void UpdateTexture( Texture texture ) 50 | { 51 | texture.Update( FrontBuffer ); 52 | } 53 | 54 | private ((int X, int Y) Min, (int X, int Y) Max, Transform transform) GetSampleRange( Rect bounds ) 55 | { 56 | var (minX, maxX, minLocalX, _) = GetSampleRange( bounds.Left, bounds.Right ); 57 | var (minY, maxY, minLocalY, _) = GetSampleRange( bounds.Top, bounds.Bottom ); 58 | 59 | var minPos = new Vector3( minLocalX, minLocalY, 0f ); 60 | 61 | return ((minX, minY), (maxX, maxY), new Transform( minPos, Rotation.Identity, UnitSize ) ); 62 | } 63 | 64 | private bool AddImpl( float[] samples, (int X, int Y) min, (int X, int Y) size ) 65 | { 66 | var max = (X: min.X + size.X, Y: min.Y + size.Y); 67 | var maxDist = Quality.MaxDistance; 68 | var changed = false; 69 | 70 | for ( var y = min.Y; y < max.Y; ++y ) 71 | { 72 | var srcIndex = (y - min.Y) * size.X; 73 | var dstIndex = min.X + y * ArraySize; 74 | 75 | for ( var x = min.X; x < max.X; ++x, ++srcIndex, ++dstIndex ) 76 | { 77 | var sampled = samples[srcIndex]; 78 | 79 | if ( sampled >= maxDist ) continue; 80 | 81 | var encoded = Encode( sampled ); 82 | 83 | var oldValue = BackBuffer[dstIndex]; 84 | var newValue = Math.Min( encoded, oldValue ); 85 | BackBuffer[dstIndex] = newValue; 86 | 87 | changed |= oldValue != newValue; 88 | } 89 | } 90 | 91 | return changed; 92 | } 93 | 94 | private bool SubtractImpl( float[] samples, (int X, int Y) min, (int X, int Y) size ) 95 | { 96 | var max = (X: min.X + size.X, Y: min.Y + size.Y); 97 | var maxDist = Quality.MaxDistance; 98 | var changed = false; 99 | 100 | for ( var y = min.Y; y < max.Y; ++y ) 101 | { 102 | var srcIndex = (y - min.Y) * size.X; 103 | var dstIndex = min.X + y * ArraySize; 104 | 105 | for ( var x = min.X; x < max.X; ++x, ++srcIndex, ++dstIndex ) 106 | { 107 | var sampled = samples[srcIndex]; 108 | 109 | if ( sampled >= maxDist ) continue; 110 | 111 | var encoded = Encode( sampled ); 112 | 113 | var oldValue = BackBuffer[dstIndex]; 114 | var newValue = Math.Max( (byte)(byte.MaxValue - encoded), oldValue ); 115 | 116 | BackBuffer[dstIndex] = newValue; 117 | 118 | changed |= oldValue != newValue; 119 | } 120 | } 121 | 122 | return changed; 123 | } 124 | 125 | /// 126 | public override async Task AddAsync( T sdf ) 127 | { 128 | var (min, max, transform) = GetSampleRange( sdf.Bounds ); 129 | var size = (X: max.X - min.X, Y: max.Y - min.Y); 130 | 131 | var samples = ArrayPool.Shared.Rent( size.X * size.Y ); 132 | 133 | var changed = false; 134 | 135 | try 136 | { 137 | await sdf.SampleRangeAsync( transform, samples, size ); 138 | 139 | await GameTask.WorkerThread(); 140 | 141 | changed |= AddImpl( samples, min, size ); 142 | } 143 | finally 144 | { 145 | ArrayPool.Shared.Return( samples ); 146 | } 147 | 148 | if ( changed ) 149 | { 150 | SwapBuffers(); 151 | MarkChanged(); 152 | } 153 | 154 | return changed; 155 | } 156 | 157 | /// 158 | public override async Task SubtractAsync( T sdf ) 159 | { 160 | var (min, max, transform) = GetSampleRange( sdf.Bounds ); 161 | var size = (X: max.X - min.X, Y: max.Y - min.Y); 162 | 163 | var samples = ArrayPool.Shared.Rent( size.X * size.Y ); 164 | 165 | var changed = false; 166 | 167 | try 168 | { 169 | await sdf.SampleRangeAsync( transform, samples, size ); 170 | 171 | await GameTask.WorkerThread(); 172 | 173 | changed |= SubtractImpl( samples, min, size ); 174 | } 175 | finally 176 | { 177 | ArrayPool.Shared.Return( samples ); 178 | } 179 | 180 | if ( changed ) 181 | { 182 | SwapBuffers(); 183 | MarkChanged(); 184 | } 185 | 186 | return changed; 187 | } 188 | 189 | public override async Task RebuildAsync( IEnumerable> modifications ) 190 | { 191 | Array.Fill( BackBuffer, (byte)255 ); 192 | 193 | var samples = ArrayPool.Shared.Rent( ArraySize * ArraySize * ArraySize ); 194 | 195 | try 196 | { 197 | foreach ( var modification in modifications ) 198 | { 199 | var (min, max, transform) = GetSampleRange( modification.Sdf.Bounds ); 200 | var size = (X: max.X - min.X, Y: max.Y - min.Y); 201 | 202 | await modification.Sdf.SampleRangeAsync( transform, samples, size ); 203 | 204 | switch ( modification.Operator ) 205 | { 206 | case Operator.Add: 207 | AddImpl( samples, min, size ); 208 | break; 209 | case Operator.Subtract: 210 | SubtractImpl( samples, min, size ); 211 | break; 212 | } 213 | } 214 | } 215 | finally 216 | { 217 | ArrayPool.Shared.Return( samples ); 218 | } 219 | 220 | SwapBuffers(); 221 | MarkChanged(); 222 | 223 | return true; 224 | } 225 | 226 | internal void WriteTo( Sdf2DMeshWriter writer, Sdf2DLayer layer, bool renderMesh, bool collisionMesh ) 227 | { 228 | if ( writer.Samples == null || writer.Samples.Length < FrontBuffer.Length ) 229 | { 230 | writer.Samples = new byte[FrontBuffer.Length]; 231 | } 232 | 233 | Array.Copy( FrontBuffer, writer.Samples, FrontBuffer.Length ); 234 | 235 | var resolution = layer.Quality.ChunkResolution; 236 | 237 | var data = new Sdf2DArrayData( writer.Samples, Margin * ArraySize + Margin, resolution, ArraySize ); 238 | 239 | writer.Write( data, layer, renderMesh, collisionMesh ); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /Code/SdfArray.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Sandbox.Sdf; 6 | 7 | /// 8 | /// Base class for networked arrays containing raw SDF samples as bytes. 9 | /// 10 | /// Interface for SDFs that can modify the array 11 | public abstract partial class SdfArray : IDisposable 12 | where TSdf : ISdf 13 | { 14 | /// 15 | /// How far outside the chunk boundary should samples be stored. 16 | /// This is used to ensure generated normals are smooth when on a chunk boundary. 17 | /// 18 | public const int Margin = 1; 19 | 20 | /// 21 | /// Spacial dimensions of the array (2D / 3D). 22 | /// 23 | public int Dimensions { get; } 24 | 25 | /// 26 | /// Quality settings, affecting the resolution of the array. 27 | /// 28 | public WorldQuality Quality { get; private set; } 29 | 30 | /// 31 | /// Actual raw samples, encoded as bytes. A value of 0 is -, 32 | /// 255 is +, and 127.5 is on the surface. 33 | /// 34 | public byte[] BackBuffer { get; private set; } 35 | public byte[] FrontBuffer { get; private set; } 36 | 37 | /// 38 | /// Number of samples stored in one dimension of this array. 39 | /// 40 | public int ArraySize { get; private set; } 41 | 42 | /// 43 | /// Total number of samples stored in the entire array. Equal to to the 44 | /// power of . 45 | /// 46 | public int SampleCount { get; private set; } 47 | 48 | /// 49 | /// Distance between samples in one axis. 50 | /// 51 | protected float UnitSize { get; private set; } 52 | 53 | /// 54 | /// Inverse of . 55 | /// 56 | protected float InvUnitSize { get; private set; } 57 | 58 | /// 59 | /// Inverse of . 60 | /// 61 | protected float InvMaxDistance { get; private set; } 62 | 63 | private bool _textureInvalid = true; 64 | private Texture _texture; 65 | 66 | /// 67 | /// Creates an array with a given number of spacial dimensions. 68 | /// 69 | /// Spacial dimensions of the array (2D / 3D). 70 | protected SdfArray( int dimensions ) 71 | { 72 | Dimensions = dimensions; 73 | } 74 | 75 | protected void SwapBuffers() 76 | { 77 | Array.Copy( BackBuffer, FrontBuffer, BackBuffer.Length ); 78 | } 79 | 80 | /// 81 | /// Gets the min and max index for a local-space range of samples, clamped to the array bounds. 82 | /// 83 | /// Minimum position in local-space along the axis 84 | /// Maximum position in local-space along the axis 85 | /// Minimum (inclusive) and maximum (exclusive) indices 86 | protected (int Min, int Max, float LocalMin, float LocalMax) GetSampleRange( float localMin, float localMax ) 87 | { 88 | var min = Math.Max( 0, (int)MathF.Ceiling( (localMin - Quality.MaxDistance) * InvUnitSize ) + Margin ); 89 | var max = Math.Min( ArraySize, (int)MathF.Ceiling( (localMax + Quality.MaxDistance) * InvUnitSize ) + Margin ); 90 | 91 | localMin = (min - Margin) * UnitSize; 92 | localMax = (max - Margin) * UnitSize; 93 | 94 | return (min, max, localMin, localMax); 95 | } 96 | 97 | /// 98 | /// Encodes a distance value to a byte. 99 | /// 100 | /// Distance to encode. 101 | /// - encodes to 0, 102 | /// + encodes to 255, and therefore 0 becomes ~128. 103 | protected byte Encode( float distance ) 104 | { 105 | return (byte) ((int) ((distance * InvMaxDistance * 0.5f + 0.5f) * byte.MaxValue)).Clamp( 0, 255 ); 106 | } 107 | 108 | /// 109 | /// Lazily creates / updates a texture containing the encoded samples for use in shaders. 110 | /// 111 | public Texture Texture 112 | { 113 | get 114 | { 115 | if ( !_textureInvalid && _texture != null ) return _texture; 116 | 117 | ThreadSafe.AssertIsMainThread(); 118 | 119 | _textureInvalid = false; 120 | 121 | if ( _texture == null ) 122 | _texture = CreateTexture(); 123 | else 124 | UpdateTexture( _texture ); 125 | 126 | return _texture; 127 | } 128 | } 129 | 130 | /// 131 | /// Implements creating a texture containing the encoded samples. 132 | /// 133 | /// A 2D / 3D texture containing the samples 134 | protected abstract Texture CreateTexture(); 135 | 136 | /// 137 | /// Implements updating a texture containing the encoded samples. 138 | /// 139 | protected abstract void UpdateTexture( Texture texture ); 140 | 141 | /// 142 | /// Implements adding a local-space shape to the samples in this array. 143 | /// 144 | /// SDF type 145 | /// Shape to add 146 | /// True if any geometry was modified 147 | public abstract Task AddAsync( T sdf ) 148 | where T : TSdf; 149 | 150 | /// 151 | /// Implements subtracting a local-space shape from the samples in this array. 152 | /// 153 | /// SDF type 154 | /// Shape to subtract 155 | /// True if any geometry was modified 156 | public abstract Task SubtractAsync( T sdf ) 157 | where T : TSdf; 158 | 159 | public abstract Task RebuildAsync( IEnumerable> modifications ); 160 | 161 | internal void Init( WorldQuality quality ) 162 | { 163 | if ( Quality.Equals( quality ) ) 164 | { 165 | return; 166 | } 167 | 168 | Quality = quality; 169 | 170 | ArraySize = Quality.ChunkResolution + Margin * 2 + 1; 171 | UnitSize = Quality.ChunkSize / Quality.ChunkResolution; 172 | InvUnitSize = Quality.ChunkResolution / Quality.ChunkSize; 173 | InvMaxDistance = 1f / Quality.MaxDistance; 174 | 175 | SampleCount = 1; 176 | 177 | for ( var i = 0; i < Dimensions; ++i ) 178 | { 179 | SampleCount *= ArraySize; 180 | } 181 | 182 | BackBuffer = new byte[SampleCount]; 183 | FrontBuffer = new byte[SampleCount]; 184 | 185 | Clear( false ); 186 | } 187 | 188 | /// 189 | /// Sets every sample to solid or empty. 190 | /// 191 | /// Solidity to set each sample to. 192 | public void Clear( bool solid ) 193 | { 194 | Array.Fill( BackBuffer, solid ? (byte) 0 : (byte) 255 ); 195 | SwapBuffers(); 196 | MarkChanged(); 197 | } 198 | 199 | /// 200 | /// Invalidates the texture, and collision / render meshes for the chunk this array represents. 201 | /// This doesn't trigger a net write. 202 | /// 203 | protected void MarkChanged() 204 | { 205 | _textureInvalid = true; 206 | } 207 | 208 | /// 209 | public void Dispose() 210 | { 211 | _texture?.Dispose(); 212 | _texture = null; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /Code/facepunch.libpolygon/PolygonMeshBuilder.SVG.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Sandbox.Utility.Svg; 6 | 7 | namespace Sandbox.Polygons; 8 | 9 | /// 10 | /// Options for . 11 | /// 12 | public class AddSvgOptions 13 | { 14 | public static AddSvgOptions Default { get; } = new(); 15 | 16 | /// 17 | /// If true, any unsupported path types will throw an exception. Defaults to false. 18 | /// 19 | public bool ThrowIfNotSupported { get; set; } 20 | 21 | /// 22 | /// Maximum distance between vertices on curved paths. Defaults to 1. 23 | /// 24 | public float CurveResolution { get; set; } = 1f; 25 | 26 | public bool KeepAspectRatio { get; set; } = true; 27 | } 28 | 29 | partial class PolygonMeshBuilder 30 | { 31 | /// 32 | /// Add all supported paths from the given SVG document. 33 | /// 34 | /// SVG document contents. 35 | /// Options for generating vertices from paths. 36 | /// Rescale and translate the imported SVG to fill the given bounds 37 | public PolygonMeshBuilder AddSvg( string contents, AddSvgOptions options = null, Rect? targetBounds = null ) 38 | { 39 | options ??= AddSvgOptions.Default; 40 | 41 | var svg = SvgDocument.FromString( contents ); 42 | 43 | if ( svg.Paths.Count == 0 ) 44 | { 45 | return this; 46 | } 47 | 48 | if ( targetBounds == null ) 49 | { 50 | foreach ( var path in svg.Paths ) 51 | { 52 | AddPath( path, options ); 53 | } 54 | 55 | return this; 56 | } 57 | 58 | var bounds = svg.Paths[0].Bounds; 59 | 60 | foreach ( var path in svg.Paths ) 61 | { 62 | bounds.Add( path.Bounds ); 63 | } 64 | 65 | var scale = targetBounds.Value.Size / bounds.Size; 66 | var aspectOffset = Vector2.Zero; 67 | 68 | if ( options.KeepAspectRatio ) 69 | { 70 | var oldScale = scale; 71 | 72 | scale = Math.Min( scale.x, scale.y ); 73 | aspectOffset = (oldScale - scale) * targetBounds.Value.Size * 0.25f; 74 | } 75 | 76 | var offset = targetBounds.Value.Position - bounds.Position * scale + aspectOffset; 77 | 78 | foreach ( var path in svg.Paths ) 79 | { 80 | AddPath( path, options, offset, scale ); 81 | } 82 | 83 | return this; 84 | } 85 | 86 | private static void ThrowNotSupported( AddSvgOptions options, string message ) 87 | { 88 | if ( !options.ThrowIfNotSupported ) 89 | { 90 | return; 91 | } 92 | 93 | throw new NotImplementedException( $"SVG path element not supported: {message}" ); 94 | } 95 | 96 | /// 97 | /// Add an individual path from an SVG document, if supported. 98 | /// 99 | /// SVG path element. 100 | /// Options for generating vertices from paths. 101 | /// Rescale and translate the imported SVG to fill the given bounds 102 | public PolygonMeshBuilder AddPath( SvgPath path, AddSvgOptions options = null ) 103 | { 104 | options ??= AddSvgOptions.Default; 105 | return AddPath( path, options, Vector2.Zero, Vector2.One ); 106 | } 107 | 108 | private PolygonMeshBuilder AddPath( SvgPath path, AddSvgOptions options, Vector2 offset, Vector2 scale ) 109 | { 110 | if ( path.IsEmpty ) 111 | { 112 | return this; 113 | } 114 | 115 | if ( path.FillColor == null ) 116 | { 117 | return this; 118 | } 119 | 120 | if ( path.FillType != PathFillType.Winding ) 121 | { 122 | if ( options.ThrowIfNotSupported ) 123 | { 124 | //throw new NotImplementedException( "Only fill-type: winding is supported." ); 125 | } 126 | 127 | //return this; 128 | } 129 | 130 | var openPath = new List(); 131 | var last = Vector2.Zero; 132 | 133 | foreach ( var cmd in path.Commands ) 134 | { 135 | switch ( cmd ) 136 | { 137 | case AddPolyPathCommand addPolyPathCommand: 138 | AddPolyPath( addPolyPathCommand, options, offset, scale ); 139 | break; 140 | 141 | case AddCirclePathCommand addCirclePathCommand: 142 | AddCirclePath( addCirclePathCommand, options, openPath, offset, scale ); 143 | break; 144 | 145 | case MoveToPathCommand moveToPathCommand: 146 | openPath.Clear(); 147 | openPath.Add( new Vector2( moveToPathCommand.X, moveToPathCommand.Y ) ); 148 | break; 149 | 150 | case LineToPathCommand lineToPathCommand: 151 | openPath.Add( new Vector2( lineToPathCommand.X, lineToPathCommand.Y ) ); 152 | break; 153 | 154 | case CubicToPathCommand cubicToPathCommand: 155 | CubicToPath( cubicToPathCommand, options, openPath, last ); 156 | break; 157 | 158 | case ClosePathCommand: 159 | if ( openPath.Count >= 3 ) 160 | { 161 | AddEdgeLoop( openPath, 0, openPath.Count, offset, scale ); 162 | } 163 | 164 | openPath.Clear(); 165 | break; 166 | 167 | default: 168 | ThrowNotSupported( options, $"{cmd.GetType()}" ); 169 | break; 170 | } 171 | 172 | if ( openPath.Count > 0 ) 173 | { 174 | last = openPath[^1]; 175 | } 176 | } 177 | 178 | return this; 179 | } 180 | 181 | private void AddPolyPath( AddPolyPathCommand cmd, AddSvgOptions options, Vector2 offset, Vector2 scale ) 182 | { 183 | if ( !cmd.Close ) 184 | { 185 | return; 186 | } 187 | 188 | AddEdgeLoop( cmd.Points, 0, cmd.Points.Count, offset, scale ); 189 | } 190 | 191 | private void AddCirclePath( AddCirclePathCommand cmd, AddSvgOptions options, List openPath, Vector2 offset, Vector2 scale ) 192 | { 193 | openPath.Clear(); 194 | 195 | var center = new Vector2( cmd.X, cmd.Y ); 196 | 197 | for ( var i = 23; i >= 0; i-- ) 198 | { 199 | var r = i * (MathF.PI * 2f / 24f); 200 | 201 | var cos = MathF.Cos( r ); 202 | var sin = MathF.Sin( r ); 203 | 204 | openPath.Add( new Vector2( cos, sin ) * cmd.Radius + center ); 205 | } 206 | 207 | AddEdgeLoop( openPath, 0, openPath.Count, offset, scale ); 208 | } 209 | 210 | private void CubicToPath( CubicToPathCommand cmd, AddSvgOptions options, List openPath, Vector2 last ) 211 | { 212 | var pointCount = 6; 213 | var tScale = 1f / pointCount; 214 | 215 | for ( var i = 0; i < pointCount; i++ ) 216 | { 217 | var t = (i + 1) * tScale; 218 | var s = 1f - t; 219 | 220 | var a = s * s * s; 221 | var b = 3f * s * s * t; 222 | var c = 3f * s * t * t; 223 | var d = t * t * t; 224 | 225 | var p0 = last; 226 | var p1 = new Vector2( cmd.X0, cmd.Y0 ); 227 | var p2 = new Vector2( cmd.X1, cmd.Y1 ); 228 | var p3 = new Vector2( cmd.X2, cmd.Y2 ); 229 | 230 | openPath.Add( p0 * a + p1 * b + p2 * c + p3 * d ); 231 | } 232 | } 233 | 234 | public string ToSvg() 235 | { 236 | var openEdges = new HashSet( _activeEdges ); 237 | var writer = new StringWriter(); 238 | 239 | writer.WriteLine( "" ); 240 | 241 | while ( openEdges.Count > 0 ) 242 | { 243 | var firstIndex = openEdges.First(); 244 | 245 | var edge = _allEdges[firstIndex]; 246 | 247 | writer.Write( " "); 263 | } 264 | 265 | writer.WriteLine( @"" ); 266 | 267 | return writer.ToString(); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /Code/3D/Sdf3DArray.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.Threading; 6 | using System.Threading.Channels; 7 | using System.Threading.Tasks; 8 | 9 | namespace Sandbox.Sdf; 10 | 11 | internal record struct Sdf3DArrayData( byte[] Samples, int Margin, int ArraySize, int BaseIndex ) 12 | { 13 | public Sdf3DArrayData( byte[] samples, int margin, int arraySize ) 14 | : this( samples, margin, arraySize, margin * (1 + arraySize + arraySize * arraySize) ) 15 | { 16 | 17 | } 18 | 19 | public byte this[int x, int y, int z] => Samples[BaseIndex + x + (y + z * ArraySize) * ArraySize]; 20 | 21 | public float this[ float x, int y, int z ] 22 | { 23 | get 24 | { 25 | var x0 = (int) MathF.Floor( x ); 26 | var x1 = x0 + 1; 27 | 28 | var a = this[x0, y, z]; 29 | var b = this[x1, y, z]; 30 | 31 | return a + (b - a) * (x - x0); 32 | } 33 | } 34 | 35 | public float this[int x, float y, int z] 36 | { 37 | get 38 | { 39 | var y0 = (int) MathF.Floor( y ); 40 | var y1 = y0 + 1; 41 | 42 | var a = this[x, y0, z]; 43 | var b = this[x, y1, z]; 44 | 45 | return a + (b - a) * (y - y0); 46 | } 47 | } 48 | 49 | public float this[int x, int y, float z] 50 | { 51 | get 52 | { 53 | var z0 = (int) MathF.Floor( z ); 54 | var z1 = z0 + 1; 55 | 56 | var a = this[x, y, z0]; 57 | var b = this[x, y, z1]; 58 | 59 | return a + (b - a) * (z - z0); 60 | } 61 | } 62 | } 63 | 64 | /// 65 | /// Networked array containing raw SDF samples for a . 66 | /// 67 | public partial class Sdf3DArray : SdfArray 68 | { 69 | /// 70 | /// Networked array containing raw SDF samples for a . 71 | /// 72 | public Sdf3DArray() 73 | : base( 3 ) 74 | { 75 | } 76 | 77 | /// 78 | protected override Texture CreateTexture() 79 | { 80 | return new Texture3DBuilder() 81 | .WithFormat( ImageFormat.I8 ) 82 | .WithSize( ArraySize, ArraySize, ArraySize ) 83 | .WithData( FrontBuffer ) 84 | .WithAnonymous( true ) 85 | .Finish(); 86 | } 87 | 88 | /// 89 | protected override void UpdateTexture( Texture texture ) 90 | { 91 | texture.Update3D( FrontBuffer ); 92 | } 93 | 94 | private ((int X, int Y, int Z) Min, (int X, int Y, int Z) Max, Transform Transform) GetSampleRange( BBox? bounds ) 95 | { 96 | if ( bounds is not {} b ) 97 | { 98 | return ((0, 0, 0), (ArraySize, ArraySize, ArraySize), new Transform( 99 | -Margin * UnitSize, Rotation.Identity, UnitSize )); 100 | } 101 | 102 | var (minX, maxX, minLocalX, maxLocalX) = GetSampleRange( b.Mins.x, b.Maxs.x ); 103 | var (minY, maxY, minLocalY, maxLocalY) = GetSampleRange( b.Mins.y, b.Maxs.y ); 104 | var (minZ, maxZ, minLocalZ, maxLocalZ) = GetSampleRange( b.Mins.z, b.Maxs.z ); 105 | 106 | var min = new Vector3( minLocalX, minLocalY, minLocalZ ); 107 | var max = new Vector3( maxLocalX, maxLocalY, maxLocalZ ); 108 | 109 | return ((minX, minY, minZ), (maxX, maxY, maxZ), new Transform( 110 | min, Rotation.Identity, UnitSize )); 111 | } 112 | 113 | private bool AddImpl( float[] samples, (int X, int Y, int Z) min, (int X, int Y, int Z) size ) 114 | { 115 | var max = (X: min.X + size.X, Y: min.Y + size.Y, Z: min.Z + size.Z); 116 | var maxDist = Quality.MaxDistance; 117 | var changed = false; 118 | 119 | for ( var z = min.Z; z < max.Z; ++z ) 120 | { 121 | for ( var y = min.Y; y < max.Y; ++y ) 122 | { 123 | var srcIndex = (y - min.Y) * size.X + (z - min.Z) * size.X * size.Y; 124 | var dstIndex = min.X + y * ArraySize + z * ArraySize * ArraySize; 125 | 126 | for ( var x = min.X; x < max.X; ++x, ++srcIndex, ++dstIndex ) 127 | { 128 | var sampled = samples[srcIndex]; 129 | 130 | if ( sampled >= maxDist ) continue; 131 | 132 | var encoded = Encode( sampled ); 133 | 134 | var oldValue = BackBuffer[dstIndex]; 135 | var newValue = Math.Min( encoded, oldValue ); 136 | BackBuffer[dstIndex] = newValue; 137 | 138 | changed |= oldValue != newValue; 139 | } 140 | } 141 | } 142 | 143 | return changed; 144 | } 145 | 146 | private bool SubtractImpl( float[] samples, (int X, int Y, int Z) min, (int X, int Y, int Z) size ) 147 | { 148 | var max = (X: min.X + size.X, Y: min.Y + size.Y, Z: min.Z + size.Z); 149 | var maxDist = Quality.MaxDistance; 150 | var changed = false; 151 | 152 | for ( var z = min.Z; z < max.Z; ++z ) 153 | { 154 | for ( var y = min.Y; y < max.Y; ++y ) 155 | { 156 | var srcIndex = (y - min.Y) * size.X + (z - min.Z) * size.X * size.Y; 157 | var dstIndex = min.X + y * ArraySize + z * ArraySize * ArraySize; 158 | 159 | for ( var x = min.X; x < max.X; ++x, ++srcIndex, ++dstIndex ) 160 | { 161 | var sampled = samples[srcIndex]; 162 | 163 | if ( sampled >= maxDist ) continue; 164 | 165 | var encoded = Encode( sampled ); 166 | 167 | var oldValue = BackBuffer[dstIndex]; 168 | var newValue = Math.Max( (byte) (byte.MaxValue - encoded), oldValue ); 169 | 170 | BackBuffer[dstIndex] = newValue; 171 | 172 | changed |= oldValue != newValue; 173 | } 174 | } 175 | } 176 | 177 | return changed; 178 | } 179 | 180 | /// 181 | public override async Task AddAsync( T sdf ) 182 | { 183 | var (min, max, transform) = GetSampleRange( sdf.Bounds ); 184 | var size = (X: max.X - min.X, Y: max.Y - min.Y, Z: max.Z - min.Z); 185 | 186 | var samples = ArrayPool.Shared.Rent( size.X * size.Y * size.Z ); 187 | 188 | var changed = false; 189 | 190 | try 191 | { 192 | await sdf.SampleRangeAsync( transform, samples, size ); 193 | await GameTask.WorkerThread(); 194 | changed |= AddImpl( samples, min, size ); 195 | } 196 | finally 197 | { 198 | ArrayPool.Shared.Return( samples ); 199 | } 200 | 201 | if ( changed ) 202 | { 203 | SwapBuffers(); 204 | MarkChanged(); 205 | } 206 | 207 | return changed; 208 | } 209 | 210 | /// 211 | public override async Task SubtractAsync( T sdf ) 212 | { 213 | var (min, max, transform) = GetSampleRange( sdf.Bounds ); 214 | var size = (X: max.X - min.X, Y: max.Y - min.Y, Z: max.Z - min.Z); 215 | 216 | var samples = ArrayPool.Shared.Rent( size.X * size.Y * size.Z ); 217 | 218 | var changed = false; 219 | 220 | try 221 | { 222 | await sdf.SampleRangeAsync( transform, samples, size ); 223 | await GameTask.WorkerThread(); 224 | changed |= SubtractImpl( samples, min, size ); 225 | } 226 | finally 227 | { 228 | ArrayPool.Shared.Return( samples ); 229 | } 230 | 231 | if ( changed ) 232 | { 233 | SwapBuffers(); 234 | MarkChanged(); 235 | } 236 | 237 | return changed; 238 | } 239 | 240 | public override async Task RebuildAsync( IEnumerable> modifications ) 241 | { 242 | Array.Fill( BackBuffer, (byte) 255 ); 243 | 244 | var samples = ArrayPool.Shared.Rent( ArraySize * ArraySize * ArraySize ); 245 | 246 | try 247 | { 248 | foreach ( var modification in modifications ) 249 | { 250 | var (min, max, transform) = GetSampleRange( modification.Sdf.Bounds ); 251 | var size = (X: max.X - min.X, Y: max.Y - min.Y, Z: max.Z - min.Z); 252 | 253 | await modification.Sdf.SampleRangeAsync( transform, samples, size ); 254 | await GameTask.WorkerThread(); 255 | 256 | switch ( modification.Operator ) 257 | { 258 | case Operator.Add: 259 | AddImpl( samples, min, size ); 260 | break; 261 | 262 | case Operator.Subtract: 263 | SubtractImpl( samples, min, size ); 264 | break; 265 | } 266 | } 267 | } 268 | finally 269 | { 270 | ArrayPool.Shared.Return( samples ); 271 | } 272 | 273 | SwapBuffers(); 274 | MarkChanged(); 275 | 276 | return true; 277 | } 278 | 279 | internal Task WriteToAsync( Sdf3DMeshWriter writer, Sdf3DVolume volume ) 280 | { 281 | if ( writer.Samples == null || writer.Samples.Length < FrontBuffer.Length ) 282 | { 283 | writer.Samples = new byte[FrontBuffer.Length]; 284 | } 285 | 286 | Array.Copy( FrontBuffer, writer.Samples, FrontBuffer.Length ); 287 | 288 | return writer.WriteAsync( new Sdf3DArrayData( writer.Samples, Margin, ArraySize ), volume ); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /Code/3D/Sdf3DMeshWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Sandbox.Sdf; 8 | 9 | internal partial class Sdf3DMeshWriter : Pooled, IMeshWriter 10 | { 11 | private ConcurrentQueue Triangles { get; } = new ConcurrentQueue(); 12 | private Dictionary<(VertexKey Key, UvPlane Plane), int> VertexMap { get; } = new(); 13 | 14 | public List Vertices { get; } = new List(); 15 | public List VertexPositions { get; } = new List(); 16 | public List Indices { get; } = new List(); 17 | 18 | public bool IsEmpty => Indices.Count == 0; 19 | 20 | public byte[] Samples { get; set; } 21 | 22 | public override void Reset() 23 | { 24 | Triangles.Clear(); 25 | VertexMap.Clear(); 26 | 27 | Vertices.Clear(); 28 | VertexPositions.Clear(); 29 | Indices.Clear(); 30 | } 31 | 32 | private void WriteSlice( in Sdf3DArrayData data, Sdf3DVolume volume, int z ) 33 | { 34 | var quality = volume.Quality; 35 | var size = quality.ChunkResolution; 36 | 37 | for ( var y = 0; y < size; ++y ) 38 | for ( var x = 0; x < size; ++x ) 39 | AddTriangles( in data, x, y, z ); 40 | } 41 | 42 | public async Task WriteAsync( Sdf3DArrayData data, Sdf3DVolume volume ) 43 | { 44 | Triangles.Clear(); 45 | VertexMap.Clear(); 46 | 47 | var baseIndex = Vertices.Count; 48 | 49 | var quality = volume.Quality; 50 | var size = quality.ChunkResolution; 51 | 52 | var tasks = new List(); 53 | 54 | for ( var z = 0; z < size; ++z ) 55 | { 56 | var zCopy = z; 57 | 58 | tasks.Add( GameTask.RunInThreadAsync( () => 59 | { 60 | WriteSlice( data, volume, zCopy ); 61 | } ) ); 62 | } 63 | 64 | await GameTask.WhenAll( tasks ); 65 | 66 | await GameTask.WorkerThread(); 67 | 68 | var unitSize = volume.Quality.UnitSize; 69 | 70 | foreach ( var triangle in Triangles ) 71 | { 72 | var pos0 = GetPosition( data, triangle.V0 ); 73 | var pos1 = GetPosition( data, triangle.V1 ); 74 | var pos2 = GetPosition( data, triangle.V2 ); 75 | 76 | var uvPlane = GetUvPlane( pos0, pos1, pos2 ); 77 | 78 | Indices.Add( AddVertex( data, triangle.V0, uvPlane, pos0, unitSize ) ); 79 | Indices.Add( AddVertex( data, triangle.V1, uvPlane, pos1, unitSize ) ); 80 | Indices.Add( AddVertex( data, triangle.V2, uvPlane, pos2, unitSize ) ); 81 | } 82 | 83 | for ( var i = baseIndex; i < Vertices.Count; ++i ) 84 | { 85 | var vertex = Vertices[i]; 86 | 87 | Vertices[i] = vertex with { Normal = vertex.Normal.Normal }; 88 | } 89 | } 90 | 91 | private static UvPlane GetUvPlane( Vector3 pos0, Vector3 pos1, Vector3 pos2 ) 92 | { 93 | var cross = Vector3.Cross( pos1 - pos0, pos2 - pos0 ); 94 | 95 | var absX = MathF.Abs( cross.x ); 96 | var absY = MathF.Abs( cross.y ); 97 | var absZ = MathF.Abs( cross.z ); 98 | 99 | return absX >= absY && absX >= absZ ? cross.x > 0f ? UvPlane.PosX : UvPlane.NegX 100 | : absY >= absZ ? cross.y > 0f ? UvPlane.PosY : UvPlane.NegY 101 | : cross.z > 0f ? UvPlane.PosZ : UvPlane.NegZ; 102 | } 103 | 104 | private static (Vector3 U, Vector3 V) GetTangents( UvPlane plane ) 105 | { 106 | return plane switch 107 | { 108 | UvPlane.PosX => (new Vector3( 0f, 0f, -1f ), new Vector3( 0f, 1f, 0f )), 109 | UvPlane.NegX => (new Vector3( 0f, 0f, 1f ), new Vector3( 0f, 1f, 0f )), 110 | 111 | UvPlane.PosY => (new Vector3( 1f, 0f, 0f ), new Vector3( 0f, 0f, 1f )), 112 | UvPlane.NegY => (new Vector3( -1f, 0f, 0f ), new Vector3( 0f, 0f, 1f )), 113 | 114 | UvPlane.PosZ => (new Vector3( 1f, 0f, 0f ), new Vector3( 0f, 1f, 0f )), 115 | UvPlane.NegZ => (new Vector3( -1f, 0f, 0f ), new Vector3( 0f, 1f, 0f )), 116 | 117 | _ => throw new NotImplementedException() 118 | }; 119 | } 120 | 121 | public void ApplyTo( Mesh mesh ) 122 | { 123 | ThreadSafe.AssertIsMainThread(); 124 | 125 | if ( mesh == null ) 126 | { 127 | return; 128 | } 129 | 130 | if ( mesh.HasVertexBuffer ) 131 | { 132 | if ( Indices.Count > 0 ) 133 | { 134 | if ( mesh.IndexCount < Indices.Count ) 135 | { 136 | mesh.SetIndexBufferSize( Indices.Count ); 137 | } 138 | 139 | if ( mesh.VertexCount < Vertices.Count ) 140 | { 141 | mesh.SetVertexBufferSize( Vertices.Count ); 142 | } 143 | 144 | mesh.SetIndexBufferData( Indices ); 145 | mesh.SetVertexBufferData( Vertices ); 146 | } 147 | 148 | mesh.SetIndexRange( 0, Indices.Count ); 149 | } 150 | else if ( Indices.Count > 0 ) 151 | { 152 | mesh.CreateVertexBuffer( Vertices.Count, Vertex.Layout, Vertices ); 153 | mesh.CreateIndexBuffer( Indices.Count, Indices ); 154 | } 155 | } 156 | 157 | private static Vector3 GetPosition( in Sdf3DArrayData data, VertexKey key ) 158 | { 159 | switch ( key.Vertex ) 160 | { 161 | case NormalizedVertex.A: 162 | { 163 | return new Vector3( key.X, key.Y, key.Z ); 164 | } 165 | 166 | case NormalizedVertex.AB: 167 | { 168 | var a = data[key.X, key.Y, key.Z] - 127.5f; 169 | var b = data[key.X + 1, key.Y, key.Z] - 127.5f; 170 | var t = a / (a - b); 171 | 172 | return new Vector3( key.X + t, key.Y, key.Z ); 173 | } 174 | 175 | case NormalizedVertex.AC: 176 | { 177 | var a = data[key.X, key.Y, key.Z] - 127.5f; 178 | var c = data[key.X, key.Y + 1, key.Z] - 127.5f; 179 | var t = a / (a - c); 180 | 181 | return new Vector3( key.X, key.Y + t, key.Z ); 182 | } 183 | 184 | case NormalizedVertex.AE: 185 | { 186 | var a = data[key.X, key.Y, key.Z] - 127.5f; 187 | var e = data[key.X, key.Y, key.Z + 1] - 127.5f; 188 | var t = a / (a - e); 189 | 190 | return new Vector3( key.X, key.Y, key.Z + t ); 191 | } 192 | 193 | default: 194 | throw new NotImplementedException(); 195 | } 196 | } 197 | 198 | private static Vertex GetVertex( in Sdf3DArrayData data, VertexKey key, UvPlane plane, Vector3 pos ) 199 | { 200 | float xNeg, xPos, yNeg, yPos, zNeg, zPos; 201 | 202 | switch ( key.Vertex ) 203 | { 204 | case NormalizedVertex.A: 205 | { 206 | xNeg = data[key.X - 1, key.Y, key.Z]; 207 | xPos = data[key.X + 1, key.Y, key.Z]; 208 | yNeg = data[key.X, key.Y - 1, key.Z]; 209 | yPos = data[key.X, key.Y + 1, key.Z]; 210 | zNeg = data[key.X, key.Y, key.Z - 1]; 211 | zPos = data[key.X, key.Y, key.Z + 1]; 212 | break; 213 | } 214 | 215 | case NormalizedVertex.AB: 216 | { 217 | xNeg = data[pos.x - 1, key.Y, key.Z]; 218 | xPos = data[pos.x + 1, key.Y, key.Z]; 219 | yNeg = data[pos.x, key.Y - 1, key.Z]; 220 | yPos = data[pos.x, key.Y + 1, key.Z]; 221 | zNeg = data[pos.x, key.Y, key.Z - 1]; 222 | zPos = data[pos.x, key.Y, key.Z + 1]; 223 | break; 224 | } 225 | 226 | case NormalizedVertex.AC: 227 | { 228 | xNeg = data[key.X - 1, pos.y, key.Z]; 229 | xPos = data[key.X + 1, pos.y, key.Z]; 230 | yNeg = data[key.X, pos.y - 1, key.Z]; 231 | yPos = data[key.X, pos.y + 1, key.Z]; 232 | zNeg = data[key.X, pos.y, key.Z - 1]; 233 | zPos = data[key.X, pos.y, key.Z + 1]; 234 | break; 235 | } 236 | 237 | case NormalizedVertex.AE: 238 | { 239 | xNeg = data[key.X - 1, key.Y, pos.z]; 240 | xPos = data[key.X + 1, key.Y, pos.z]; 241 | yNeg = data[key.X, key.Y - 1, pos.z]; 242 | yPos = data[key.X, key.Y + 1, pos.z]; 243 | zNeg = data[key.X, key.Y, pos.z - 1]; 244 | zPos = data[key.X, key.Y, pos.z + 1]; 245 | break; 246 | } 247 | 248 | default: 249 | throw new NotImplementedException(); 250 | } 251 | 252 | var normal = new Vector3( xPos - xNeg, yPos - yNeg, zPos - zNeg ).Normal; 253 | var basisTangents = GetTangents( plane ); 254 | 255 | var u = Vector3.Dot( basisTangents.U, pos ); 256 | var v = Vector3.Dot( basisTangents.V, pos ); 257 | 258 | var tangent = Vector3.Cross( basisTangents.V, normal ); 259 | var binormal = Vector3.Cross( tangent, normal ); 260 | 261 | return new Vertex( pos, normal, 262 | new Vector4( tangent, MathF.Sign( Vector3.Dot( basisTangents.V, binormal ) ) ), 263 | new Vector2( u, v ) ); 264 | } 265 | 266 | partial void AddTriangles( in Sdf3DArrayData data, int x, int y, int z ); 267 | 268 | private void AddTriangle( int x, int y, int z, CubeVertex v0, CubeVertex v1, CubeVertex v2 ) 269 | { 270 | Triangles.Enqueue( new Triangle( x, y, z, v0, v1, v2 ) ); 271 | } 272 | 273 | private int AddVertex( in Sdf3DArrayData data, VertexKey key, UvPlane plane, Vector3 pos, float unitSize ) 274 | { 275 | if ( VertexMap.TryGetValue( (key, plane), out var index ) ) 276 | { 277 | return index; 278 | } 279 | 280 | index = Vertices.Count; 281 | 282 | var vertex = GetVertex( in data, key, plane, pos ); 283 | 284 | vertex = vertex with { Position = vertex.Position * unitSize }; 285 | 286 | Vertices.Add( vertex ); 287 | VertexPositions.Add( vertex.Position ); 288 | 289 | VertexMap.Add( (key, plane), index ); 290 | 291 | return index; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /Code/facepunch.libpolygon/PolygonMeshBuilder.Fill.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Sandbox.Polygons; 6 | 7 | partial class PolygonMeshBuilder 8 | { 9 | /// 10 | /// Triangulate any remaining active edges so that the generated mesh is closed. 11 | /// 12 | public PolygonMeshBuilder Fill() 13 | { 14 | Validate(); 15 | 16 | Fill_UpdateExistingVertices(); 17 | Fill_SplitIntoMonotonicPolygons(); 18 | Fill_Triangulate(); 19 | 20 | PostBevel(); 21 | 22 | return this; 23 | } 24 | 25 | private enum SweepEvent 26 | { 27 | Start, 28 | End, 29 | Split, 30 | Merge, 31 | Upper, 32 | Lower 33 | } 34 | 35 | private static SweepEvent CategorizeEvent( in Edge prev, in Edge curr, in Edge next ) 36 | { 37 | var prevLeft = Compare( prev.Origin, curr.Origin ) < 0; 38 | var nextLeft = Compare( next.Origin, curr.Origin ) < 0; 39 | 40 | var nextBelow = curr.Tangent.y < -prev.Tangent.y; 41 | 42 | switch (prevLeft, nextLeft, nextBelow) 43 | { 44 | case (false, false, false ): 45 | return SweepEvent.Start; 46 | 47 | case (true, true, true ): 48 | return SweepEvent.End; 49 | 50 | case (false, false, true ): 51 | return SweepEvent.Split; 52 | 53 | case (true, true, false ): 54 | return SweepEvent.Merge; 55 | 56 | case (true, false, _ ): 57 | return SweepEvent.Upper; 58 | 59 | case (false, true, _ ): 60 | return SweepEvent.Lower; 61 | } 62 | } 63 | 64 | [ThreadStatic] 65 | private static List Fill_SortedEdges; 66 | 67 | [ThreadStatic] 68 | private static Dictionary Fill_Helpers; 69 | 70 | [ThreadStatic] 71 | private static List Fill_SweepEdges; 72 | 73 | private readonly struct SweepEdge 74 | { 75 | public int Index { get; } 76 | 77 | public Vector2 Origin { get; } 78 | public float DeltaY { get; } 79 | 80 | public SweepEdge( in Edge edge ) 81 | { 82 | Index = edge.Index; 83 | 84 | Origin = edge.Origin; 85 | DeltaY = Math.Abs( edge.Tangent.x ) <= 0.0001f 86 | ? 0f : edge.Tangent.y / edge.Tangent.x; 87 | } 88 | 89 | public float GetEdgeY( float x ) 90 | { 91 | return Origin.y + DeltaY * (x - Origin.x); 92 | } 93 | } 94 | 95 | private int ConnectTwoWay( ref Edge a, ref Edge b ) 96 | { 97 | ref var prevA = ref _allEdges[a.PrevEdge]; 98 | ref var prevB = ref _allEdges[b.PrevEdge]; 99 | 100 | ref var aNew = ref _allEdges[AddEdge( a.Origin, (b.Origin - a.Origin).Normal, a.Distance )]; 101 | ref var bNew = ref _allEdges[AddEdge( b.Origin, (a.Origin - b.Origin).Normal, b.Distance )]; 102 | 103 | aNew.Vertices = AddVertices( ref a ); 104 | bNew.Vertices = AddVertices( ref b ); 105 | 106 | SimpleConnectEdges( ref prevA, ref aNew ); 107 | SimpleConnectEdges( ref aNew, ref b ); 108 | 109 | SimpleConnectEdges( ref prevB, ref bNew ); 110 | SimpleConnectEdges( ref bNew, ref a ); 111 | 112 | _activeEdges.Add( aNew.Index ); 113 | _activeEdges.Add( bNew.Index ); 114 | 115 | return aNew.Index; 116 | } 117 | 118 | private int FixUp( ref Edge v, in Edge e ) 119 | { 120 | var helperInfo = Fill_Helpers[e.Index]; 121 | 122 | if ( helperInfo.WasMerge ) 123 | { 124 | return ConnectTwoWay( ref v, ref _allEdges[helperInfo.Index] ); 125 | } 126 | 127 | return v.Index; 128 | } 129 | 130 | private void SetHelper( in Edge edge, in Edge helper, bool wasMerge ) 131 | { 132 | Fill_Helpers[edge.Index] = (helper.Index, wasMerge); 133 | } 134 | 135 | private void AddSweepEdge( in Edge edge ) 136 | { 137 | // TODO: could binary search for insertion point 138 | 139 | var origin = edge.Origin; 140 | Fill_SweepEdges.Add( new SweepEdge( edge ) ); 141 | Fill_SweepEdges.Sort( ( a, b ) => 142 | a.GetEdgeY( origin.x ).CompareTo( b.GetEdgeY( origin.x ) ) ); 143 | } 144 | 145 | private void ReplaceSweepEdge( in Edge old, in Edge replacement ) 146 | { 147 | // TODO: could binary search 148 | 149 | for ( var i = 0; i < Fill_SweepEdges.Count; ++i ) 150 | { 151 | if ( Fill_SweepEdges[i].Index == old.Index ) 152 | { 153 | Fill_SweepEdges[i] = new SweepEdge( in replacement ); 154 | break; 155 | } 156 | } 157 | } 158 | 159 | private void RemoveSweepEdge( in Edge edge ) 160 | { 161 | // TODO: could binary search 162 | 163 | for ( var i = 0; i < Fill_SweepEdges.Count; ++i ) 164 | { 165 | if ( Fill_SweepEdges[i].Index == edge.Index ) 166 | { 167 | Fill_SweepEdges.RemoveAt( i ); 168 | break; 169 | } 170 | } 171 | } 172 | 173 | private int FindAboveSweepEdge( in Edge edge ) 174 | { 175 | // TODO: could binary search 176 | 177 | foreach ( var other in Fill_SweepEdges ) 178 | { 179 | if ( edge.PrevEdge == other.Index || edge.Index == other.Index ) 180 | { 181 | continue; 182 | } 183 | 184 | if ( other.GetEdgeY( edge.Origin.x ) - edge.Origin.y >= 0f ) 185 | { 186 | return other.Index; 187 | } 188 | } 189 | 190 | throw new Exception(); 191 | } 192 | 193 | private void Fill_UpdateExistingVertices() 194 | { 195 | _nextAngle = MathF.PI * 0.5f; 196 | _nextDistance = float.PositiveInfinity; 197 | 198 | if ( !SkipNormals && Math.Abs( _prevAngle - _nextAngle ) >= 0.001f ) 199 | { 200 | foreach ( var index in _activeEdges ) 201 | { 202 | ref var edge = ref _allEdges[index]; 203 | edge.Vertices = (-1, -1); 204 | 205 | AddVertices( ref edge, true ); 206 | } 207 | } 208 | 209 | _prevAngle = _nextAngle; 210 | } 211 | 212 | private void Fill_SplitIntoMonotonicPolygons() 213 | { 214 | Fill_SortedEdges ??= new List(); 215 | Fill_SortedEdges.Clear(); 216 | 217 | Fill_SortedEdges.AddRange( _activeEdges ); 218 | 219 | Fill_SortedEdges.Sort( ( a, b ) => Compare( _allEdges[a].Origin, _allEdges[b].Origin ) ); 220 | 221 | Fill_Helpers ??= new Dictionary(); 222 | Fill_Helpers.Clear(); 223 | 224 | Fill_SweepEdges ??= new List(); 225 | Fill_SweepEdges.Clear(); 226 | 227 | // Based on https://www.cs.umd.edu/class/spring2020/cmsc754/Lects/lect05-triangulate.pdf 228 | 229 | // Add pairs of edges to split into x-monotonic polygons 230 | 231 | foreach ( var index in Fill_SortedEdges ) 232 | { 233 | EnsureCapacity( 4 ); 234 | 235 | ref var edge = ref _allEdges[index]; 236 | ref var next = ref _allEdges[edge.NextEdge]; 237 | ref var prev = ref _allEdges[edge.PrevEdge]; 238 | 239 | switch ( CategorizeEvent( in prev, in edge, in next ) ) 240 | { 241 | case SweepEvent.Start: 242 | AddSweepEdge( in edge ); 243 | SetHelper( in edge, in edge, false ); 244 | break; 245 | 246 | case SweepEvent.End: 247 | FixUp( ref edge, in prev ); 248 | RemoveSweepEdge( in prev ); 249 | break; 250 | 251 | case SweepEvent.Split: 252 | { 253 | ref var above = ref _allEdges[FindAboveSweepEdge( in edge )]; 254 | ref var helper = ref _allEdges[Fill_Helpers[above.Index].Index]; 255 | ref var fixedUp = ref _allEdges[ConnectTwoWay( ref edge, ref helper )]; 256 | AddSweepEdge( in edge ); 257 | SetHelper( in above, in fixedUp, false ); 258 | SetHelper( in edge, in edge, false ); 259 | break; 260 | } 261 | 262 | case SweepEvent.Merge: 263 | { 264 | ref var above = ref _allEdges[FindAboveSweepEdge( in edge )]; 265 | RemoveSweepEdge( in prev ); 266 | ref var new1 = ref _allEdges[FixUp( ref edge, in above )]; 267 | FixUp( ref new1, in prev ); 268 | SetHelper( in above, in new1, true ); 269 | break; 270 | } 271 | 272 | case SweepEvent.Upper: 273 | FixUp( ref edge, in prev ); 274 | ReplaceSweepEdge( in prev, in edge ); 275 | SetHelper( in edge, in edge, false ); 276 | break; 277 | 278 | case SweepEvent.Lower: 279 | { 280 | ref var above = ref _allEdges[FindAboveSweepEdge( in edge )]; 281 | ref var helper = ref _allEdges[FixUp( ref edge, in above )]; 282 | SetHelper( in above, in helper, false ); 283 | break; 284 | } 285 | } 286 | } 287 | } 288 | 289 | private readonly struct CloseVertex 290 | { 291 | public Vector2 Position { get; } 292 | 293 | /// 294 | /// Difference to this vertex from the previous one. 295 | /// 296 | public Vector2 Delta { get; } 297 | 298 | public int Vertex { get; } 299 | public bool IsUpper { get; } 300 | 301 | public CloseVertex( Vector2 position, Vector2 delta, int vertex, bool isUpper ) 302 | { 303 | Position = position; 304 | Delta = delta; 305 | Vertex = vertex; 306 | IsUpper = isUpper; 307 | } 308 | } 309 | 310 | [ThreadStatic] 311 | private static List Fill_Vertices; 312 | 313 | [ThreadStatic] 314 | private static Stack Fill_Stack; 315 | 316 | private static bool IsReflex( Vector2 prevDelta, Vector2 nextDelta ) 317 | { 318 | return Vector2.Dot( Helpers.Rotate90( nextDelta ), prevDelta ) >= 0f; 319 | } 320 | 321 | private static int Compare( Vector2 a, Vector2 b ) 322 | { 323 | var xCompare = a.x.CompareTo( b.x ); 324 | if ( xCompare != 0 ) return xCompare; 325 | return a.y.CompareTo( b.y ); 326 | } 327 | 328 | private void Fill_Triangulate() 329 | { 330 | Fill_Vertices ??= new List(); 331 | Fill_Stack ??= new Stack(); 332 | 333 | while ( _activeEdges.Count > 0 ) 334 | { 335 | var firstIndex = _activeEdges.First(); 336 | _activeEdges.Remove( firstIndex ); 337 | 338 | var first = _allEdges[firstIndex]; 339 | 340 | var minPos = first.Origin; 341 | var maxPos = first.Origin; 342 | var minEdgeIndex = firstIndex; 343 | var maxEdgeIndex = firstIndex; 344 | 345 | var edge = first; 346 | 347 | while ( edge.NextEdge != first.Index ) 348 | { 349 | edge = _allEdges[edge.NextEdge]; 350 | _activeEdges.Remove( edge.Index ); 351 | 352 | if ( Compare( edge.Origin, minPos ) < 0 ) 353 | { 354 | minPos = edge.Origin; 355 | minEdgeIndex = edge.Index; 356 | } 357 | 358 | if ( Compare( edge.Origin, maxPos ) > 0 ) 359 | { 360 | maxPos = edge.Origin; 361 | maxEdgeIndex = edge.Index; 362 | } 363 | } 364 | 365 | Fill_Vertices.Clear(); 366 | 367 | edge = _allEdges[minEdgeIndex]; 368 | Fill_Vertices.Add( new CloseVertex( edge.Origin, default, edge.Vertices.Prev, true ) ); 369 | 370 | while ( edge.NextEdge != maxEdgeIndex ) 371 | { 372 | var next = _allEdges[edge.NextEdge]; 373 | Fill_Vertices.Add( new CloseVertex( next.Origin, next.Origin - edge.Origin, next.Vertices.Prev, true ) ); 374 | edge = next; 375 | } 376 | 377 | edge = _allEdges[maxEdgeIndex]; 378 | 379 | while ( edge.Index != minEdgeIndex ) 380 | { 381 | var next = _allEdges[edge.NextEdge]; 382 | Fill_Vertices.Add( new CloseVertex( edge.Origin, edge.Origin - next.Origin, edge.Vertices.Prev, false ) ); 383 | edge = next; 384 | } 385 | 386 | Fill_Vertices.Sort( ( a, b ) => Compare( a.Position, b.Position ) ); 387 | 388 | Fill_Stack.Clear(); 389 | Fill_Stack.Push( Fill_Vertices[0] ); 390 | Fill_Stack.Push( Fill_Vertices[1] ); 391 | 392 | for ( var i = 2; i < Fill_Vertices.Count; ++i ) 393 | { 394 | var next = Fill_Vertices[i]; 395 | var top = Fill_Stack.Peek(); 396 | 397 | if ( top.IsUpper != next.IsUpper ) 398 | { 399 | // Case 1 400 | 401 | while ( Fill_Stack.Count > 1 ) 402 | { 403 | var curr = Fill_Stack.Pop(); 404 | var prev = Fill_Stack.Peek(); 405 | 406 | if ( next.IsUpper ) 407 | { 408 | AddTriangle( next.Vertex, prev.Vertex, curr.Vertex ); 409 | } 410 | else 411 | { 412 | AddTriangle( next.Vertex, curr.Vertex, prev.Vertex ); 413 | } 414 | } 415 | 416 | Fill_Stack.Clear(); 417 | Fill_Stack.Push( top ); 418 | Fill_Stack.Push( new CloseVertex( next.Position, 419 | next.Position - top.Position, 420 | next.Vertex, next.IsUpper ) ); 421 | continue; 422 | } 423 | 424 | while ( Fill_Stack.Count > 1 && IsReflex( top.Delta, next.Position - top.Position ) != top.IsUpper ) 425 | { 426 | var curr = Fill_Stack.Pop(); 427 | top = Fill_Stack.Peek(); 428 | 429 | if ( next.IsUpper ) 430 | { 431 | AddTriangle( next.Vertex, curr.Vertex, top.Vertex ); 432 | } 433 | else 434 | { 435 | AddTriangle( next.Vertex, top.Vertex, curr.Vertex ); 436 | } 437 | } 438 | 439 | Fill_Stack.Push( new CloseVertex( next.Position, 440 | next.Position - top.Position, 441 | next.Vertex, next.IsUpper ) ); 442 | } 443 | } 444 | } 445 | } -------------------------------------------------------------------------------- /Code/2D/Sdf2DMeshWriter.SourceEdges.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Sandbox.Sdf 6 | { 7 | partial class Sdf2DMeshWriter 8 | { 9 | private static float GetAdSubBc( float a, float b, float c, float d ) 10 | { 11 | return (a - 127.5f) * (d - 127.5f) - (b - 127.5f) * (c - 127.5f); 12 | } 13 | 14 | private void AddSourceEdges( int x, int y, int aRaw, int bRaw, int cRaw, int dRaw ) 15 | { 16 | var a = aRaw < 128 ? SquareConfiguration.A : 0; 17 | var b = bRaw < 128 ? SquareConfiguration.B : 0; 18 | var c = cRaw < 128 ? SquareConfiguration.C : 0; 19 | var d = dRaw < 128 ? SquareConfiguration.D : 0; 20 | 21 | var config = a | b | c | d; 22 | 23 | switch ( config ) 24 | { 25 | case SquareConfiguration.None: 26 | break; 27 | 28 | case SquareConfiguration.A: 29 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.AC, SquareVertex.AB ) ); 30 | break; 31 | 32 | case SquareConfiguration.B: 33 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.AB, SquareVertex.BD ) ); 34 | break; 35 | 36 | case SquareConfiguration.C: 37 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.CD, SquareVertex.AC ) ); 38 | break; 39 | 40 | case SquareConfiguration.D: 41 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.BD, SquareVertex.CD ) ); 42 | break; 43 | 44 | 45 | case SquareConfiguration.AB: 46 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.AC, SquareVertex.BD ) ); 47 | break; 48 | 49 | case SquareConfiguration.AC: 50 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.CD, SquareVertex.AB ) ); 51 | break; 52 | 53 | case SquareConfiguration.CD: 54 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.BD, SquareVertex.AC ) ); 55 | break; 56 | 57 | case SquareConfiguration.BD: 58 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.AB, SquareVertex.CD ) ); 59 | break; 60 | 61 | 62 | case SquareConfiguration.AD: 63 | if ( GetAdSubBc( aRaw, bRaw, cRaw, dRaw ) > 0f ) 64 | { 65 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.AC, SquareVertex.CD ) ); 66 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.BD, SquareVertex.AB ) ); 67 | } 68 | else 69 | { 70 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.AC, SquareVertex.AB ) ); 71 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.BD, SquareVertex.CD ) ); 72 | } 73 | 74 | break; 75 | 76 | case SquareConfiguration.BC: 77 | if ( GetAdSubBc( aRaw, bRaw, cRaw, dRaw ) < 0f ) 78 | { 79 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.AB, SquareVertex.AC ) ); 80 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.CD, SquareVertex.BD ) ); 81 | } 82 | else 83 | { 84 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.AB, SquareVertex.BD ) ); 85 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.CD, SquareVertex.AC ) ); 86 | } 87 | 88 | break; 89 | 90 | case SquareConfiguration.ABC: 91 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.CD, SquareVertex.BD ) ); 92 | break; 93 | 94 | case SquareConfiguration.ABD: 95 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.AC, SquareVertex.CD ) ); 96 | break; 97 | 98 | case SquareConfiguration.ACD: 99 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.BD, SquareVertex.AB ) ); 100 | break; 101 | 102 | case SquareConfiguration.BCD: 103 | SourceEdges.Add( new SourceEdge( x, y, SquareVertex.AB, SquareVertex.AC ) ); 104 | break; 105 | 106 | case SquareConfiguration.ABCD: 107 | break; 108 | 109 | default: 110 | throw new NotImplementedException(); 111 | } 112 | } 113 | 114 | private Dictionary VertexMap { get; } = new(); 115 | private HashSet RemainingSourceEdges { get; } = new(); 116 | 117 | private record struct EdgeLoop( int FirstIndex, int Count, float Area, Vector2 Min, Vector2 Max ); 118 | 119 | private List SourceVertices { get; } = new(); 120 | private List EdgeLoops { get; } = new(); 121 | 122 | private static Vector3 GetVertexPos( in Sdf2DArrayData data, VertexKey key ) 123 | { 124 | switch ( key.Vertex ) 125 | { 126 | case NormalizedVertex.A: 127 | return new Vector3( key.X, key.Y ); 128 | 129 | case NormalizedVertex.AB: 130 | { 131 | var a = data[key.X, key.Y] - 127.5f; 132 | var b = data[key.X + 1, key.Y] - 127.5f; 133 | var t = a / (a - b); 134 | return new Vector3( key.X + t, key.Y ); 135 | } 136 | 137 | case NormalizedVertex.AC: 138 | { 139 | var a = data[key.X, key.Y] - 127.5f; 140 | var c = data[key.X, key.Y + 1] - 127.5f; 141 | var t = a / (a - c); 142 | return new Vector3( key.X, key.Y + t ); 143 | } 144 | 145 | default: 146 | throw new NotImplementedException(); 147 | } 148 | } 149 | 150 | private bool Contains( EdgeLoop loop, Vector2 pos ) 151 | { 152 | if ( pos.x < loop.Min.x || pos.x > loop.Max.x ) 153 | { 154 | return loop.Area < 0f; 155 | } 156 | 157 | if ( pos.y < loop.Min.y || pos.y > loop.Max.y ) 158 | { 159 | return loop.Area < 0f; 160 | } 161 | 162 | var v0 = SourceVertices[loop.FirstIndex + loop.Count - 1] - pos; 163 | Vector2 v1; 164 | 165 | var intersections = 0; 166 | 167 | for ( var i = 0; i < loop.Count; ++i, v0 = v1 ) 168 | { 169 | v1 = SourceVertices[loop.FirstIndex + i] - pos; 170 | 171 | if ( v0.y >= 0f == v1.y >= 0f ) 172 | { 173 | continue; 174 | } 175 | 176 | if ( v0.x >= 0f && v1.x >= 0f ) 177 | { 178 | continue; 179 | } 180 | 181 | var t = -v0.y / (v1.y - v0.y); 182 | 183 | if ( t >= 0f && t < 1f ) 184 | { 185 | ++intersections; 186 | } 187 | } 188 | 189 | return (intersections & 1) == 1 == loop.Area > 0f; 190 | } 191 | 192 | private bool Contains( EdgeLoop posLoop, EdgeLoop negLoop ) 193 | { 194 | return posLoop.Area >= -negLoop.Area && Contains( posLoop, SourceVertices[negLoop.FirstIndex] ); 195 | } 196 | 197 | private void FindEdgeLoops( in Sdf2DArrayData data, float maxSmoothAngle, float smoothRadius ) 198 | { 199 | VertexMap.Clear(); 200 | RemainingSourceEdges.Clear(); 201 | 202 | foreach ( var sourceEdge in SourceEdges ) 203 | { 204 | VertexMap[sourceEdge.V0] = (sourceEdge, GetVertexPos( in data, sourceEdge.V0 )); 205 | RemainingSourceEdges.Add( sourceEdge ); 206 | } 207 | 208 | EdgeLoops.Clear(); 209 | SourceVertices.Clear(); 210 | 211 | while ( RemainingSourceEdges.Count > 0 ) 212 | { 213 | var firstIndex = SourceVertices.Count; 214 | var first = RemainingSourceEdges.First(); 215 | 216 | RemainingSourceEdges.Remove( first ); 217 | SourceVertices.Add( VertexMap[first.V0].Position ); 218 | 219 | // Build edge loop 220 | 221 | var count = 1; 222 | var next = first; 223 | 224 | while ( next.V1 != first.V0 ) 225 | { 226 | (next, Vector2 pos) = VertexMap[next.V1]; 227 | 228 | RemainingSourceEdges.Remove( next ); 229 | SourceVertices.Add( pos ); 230 | 231 | ++count; 232 | } 233 | 234 | if ( RemoveIfDegenerate( firstIndex, count ) ) 235 | { 236 | continue; 237 | } 238 | 239 | RemoveCollinearVertices( firstIndex, ref count, data.Size ); 240 | 241 | if ( RemoveIfDegenerate( firstIndex, count ) ) 242 | { 243 | continue; 244 | } 245 | 246 | // AddSmoothingVertices( firstIndex, ref count, maxSmoothAngle, smoothRadius ); 247 | 248 | var area = CalculateArea( firstIndex, count, out var min, out var max ); 249 | 250 | if ( Math.Abs( area ) < 0.00001f ) 251 | { 252 | // Degenerate edge loop 253 | SourceVertices.RemoveRange( firstIndex, count ); 254 | continue; 255 | } 256 | 257 | EdgeLoops.Add( new EdgeLoop( firstIndex, count, area, min, max ) ); 258 | } 259 | 260 | if ( EdgeLoops.Count == 0 ) 261 | { 262 | return; 263 | } 264 | 265 | // TODO: The below wasn't working perfectly, so we just treat everything as one possibly disconnected polygon for now 266 | 267 | return; 268 | 269 | // Sort by area: largest negative first, largest positive last 270 | 271 | EdgeLoops.Sort( ( a, b ) => a.Area.CompareTo( b.Area ) ); 272 | 273 | // Put negative loops after the positive loops that contain them 274 | 275 | while ( EdgeLoops[0].Area < 0 ) 276 | { 277 | var negLoop = EdgeLoops[0]; 278 | EdgeLoops.RemoveAt( 0 ); 279 | 280 | // Find containing positive loop 281 | 282 | for ( var i = 0; i < EdgeLoops.Count; ++i ) 283 | { 284 | var posLoop = EdgeLoops[i]; 285 | 286 | if ( !Contains( posLoop, negLoop ) ) 287 | { 288 | continue; 289 | } 290 | 291 | EdgeLoops.Insert( i + 1, negLoop ); 292 | break; 293 | } 294 | } 295 | } 296 | 297 | private bool RemoveIfDegenerate( int firstIndex, int count ) 298 | { 299 | if ( count >= 3 ) return false; 300 | 301 | SourceVertices.RemoveRange( firstIndex, count ); 302 | return true; 303 | } 304 | 305 | private static bool IsChunkBoundary( Vector2 pos, int chunkSize ) 306 | { 307 | // ReSharper disable once CompareOfFloatsByEqualityOperator 308 | return pos.x == 0f || pos.y == 0f || pos.x == chunkSize || pos.y == chunkSize; 309 | } 310 | 311 | private void RemoveCollinearVertices( int firstIndex, ref int count, int chunkSize ) 312 | { 313 | const float collinearThreshold = 0.999877929688f; 314 | 315 | var v0 = SourceVertices[firstIndex + count - 1]; 316 | var v1 = SourceVertices[firstIndex]; 317 | var e01 = Helpers.NormalizeSafe( v1 - v0 ); 318 | 319 | for ( var i = 0; i < count; ++i ) 320 | { 321 | var v2 = SourceVertices[firstIndex + (i + 1) % count]; 322 | var e12 = (v2 - v1).Normal; 323 | 324 | if ( !IsChunkBoundary( v1, chunkSize ) && Vector2.Dot( e01, e12 ) >= collinearThreshold ) 325 | { 326 | count -= 1; 327 | SourceVertices.RemoveAt( firstIndex + i ); 328 | --i; 329 | 330 | v1 = v2; 331 | e01 = Helpers.NormalizeSafe( v1 - v0 ); 332 | continue; 333 | } 334 | 335 | v0 = v1; 336 | v1 = v2; 337 | e01 = e12; 338 | } 339 | } 340 | 341 | private void AddSmoothingVertices( int firstIndex, ref int count, float maxSmoothAngle, float smoothRadius ) 342 | { 343 | if ( maxSmoothAngle <= 0.0001f || smoothRadius <= 0.0001f ) 344 | { 345 | return; 346 | } 347 | 348 | var minSmoothNormalDot = MathF.Cos( maxSmoothAngle * MathF.PI / 180f ); 349 | 350 | var v3 = SourceVertices[firstIndex + 1]; 351 | var v2 = SourceVertices[firstIndex + 0]; 352 | var v1 = SourceVertices[firstIndex + count - 1]; 353 | 354 | var lastVertex = v1; 355 | 356 | var e23 = Helpers.NormalizeSafe( v3 - v2 ); 357 | var e12 = Helpers.NormalizeSafe( v2 - v1 ); 358 | 359 | var nextSmoothed = Vector2.Dot( e12, e23 ) >= minSmoothNormalDot; 360 | 361 | for ( var i = count - 1; i >= 0; --i ) 362 | { 363 | var v0 = i == 0 ? lastVertex : SourceVertices[firstIndex + i - 1]; 364 | var e01 = Helpers.NormalizeSafe( v1 - v0 ); 365 | 366 | var prevSmoothed = Vector2.Dot( e01, e12 ) >= minSmoothNormalDot; 367 | 368 | if ( prevSmoothed || nextSmoothed ) 369 | { 370 | var dist = (v2 - v1).Length; 371 | 372 | if ( dist >= 2.5f * smoothRadius || dist >= 1.5f * smoothRadius && !(prevSmoothed && nextSmoothed) ) 373 | { 374 | if ( nextSmoothed ) 375 | { 376 | SourceVertices.Insert( firstIndex + i + 1, v2 - e12 * dist ); 377 | count++; 378 | } 379 | 380 | if ( prevSmoothed ) 381 | { 382 | SourceVertices.Insert( firstIndex + i + 1, v1 + e12 * dist ); 383 | count++; 384 | } 385 | } 386 | } 387 | 388 | v2 = v1; 389 | v1 = v0; 390 | 391 | e12 = e01; 392 | 393 | nextSmoothed = prevSmoothed; 394 | } 395 | } 396 | 397 | private float CalculateArea( int firstIndex, int count, out Vector2 min, out Vector2 max ) 398 | { 399 | var v0 = SourceVertices[firstIndex]; 400 | var v1 = SourceVertices[firstIndex + 1]; 401 | var e01 = v1 - v0; 402 | 403 | var area = 0f; 404 | 405 | min = Vector2.Min( v0, v1 ); 406 | max = Vector2.Max( v0, v1 ); 407 | 408 | for ( var i = 2; i < count; ++i ) 409 | { 410 | var v2 = SourceVertices[firstIndex + i]; 411 | var e12 = v2 - v1; 412 | 413 | area += e01.y * e12.x - e01.x * e12.y; 414 | 415 | min = Vector2.Min( min, v2 ); 416 | max = Vector2.Max( max, v2 ); 417 | 418 | v1 = v2; 419 | e01 = v1 - v0; 420 | } 421 | 422 | return area * 0.5f; 423 | } 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /Code/SdfChunk.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | namespace Sandbox.Sdf; 6 | 7 | internal static class Static 8 | { 9 | private static Texture _sWhite3D; 10 | 11 | public static Texture White3D => _sWhite3D ??= new Texture3DBuilder() 12 | .WithName( "White 3D" ) 13 | .WithSize( 1, 1, 1 ) 14 | .WithFormat( ImageFormat.I8 ) 15 | .WithData( new byte[] { 255 } ) 16 | .Finish(); 17 | 18 | private const int MaxPooledMeshes = 256; 19 | 20 | private static List _sMeshPool { get; } = new(); 21 | 22 | public static Mesh RentMesh( Material mat ) 23 | { 24 | if ( _sMeshPool.Count == 0 ) 25 | { 26 | return new Mesh( mat ); 27 | } 28 | 29 | var last = _sMeshPool[^1]; 30 | _sMeshPool.RemoveAt( _sMeshPool.Count - 1 ); 31 | 32 | last.Material = mat; 33 | 34 | return last; 35 | } 36 | 37 | public static void ReturnMesh( Mesh mesh ) 38 | { 39 | if ( _sMeshPool.Count >= MaxPooledMeshes ) 40 | { 41 | return; 42 | } 43 | 44 | _sMeshPool.Add( mesh ); 45 | } 46 | } 47 | 48 | public interface IMeshWriter 49 | { 50 | bool IsEmpty { get; } 51 | void ApplyTo( Mesh mesh ); 52 | } 53 | 54 | public record struct MeshDescription( IMeshWriter Writer, Material Material ); 55 | 56 | /// 57 | /// Base class for chunks in a . 58 | /// Each chunk contains an SDF for a sub-region of one specific volume / layer resource. 59 | /// 60 | /// Non-abstract world type 61 | /// Non-abstract chunk type 62 | /// Volume / layer resource 63 | /// Integer coordinates used to index a chunk 64 | /// Type of used to contain samples 65 | /// Interface for SDF shapes used to make modifications 66 | public abstract partial class SdfChunk : Component, Component.ExecuteInEditor 67 | where TWorld : SdfWorld 68 | where TChunk : SdfChunk, new() 69 | where TResource : SdfResource 70 | where TChunkKey : struct 71 | where TArray : SdfArray, new() 72 | where TSdf : ISdf 73 | { 74 | /// 75 | /// Array storing SDF samples for this chunk. 76 | /// 77 | protected TArray Data { get; private set; } 78 | 79 | /// 80 | /// World that owns this chunk. 81 | /// 82 | public TWorld World { get; private set; } 83 | 84 | /// 85 | /// Volume or layer resource controlling the rendering and collision of this chunk. 86 | /// 87 | public TResource Resource { get; private set; } 88 | 89 | /// 90 | /// Position index of this chunk in the world. 91 | /// 92 | public TChunkKey Key { get; private set; } 93 | 94 | /// 95 | /// If this chunk has collision, the generated physics mesh for this chunk. 96 | /// 97 | public PhysicsShape Shape { get; set; } 98 | 99 | /// 100 | /// If this chunk is rendered, the scene object containing the generated mesh. 101 | /// 102 | public SceneObject Renderer { get; private set; } 103 | 104 | private float _opacity = 1f; 105 | 106 | public float Opacity 107 | { 108 | get => _opacity; 109 | set 110 | { 111 | _opacity = value; 112 | UpdateOpacity(); 113 | } 114 | } 115 | 116 | public abstract Vector3 ChunkPosition { get; } 117 | 118 | private readonly List _usedMeshes = new(); 119 | 120 | internal void Init( TWorld world, TResource resource, TChunkKey key ) 121 | { 122 | World = world; 123 | Resource = resource; 124 | Key = key; 125 | 126 | Opacity = world.Opacity; 127 | 128 | Flags |= ComponentFlags.Hidden | ComponentFlags.NotNetworked | ComponentFlags.NotSaved; 129 | 130 | Data = new TArray(); 131 | Data.Init( resource.Quality ); 132 | 133 | OnInit(); 134 | } 135 | 136 | /// 137 | /// Called after the chunk is added to the . 138 | /// 139 | protected virtual void OnInit() 140 | { 141 | 142 | } 143 | 144 | /// 145 | /// Sets every sample in this chunk's SDF to solid or empty. 146 | /// 147 | /// Solidity to set each sample to. 148 | public Task ClearAsync( bool solid ) 149 | { 150 | Data.Clear( solid ); 151 | return GameTask.CompletedTask; 152 | } 153 | 154 | /// 155 | /// Add a world-space shape to this chunk. 156 | /// 157 | /// SDF type 158 | /// Shape to add 159 | /// True if any geometry was modified 160 | public Task AddAsync( T sdf ) 161 | where T : TSdf 162 | { 163 | return OnAddAsync( sdf ); 164 | } 165 | 166 | /// 167 | /// Subtract a world-space shape from this chunk. 168 | /// 169 | /// SDF type 170 | /// Shape to subtract 171 | /// True if any geometry was modified 172 | public Task SubtractAsync( T sdf ) 173 | where T : TSdf 174 | { 175 | return OnSubtractAsync( sdf ); 176 | } 177 | 178 | public Task RebuildAsync( IEnumerable> modifications ) 179 | { 180 | return OnRebuildAsync( modifications ); 181 | } 182 | 183 | protected abstract Task OnAddAsync( T sdf ) 184 | where T : TSdf; 185 | protected abstract Task OnSubtractAsync( T sdf ) 186 | where T : TSdf; 187 | protected abstract Task OnRebuildAsync( IEnumerable> modifications ); 188 | 189 | internal async Task UpdateMesh() 190 | { 191 | await OnUpdateMeshAsync(); 192 | 193 | if ( Renderer == null || Resource.ReferencedTextures is not { Count: > 0 } ) return; 194 | 195 | await GameTask.MainThread(); 196 | 197 | foreach ( var reference in Resource.ReferencedTextures ) 198 | { 199 | if ( reference.Source is null ) 200 | { 201 | continue; 202 | } 203 | 204 | var matching = World.GetChunk( reference.Source, Key ); 205 | UpdateLayerTexture( reference.TargetAttribute, reference.Source, matching ); 206 | } 207 | } 208 | 209 | internal void UpdateLayerTexture( TResource resource, TChunk source ) 210 | { 211 | if ( Renderer == null || Resource.ReferencedTextures is not { Count: > 0 } ) return; 212 | 213 | foreach ( var reference in Resource.ReferencedTextures ) 214 | { 215 | if ( reference.Source != resource ) continue; 216 | UpdateLayerTexture( reference.TargetAttribute, reference.Source, source ); 217 | } 218 | } 219 | 220 | internal void UpdateLayerTexture( string targetAttribute, TResource resource, TChunk source ) 221 | { 222 | ThreadSafe.AssertIsMainThread(); 223 | 224 | if ( source != null ) 225 | { 226 | if ( resource != source.Resource ) 227 | { 228 | Log.Warning( $"Source chunk is using the wrong layer or volume resource" ); 229 | return; 230 | } 231 | 232 | // ReSharper disable once CompareOfFloatsByEqualityOperator 233 | if ( resource.Quality.ChunkSize != Resource.Quality.ChunkSize ) 234 | { 235 | Log.Warning( $"Layer {Resource.ResourceName} references {resource.ResourceName} " + 236 | $"as a texture source, but their chunk sizes don't match" ); 237 | return; 238 | } 239 | 240 | Renderer.Attributes.Set( targetAttribute, source.Data.Texture ); 241 | } 242 | else 243 | { 244 | Renderer.Attributes.Set( targetAttribute, Data.Dimensions == 3 ? Static.White3D : Texture.White ); 245 | } 246 | 247 | var quality = resource.Quality; 248 | var arraySize = quality.ChunkResolution + SdfArray.Margin * 2 + 1; 249 | 250 | var margin = (SdfArray.Margin + 0.5f) / arraySize; 251 | var scale = 1f / quality.ChunkSize; 252 | var size = 1f - (SdfArray.Margin * 2 + 1f) / arraySize; 253 | 254 | var texParams = new Vector4( margin, margin, scale * size, quality.MaxDistance * 2f ); 255 | 256 | Renderer.Attributes.Set( $"{targetAttribute}_Params", texParams ); 257 | } 258 | 259 | /// 260 | /// Implements updating the render / collision meshes of this chunk. 261 | /// 262 | /// Task that completes when the meshes have finished updating. 263 | protected abstract Task OnUpdateMeshAsync(); 264 | 265 | /// 266 | /// Asynchronously updates the collision shape to the defined mesh. 267 | /// 268 | /// Collision mesh vertices 269 | /// Collision mesh indices 270 | protected async Task UpdateCollisionMeshAsync( List vertices, List indices ) 271 | { 272 | await GameTask.MainThread(); 273 | 274 | if ( !IsValid ) return; 275 | 276 | UpdateCollisionMesh( vertices, indices ); 277 | } 278 | 279 | protected async Task UpdateRenderMeshesAsync( params MeshDescription[] meshes ) 280 | { 281 | await GameTask.MainThread(); 282 | 283 | if ( !IsValid ) return; 284 | 285 | UpdateRenderMeshes( meshes ); 286 | } 287 | 288 | /// 289 | /// Updates the collision shape to the defined mesh. Must be called on the main thread. 290 | /// 291 | /// Collision mesh vertices 292 | /// Collision mesh indices 293 | protected void UpdateCollisionMesh( List vertices, List indices ) 294 | { 295 | if ( !World.HasPhysics ) 296 | { 297 | return; 298 | } 299 | 300 | ThreadSafe.AssertIsMainThread(); 301 | 302 | if ( indices.Count == 0 ) 303 | { 304 | Shape?.Remove(); 305 | Shape = null; 306 | } 307 | else 308 | { 309 | var tags = Resource.SplitCollisionTags; 310 | 311 | if ( !Shape.IsValid() ) 312 | { 313 | Shape = World.AddMeshShape( vertices, indices ); 314 | 315 | foreach ( var tag in tags ) Shape.Tags.Add( tag ); 316 | } 317 | else 318 | { 319 | Shape.UpdateMesh( vertices, indices ); 320 | } 321 | 322 | Shape.EnableSolidCollisions = Active; 323 | } 324 | } 325 | 326 | /// 327 | /// Updates this chunk's model to use the given set of meshes. Must be called on the main thread. 328 | /// 329 | /// Set of meshes this model should use 330 | private void UpdateRenderMeshes( params MeshDescription[] meshes ) 331 | { 332 | meshes = meshes.Where( x => x.Material != null && !x.Writer.IsEmpty ).ToArray(); 333 | 334 | ThreadSafe.AssertIsMainThread(); 335 | 336 | var meshCountChanged = meshes.Length != _usedMeshes.Count; 337 | 338 | if ( meshCountChanged ) 339 | { 340 | foreach ( var mesh in _usedMeshes ) 341 | { 342 | Static.ReturnMesh( mesh ); 343 | } 344 | 345 | _usedMeshes.Clear(); 346 | 347 | foreach ( var mesh in meshes ) 348 | { 349 | _usedMeshes.Add( Static.RentMesh( mesh.Material ) ); 350 | } 351 | } 352 | else 353 | { 354 | for ( var i = 0; i < meshes.Length; ++i ) 355 | { 356 | _usedMeshes[i].Material = meshes[i].Material; 357 | } 358 | } 359 | 360 | for ( var i = 0; i < meshes.Length; ++i ) 361 | { 362 | meshes[i].Writer.ApplyTo( _usedMeshes[i] ); 363 | } 364 | 365 | if ( !meshCountChanged ) 366 | { 367 | return; 368 | } 369 | 370 | if ( _usedMeshes.Count == 0 ) 371 | { 372 | Renderer?.Delete(); 373 | Renderer = null; 374 | return; 375 | } 376 | 377 | var model = new ModelBuilder() 378 | .AddMeshes( _usedMeshes.ToArray() ) 379 | .Create(); 380 | 381 | if ( !Renderer.IsValid() ) 382 | { 383 | Renderer = new SceneObject( Scene.SceneWorld, model ) 384 | { 385 | Batchable = Resource.ReferencedTextures is not { Count: > 0 } 386 | }; 387 | 388 | foreach ( var tag in World.Tags ) 389 | { 390 | Renderer.Tags.Add( tag ); 391 | } 392 | } 393 | 394 | Renderer.Model = model; 395 | 396 | UpdateTransform(); 397 | UpdateOpacity(); 398 | } 399 | 400 | internal void UpdateTransform() 401 | { 402 | if ( !Renderer.IsValid() ) 403 | return; 404 | 405 | Renderer.Transform = World.Transform.World; 406 | Renderer.Position = World.Transform.World.PointToWorld( ChunkPosition ); 407 | } 408 | 409 | private void UpdateOpacity() 410 | { 411 | if ( Renderer is not { } renderer ) return; 412 | 413 | var value = Opacity; 414 | 415 | renderer.ColorTint = Color.White.WithAlpha( value ); 416 | renderer.RenderingEnabled = value > 0f; 417 | Renderer.Flags.CastShadows = value >= 1f; 418 | } 419 | 420 | protected override void OnEnabled() 421 | { 422 | if ( Shape is { } shape ) 423 | { 424 | shape.EnableSolidCollisions = true; 425 | } 426 | 427 | UpdateTransform(); 428 | UpdateOpacity(); 429 | } 430 | 431 | protected override void OnDisabled() 432 | { 433 | if ( Renderer is { } renderer ) 434 | { 435 | renderer.RenderingEnabled = false; 436 | } 437 | 438 | if ( Shape is { } shape ) 439 | { 440 | shape.EnableSolidCollisions = false; 441 | } 442 | } 443 | 444 | protected override void OnDestroy() 445 | { 446 | Data.Dispose(); 447 | 448 | Renderer?.Delete(); 449 | Renderer = null; 450 | 451 | Shape?.Remove(); 452 | Shape = null; 453 | 454 | foreach ( var usedMesh in _usedMeshes ) 455 | { 456 | Static.ReturnMesh( usedMesh ); 457 | } 458 | 459 | _usedMeshes.Clear(); 460 | 461 | base.OnDestroy(); 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /Code/facepunch.libpolygon/PolygonMeshBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Sandbox.Sdf; 5 | 6 | namespace Sandbox.Polygons; 7 | 8 | /// 9 | /// Helper class for building 3D meshes based on a 2D polygon. Supports 10 | /// concave polygons with holes, although edges must not intersect. 11 | /// 12 | public partial class PolygonMeshBuilder : Pooled 13 | { 14 | public record struct Vertex( Vector3 Position, Vector3 Normal, Vector4 Tangent ) 15 | { 16 | public static VertexAttribute[] Layout { get; } = new[] 17 | { 18 | new VertexAttribute( VertexAttributeType.Position, VertexAttributeFormat.Float32 ), 19 | new VertexAttribute( VertexAttributeType.Normal, VertexAttributeFormat.Float32 ), 20 | new VertexAttribute( VertexAttributeType.Tangent, VertexAttributeFormat.Float32, 4 ) 21 | }; 22 | } 23 | 24 | private int _nextEdgeIndex; 25 | private Edge[] _allEdges = new Edge[64]; 26 | private readonly HashSet _activeEdges = new (); 27 | 28 | private readonly List _vertices = new (); 29 | private readonly List _indices = new (); 30 | 31 | private float _prevDistance; 32 | private float _nextDistance; 33 | 34 | private float _invDistance; 35 | 36 | private float _prevHeight; 37 | private float _nextHeight; 38 | 39 | private float _prevAngle; 40 | private float _nextAngle; 41 | 42 | private float _minSmoothNormalDot; 43 | 44 | private bool _validated; 45 | 46 | /// 47 | /// Number of edges that will be affected by calls to methods like , , and . 48 | /// 49 | public int ActiveEdgeCount => _activeEdges.Count; 50 | 51 | /// 52 | /// If true, no active edges remain because the mesh is fully closed. 53 | /// 54 | public bool IsClosed => _activeEdges.Count == 0; 55 | 56 | /// 57 | /// Corners of the original polygon with an interior or exterior 58 | /// angle less than this (in radians) will have smooth normals. 59 | /// 60 | public float MaxSmoothAngle { get; set; } = 0f; 61 | 62 | /// 63 | /// If true, don't bother generating normals / tangents. 64 | /// 65 | public bool SkipNormals { get; set; } 66 | 67 | /// 68 | /// Positions of each vertex in the generated mesh. 69 | /// 70 | public IEnumerable Positions => _vertices.Select( x => x.Position ); 71 | 72 | /// 73 | /// Normals of each vertex in the generated mesh. 74 | /// 75 | public IEnumerable Normals => _vertices.Select( x => x.Normal ); 76 | 77 | /// 78 | /// U-tangents, and the signs of the V-tangents, of each vertex in the generated mesh. 79 | /// 80 | public IEnumerable Tangents => _vertices.Select( x => x.Tangent ); 81 | 82 | /// 83 | /// Positions, normals, and tangents of each vertex. 84 | /// 85 | public List Vertices => _vertices; 86 | 87 | /// 88 | /// Indices of vertices describing the triangulation of the generated mesh. 89 | /// 90 | public List Indices => _indices; 91 | 92 | /// 93 | /// Clear all geometry from this builder. 94 | /// 95 | public PolygonMeshBuilder Clear() 96 | { 97 | _nextEdgeIndex = 0; 98 | _activeEdges.Clear(); 99 | 100 | _vertices.Clear(); 101 | _indices.Clear(); 102 | 103 | _prevDistance = 0f; 104 | _nextDistance = 0f; 105 | 106 | _invDistance = 0f; 107 | 108 | _prevHeight = 0f; 109 | _nextHeight = 0f; 110 | 111 | _prevAngle = 0f; 112 | _nextAngle = 0f; 113 | 114 | _minSmoothNormalDot = 0f; 115 | 116 | _validated = true; 117 | 118 | return this; 119 | } 120 | 121 | /// 122 | /// Reset this builder to be like a new instance. 123 | /// 124 | public override void Reset() 125 | { 126 | Clear(); 127 | 128 | MaxSmoothAngle = 0f; 129 | SkipNormals = false; 130 | } 131 | 132 | private static int NextPowerOfTwo( int value ) 133 | { 134 | var po2 = 1; 135 | while ( po2 < value ) 136 | { 137 | po2 <<= 1; 138 | } 139 | 140 | return po2; 141 | } 142 | 143 | private void EnsureCapacity( int toAdd ) 144 | { 145 | if ( _nextEdgeIndex + toAdd > _allEdges.Length ) 146 | { 147 | Array.Resize( ref _allEdges, NextPowerOfTwo( _nextEdgeIndex + toAdd ) ); 148 | } 149 | } 150 | 151 | private int AddEdge( Vector2 origin, Vector2 tangent, float distance, int? twinOffset = null ) 152 | { 153 | var edge = new Edge( _nextEdgeIndex, origin, tangent, distance, twinOffset != null ? _nextEdgeIndex + twinOffset.Value : -1 ); 154 | _allEdges[edge.Index] = edge; 155 | ++_nextEdgeIndex; 156 | return edge.Index; 157 | } 158 | 159 | private void Invalidate() 160 | { 161 | _validated = false; 162 | } 163 | 164 | /// 165 | /// Add a set of active edges forming a loop. Clockwise loops will be a solid polygon, and count-clockwise 166 | /// will form a hole. Holes must be inside of solid polygons, otherwise the mesh can't be closed correctly. 167 | /// 168 | /// List of vertices to read a range from. 169 | /// Index of the first vertex in the loop. 170 | /// Number of vertices in the loop. 171 | /// If true, reverse the order of the vertices in the loop. 172 | public PolygonMeshBuilder AddEdgeLoop( IReadOnlyList vertices, int offset, int count, bool reverse = false ) 173 | { 174 | return AddEdgeLoop( vertices, offset, count, Vector2.Zero, Vector2.One, reverse ); 175 | } 176 | 177 | public PolygonMeshBuilder AddEdgeLoop( IReadOnlyList vertices, int offset, int count, Vector2 position, Vector2 scale, bool reverse = false ) 178 | { 179 | var firstIndex = _nextEdgeIndex; 180 | 181 | EnsureCapacity( count ); 182 | Invalidate(); 183 | 184 | var prevVertex = position + vertices[offset + count - 1] * scale; 185 | for ( var i = 0; i < count; ++i ) 186 | { 187 | var nextVertex = position + vertices[offset + i] * scale; 188 | 189 | _activeEdges.Add( AddEdge( prevVertex, Helpers.NormalizeSafe( nextVertex - prevVertex ), _prevDistance ) ); 190 | 191 | prevVertex = nextVertex; 192 | } 193 | 194 | var prevIndex = count - 1; 195 | for ( var i = 0; i < count; ++i ) 196 | { 197 | ref var prevEdge = ref _allEdges[firstIndex + prevIndex]; 198 | ref var nextEdge = ref _allEdges[firstIndex + i]; 199 | 200 | if ( reverse ) 201 | { 202 | ConnectEdges( ref nextEdge, ref prevEdge ); 203 | } 204 | else 205 | { 206 | ConnectEdges( ref prevEdge, ref nextEdge ); 207 | } 208 | 209 | prevIndex = i; 210 | } 211 | 212 | return this; 213 | } 214 | 215 | [ThreadStatic] 216 | private static Dictionary AddEdges_VertexMap; 217 | 218 | /// 219 | /// Add a raw set of edges. Be careful to ensure that each loop of edges is fully closed. 220 | /// 221 | /// Positions of vertices to connect with edges. 222 | /// Indices of the start and end vertices of each edge. 223 | public void AddEdges( IReadOnlyList vertices, IReadOnlyList<(int Prev, int Next)> edges ) 224 | { 225 | AddEdges_VertexMap ??= new Dictionary(); 226 | AddEdges_VertexMap.Clear(); 227 | 228 | EnsureCapacity( edges.Count ); 229 | Invalidate(); 230 | 231 | foreach ( var (i, j) in edges ) 232 | { 233 | var prev = vertices[i]; 234 | var next = vertices[j]; 235 | 236 | var index = AddEdge( prev, Helpers.NormalizeSafe( next - prev ), _prevDistance ); 237 | 238 | _activeEdges.Add( index ); 239 | AddEdges_VertexMap.Add( i, index ); 240 | } 241 | 242 | for ( var i = 0; i < edges.Count; ++i ) 243 | { 244 | var edge = edges[i]; 245 | 246 | ref var prev = ref _allEdges[AddEdges_VertexMap[edge.Prev]]; 247 | ref var next = ref _allEdges[AddEdges_VertexMap[edge.Next]]; 248 | 249 | ConnectEdges( ref prev, ref next ); 250 | } 251 | } 252 | 253 | private static float LerpRadians( float a, float b, float t ) 254 | { 255 | var delta = b - a; 256 | delta -= MathF.Floor( delta * (0.5f / MathF.PI) ) * MathF.PI * 2f; 257 | 258 | if ( delta > MathF.PI ) 259 | { 260 | delta -= MathF.PI * 2f; 261 | } 262 | 263 | return a + delta * Math.Clamp( t, 0f, 1f ); 264 | } 265 | 266 | private Vector4 GetTangent( Vector3 normal ) 267 | { 268 | var tangent = Vector3.Cross( normal, new Vector3( 0f, 0f, 1f ) ).Normal; 269 | 270 | return new Vector4( tangent, 1f ); 271 | } 272 | 273 | private (int Prev, int Next) AddVertices( ref Edge edge, bool forceMaxDistance = false ) 274 | { 275 | if ( edge.Vertices.Prev > -1 ) 276 | { 277 | return edge.Vertices; 278 | } 279 | 280 | var prevEdge = _allEdges[edge.PrevEdge]; 281 | 282 | var index = _vertices.Count; 283 | var prevNormal = -prevEdge.Normal; 284 | var nextNormal = -edge.Normal; 285 | 286 | var t = forceMaxDistance ? 1f : (edge.Distance - _prevDistance) * _invDistance; 287 | var height = _prevHeight + t * (_nextHeight - _prevHeight); 288 | 289 | var pos = new Vector3( edge.Origin.x, edge.Origin.y, height ); 290 | 291 | if ( SkipNormals || MathF.Abs( _nextHeight - _prevHeight ) <= 0.001f ) 292 | { 293 | _vertices.Add( new( 294 | pos, 295 | new Vector3( 0f, 0f, 1f ), 296 | new Vector4( 1f, 0f, 0f, 1f ) ) ); 297 | 298 | edge.Vertices = (index, index); 299 | } 300 | else 301 | { 302 | var angle = LerpRadians( _prevAngle, _nextAngle, t ); 303 | var cos = MathF.Cos( angle ); 304 | var sin = MathF.Sin( angle ); 305 | 306 | if ( Vector2.Dot( prevNormal, nextNormal ) >= _minSmoothNormalDot ) 307 | { 308 | var normal = new Vector3( (prevNormal.x + nextNormal.x) * cos, (prevNormal.y + nextNormal.y) * cos, sin * 2f ).Normal; 309 | 310 | _vertices.Add( new( pos, normal, GetTangent( normal ) ) ); 311 | 312 | edge.Vertices = (index, index); 313 | } 314 | else 315 | { 316 | var normal0 = new Vector3( prevNormal.x * cos, prevNormal.y * cos, sin ).Normal; 317 | var normal1 = new Vector3( nextNormal.x * cos, nextNormal.y * cos, sin ).Normal; 318 | 319 | _vertices.Add( new( pos, normal0, GetTangent( normal0 ) ) ); 320 | _vertices.Add( new( pos, normal1, GetTangent( normal1 ) ) ); 321 | 322 | edge.Vertices = (index, index + 1); 323 | } 324 | } 325 | 326 | return edge.Vertices; 327 | } 328 | 329 | private void AddTriangle( int a, int b, int c ) 330 | { 331 | _indices.Add( a ); 332 | _indices.Add( b ); 333 | _indices.Add( c ); 334 | } 335 | 336 | /// 337 | /// Add faces on each active edge extending upwards by the given height. 338 | /// 339 | /// Total distance upwards, away from the plane of the polygon. 340 | public PolygonMeshBuilder Extrude( float height ) 341 | { 342 | return Bevel( 0f, height ); 343 | } 344 | 345 | /// 346 | /// Add faces on each active edge extending inwards by the given width. This will close the mesh if is large enough. 347 | /// 348 | /// Total distance inwards. 349 | public PolygonMeshBuilder Inset( float width ) 350 | { 351 | return Bevel( width, 0f ); 352 | } 353 | 354 | [ThreadStatic] 355 | private static Dictionary Mirror_IndexMap; 356 | 357 | /// 358 | /// Mirrors all previously created faces. The mirror plane is normal to the Z axis, with a given distance from the origin. 359 | /// 360 | /// Distance of the mirror plane from the origin. 361 | public PolygonMeshBuilder Mirror( float z = 0f ) 362 | { 363 | Mirror_IndexMap ??= new Dictionary(); 364 | Mirror_IndexMap.Clear(); 365 | 366 | _vertices.EnsureCapacity( _vertices.Count * 2 ); 367 | _indices.EnsureCapacity( _indices.Count * 2 ); 368 | 369 | var indexCount = _indices.Count; 370 | var vertexCount = _vertices.Count; 371 | 372 | for ( var i = 0; i < vertexCount; i++ ) 373 | { 374 | var vertex = _vertices[i]; 375 | var position = vertex.Position; 376 | var normal = vertex.Normal; 377 | var tangent = vertex.Tangent; 378 | 379 | if ( Math.Abs( position.z - z ) <= 0.001f && (SkipNormals || Math.Abs( normal.z ) <= 0.0001f && Math.Abs( tangent.z ) <= 0.0001f) ) 380 | { 381 | Mirror_IndexMap.Add( i, i ); 382 | } 383 | else 384 | { 385 | Mirror_IndexMap.Add( i, _vertices.Count ); 386 | 387 | _vertices.Add( new( 388 | new Vector3( position.x, position.y, z * 2f - position.z ), 389 | new Vector3( normal.x, normal.y, -normal.z ), 390 | new Vector4( tangent.x, tangent.y, -tangent.z, tangent.w ) ) ); 391 | } 392 | } 393 | 394 | for ( var i = 0; i < indexCount; i += 3 ) 395 | { 396 | var a = Mirror_IndexMap[_indices[i + 0]]; 397 | var b = Mirror_IndexMap[_indices[i + 1]]; 398 | var c = Mirror_IndexMap[_indices[i + 2]]; 399 | 400 | _indices.Add( a ); 401 | _indices.Add( c ); 402 | _indices.Add( b ); 403 | } 404 | 405 | return this; 406 | } 407 | 408 | /// 409 | /// Perform successive s so that the edge of the polygon curves inwards in a quarter circle arc. 410 | /// 411 | /// Radius of the arc. 412 | /// How many bevels to split the rounded edge into. 413 | /// If true, use smooth normals rather than flat shading. 414 | /// If true, the faces will be pointing outwards from the center of the arc. 415 | public PolygonMeshBuilder Arc( float radius, int faces, bool smooth = true, bool convex = true ) 416 | { 417 | return Arc( radius, radius, faces, smooth, convex ); 418 | } 419 | 420 | /// 421 | /// Perform successive s so that the edge of the polygon curves inwards in a quarter circle arc. 422 | /// 423 | /// Total distance inwards. 424 | /// Total distance upwards, away from the plane of the polygon. 425 | /// How many bevels to split the rounded edge into. 426 | /// If true, use smooth normals rather than flat shading. 427 | /// If true, the faces will be pointing outwards from the center of the arc. 428 | public PolygonMeshBuilder Arc( float width, float height, int faces, bool smooth = true, bool convex = true ) 429 | { 430 | var prevWidth = 0f; 431 | var prevHeight = 0f; 432 | var prevTheta = 0f; 433 | 434 | static float MapAngle( float theta, bool convex, bool positive ) 435 | { 436 | var min = positive ? 0f : MathF.PI * 0.5f; 437 | return convex ? min + theta : min + MathF.PI * 0.5f - theta; 438 | } 439 | 440 | for ( var i = 0; i < faces; ++i ) 441 | { 442 | var theta = MathF.PI * 0.5f * (i + 1f) / faces; 443 | 444 | var cos = MathF.Cos( theta ); 445 | var sin = MathF.Sin( theta ); 446 | 447 | var nextWidth = 1f - cos; 448 | var nextHeight = sin; 449 | 450 | if ( smooth ) 451 | { 452 | if ( height >= 0f == convex ) 453 | { 454 | Bevel( (nextWidth - prevWidth) * width, 455 | (nextHeight - prevHeight) * height, 456 | MapAngle( prevTheta, convex, height >= 0f ), 457 | MapAngle( theta, convex, height >= 0f ) ); 458 | } 459 | else 460 | { 461 | Bevel( (nextHeight - prevHeight) * width, 462 | (nextWidth - prevWidth) * height, 463 | MapAngle( prevTheta, convex, height >= 0f ), 464 | MapAngle( theta, convex, height >= 0f ) ); 465 | } 466 | } 467 | else 468 | { 469 | if ( height >= 0f == convex ) 470 | { 471 | Bevel( (nextWidth - prevWidth) * width, 472 | (nextHeight - prevHeight) * height ); 473 | } 474 | else 475 | { 476 | Bevel( (nextHeight - prevHeight) * width, 477 | (nextWidth - prevWidth) * height ); 478 | } 479 | } 480 | 481 | prevWidth = nextWidth; 482 | prevHeight = nextHeight; 483 | prevTheta = theta; 484 | } 485 | 486 | return this; 487 | } 488 | 489 | public void DrawGizmos( float minDist, float maxDist ) 490 | { 491 | foreach ( var index in _activeEdges ) 492 | { 493 | var edge = _allEdges[index]; 494 | var next = _allEdges[edge.NextEdge]; 495 | 496 | var start = edge.Project( minDist ); 497 | var end = next.Project( minDist ); 498 | 499 | Gizmo.Draw.Line( start, end ); 500 | 501 | var dist = (Gizmo.LocalCameraTransform.Position - (Vector3)start).Length; 502 | var textOffset = dist / 64f; 503 | var textPos = start + (end - start).Normal * textOffset - edge.Normal * textOffset; 504 | 505 | Gizmo.Draw.Text( edge.ToString(), new Transform( textPos ) ); 506 | 507 | if ( minDist < maxDist ) 508 | { 509 | // Gizmo.Draw.Line( edge.Project( minDist ), edge.Project( Math.Min( edge.MaxDistance, maxDist ) ) ); 510 | } 511 | } 512 | } 513 | 514 | public static void RunDebugDump( string dump, float? width, bool fromSdf, int maxIterations ) 515 | { 516 | var parsed = Json.Deserialize( dump ); 517 | 518 | if ( fromSdf && parsed.SdfData is not null ) 519 | { 520 | var samples = Convert.FromBase64String( parsed.SdfData.Samples ); 521 | var data = new Sdf2DArrayData( samples, parsed.SdfData.BaseIndex, parsed.SdfData.Size, 522 | parsed.SdfData.RowStride ); 523 | 524 | using var writer = Sdf2DMeshWriter.Rent(); 525 | 526 | writer.AddEdgeLoops( data, 0f ); 527 | writer.DrawGizmos(); 528 | 529 | return; 530 | } 531 | 532 | using var builder = Rent(); 533 | 534 | parsed.Init( builder ); 535 | 536 | Gizmo.Draw.Color = Color.White; 537 | builder.DrawGizmos( 0f, width ?? parsed.EdgeWidth ); 538 | 539 | if ( width <= 0f ) return; 540 | 541 | parsed.Bevel( builder, width ); 542 | 543 | Gizmo.Draw.Color = Color.Blue; 544 | builder.DrawGizmos( width ?? parsed.EdgeWidth, width ?? parsed.EdgeWidth ); 545 | 546 | builder.Fill(); 547 | } 548 | } 549 | -------------------------------------------------------------------------------- /Code/facepunch.libpolygon/PolygonMeshBuilder.Bevel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Sandbox.Polygons; 6 | 7 | partial class PolygonMeshBuilder 8 | { 9 | private HashSet<(int A, int B)> PossibleCuts { get; } = new(); 10 | 11 | [ThreadStatic] private static List<(int A, int B)> Bevel_PossibleCutList; 12 | 13 | [ThreadStatic] private static List Bevel_ActiveEdgeList; 14 | 15 | 16 | /// 17 | /// Add faces starting at each active edge, traveling inwards and upwards to produce a bevel. 18 | /// If the bevel distance is large enough the mesh will become closed. Otherwise, you can use 19 | /// to add a flat face after the bevel. 20 | /// 21 | /// Total distance inwards. 22 | /// Total distance upwards, away from the plane of the polygon. 23 | public PolygonMeshBuilder Bevel( float width, float height ) 24 | { 25 | var angle = MathF.Atan2( width, height ); 26 | 27 | return Bevel( width, height, angle, angle ); 28 | } 29 | 30 | /// 31 | /// Add faces starting at each active edge, traveling inwards and upwards to produce a bevel. 32 | /// Use and to control the normal directions 33 | /// at the start and end of the bevel faces. Angles are in radians, with 0 pointing outwards along 34 | /// the plane of the polygon, and PI/2 pointing upwards away from the plane. 35 | /// If the bevel distance is large enough the mesh will become closed. Otherwise, you can use 36 | /// to add a flat face after the bevel. 37 | /// 38 | /// Total distance inwards. 39 | /// Total distance upwards, away from the plane of the polygon. 40 | /// Angle, in radians, to use for normals at the outside of the bevel. 41 | /// 42 | public PolygonMeshBuilder Bevel( float width, float height, float prevAngle, float nextAngle ) 43 | { 44 | if ( width < 0f ) 45 | { 46 | throw new ArgumentOutOfRangeException( nameof( width ) ); 47 | } 48 | 49 | Validate(); 50 | Bevel_UpdateExistingVertices( width, height, prevAngle, nextAngle ); 51 | 52 | var cutList = Bevel_PossibleCutList ??= new List<(int A, int B)>(); 53 | var edgeList = Bevel_ActiveEdgeList ??= new List(); 54 | 55 | var finished = false; 56 | var endDist = _nextDistance; 57 | 58 | if ( MathF.Abs( _nextDistance ) > 0.001f ) 59 | { 60 | var maxIterations = _activeEdges.Count * _activeEdges.Count; 61 | var maxEdges = _nextEdgeIndex + _activeEdges.Count * 4; 62 | 63 | // Find each event as we sweep inwards with all the active edges 64 | 65 | int iterations; 66 | for ( iterations = 0; iterations < maxIterations && _activeEdges.Count > 0 && _nextEdgeIndex <= maxEdges; ++iterations ) 67 | { 68 | int? closedEdge = null; 69 | int? splitEdge = null; 70 | int? splittingEdge = null; 71 | 72 | Vector2 bestPos = default; 73 | 74 | var bestDist = _nextDistance; 75 | var bestMerge = false; 76 | 77 | // Are any edges closing (reducing down to a point)? 78 | 79 | foreach ( var index in _activeEdges ) 80 | { 81 | ref var edge = ref _allEdges[index]; 82 | 83 | if ( edge.MaxDistance >= bestDist ) continue; 84 | 85 | var next = _allEdges[edge.NextEdge]; 86 | 87 | bestDist = edge.MaxDistance; 88 | closedEdge = edge.Index; 89 | bestPos = (edge.Project( edge.MaxDistance ) + next.Project( edge.MaxDistance )) * 0.5f; 90 | } 91 | 92 | cutList.Clear(); 93 | cutList.AddRange( PossibleCuts ); 94 | 95 | // Are any edges being cut by a vertex? 96 | 97 | foreach ( var (index, otherIndex) in cutList ) 98 | { 99 | if ( !_activeEdges.Contains( index ) || !_activeEdges.Contains( otherIndex ) ) 100 | { 101 | PossibleCuts.Remove( (index, otherIndex) ); 102 | continue; 103 | } 104 | 105 | var edge = _allEdges[index]; 106 | var other = _allEdges[otherIndex]; 107 | var otherNext = _allEdges[other.NextEdge]; 108 | 109 | var splitDist = CalculateSplitDistance( edge, other, otherNext, 110 | out var splitPos, out var merge ); 111 | 112 | if ( splitDist - _nextDistance > 0.001f ) 113 | { 114 | PossibleCuts.Remove( (index, otherIndex) ); 115 | continue; 116 | } 117 | 118 | if ( splitDist >= bestDist ) continue; 119 | 120 | bestDist = splitDist; 121 | bestPos = splitPos; 122 | bestMerge = merge != MergeMode.None; 123 | 124 | closedEdge = null; 125 | splittingEdge = edge.Index; 126 | 127 | splitEdge = merge == MergeMode.End ? otherNext.Index : other.Index; 128 | } 129 | 130 | if ( splittingEdge != null && bestMerge ) 131 | { 132 | Bevel_Merge( splittingEdge.Value, splitEdge.Value, bestPos, bestDist ); 133 | continue; 134 | } 135 | 136 | if ( splittingEdge != null ) 137 | { 138 | Bevel_Split( splittingEdge.Value, splitEdge.Value, bestPos, bestDist ); 139 | continue; 140 | } 141 | 142 | if ( closedEdge != null ) 143 | { 144 | Bevel_Close( closedEdge.Value, bestPos, bestDist ); 145 | continue; 146 | } 147 | 148 | finished = true; 149 | break; 150 | } 151 | 152 | if ( _activeEdges.Count > 0 && iterations == maxIterations || _nextEdgeIndex > maxEdges ) 153 | { 154 | throw new Exception( $"Exploded after {iterations} with {_activeEdges.Count} active edges!" ); 155 | } 156 | } 157 | else 158 | { 159 | finished = true; 160 | } 161 | 162 | if ( !finished && _activeEdges.Count > 0 ) 163 | { 164 | endDist = _activeEdges.Max( i => _allEdges[i].Distance ); 165 | } 166 | 167 | EnsureCapacity( _activeEdges.Count ); 168 | 169 | edgeList.Clear(); 170 | edgeList.AddRange( _activeEdges ); 171 | 172 | _activeEdges.Clear(); 173 | 174 | foreach ( var index in edgeList ) 175 | { 176 | ref var b = ref _allEdges[index]; 177 | ref var a = ref _allEdges[b.PrevEdge]; 178 | ref var c = ref _allEdges[b.NextEdge]; 179 | ref var d = ref _allEdges[AddEdge( b.Project( endDist ), b.Tangent, endDist )]; 180 | 181 | var ai = AddVertices( ref a ); 182 | var bi = AddVertices( ref b ); 183 | var ci = AddVertices( ref c ); 184 | 185 | ConnectEdges( ref a, ref d ); 186 | ConnectEdges( ref d, ref c ); 187 | 188 | var di = AddVertices( ref d, true ); 189 | 190 | AddTriangle( ai.Next, di.Prev, bi.Prev ); 191 | AddTriangle( bi.Next, di.Next, ci.Prev ); 192 | 193 | _activeEdges.Add( d.Index ); 194 | } 195 | 196 | PostBevel(); 197 | 198 | return this; 199 | } 200 | 201 | private void Bevel_UpdateExistingVertices( float width, float height, float prevAngle, float nextAngle ) 202 | { 203 | _nextDistance = _prevDistance + width; 204 | _nextHeight = _prevHeight + height; 205 | _nextAngle = nextAngle; 206 | _minSmoothNormalDot = MathF.Cos( Math.Clamp( MaxSmoothAngle, 0f, MathF.PI * (511f / 512f) ) ); 207 | 208 | _invDistance = width <= 0.0001f ? 0f : 1f / (_nextDistance - _prevDistance); 209 | 210 | if ( !SkipNormals && Math.Abs( _prevAngle - prevAngle ) >= 0.001f ) 211 | { 212 | foreach ( var index in _activeEdges ) 213 | { 214 | ref var edge = ref _allEdges[index]; 215 | edge.Vertices = (-1, -1); 216 | } 217 | } 218 | 219 | _prevAngle = prevAngle; 220 | 221 | PossibleCuts.Clear(); 222 | 223 | foreach ( var index in _activeEdges ) 224 | { 225 | ref var edge = ref _allEdges[index]; 226 | UpdateMaxDistance( ref edge, _allEdges[edge.NextEdge] ); 227 | 228 | foreach ( var otherIndex in _activeEdges ) 229 | { 230 | if ( otherIndex != index ) 231 | { 232 | PossibleCuts.Add( (index, otherIndex) ); 233 | } 234 | } 235 | } 236 | } 237 | 238 | private void Bevel_Merge( int edgeA, int edgeB, Vector2 mergePos, float bestDist ) 239 | { 240 | EnsureCapacity( 2 ); 241 | 242 | ref var a = ref _allEdges[edgeA]; 243 | ref var b = ref _allEdges[edgeB]; 244 | 245 | _activeEdges.Remove( a.Index ); 246 | _activeEdges.Remove( b.Index ); 247 | 248 | if ( a.NextEdge == b.Index && b.NextEdge == a.Index ) 249 | { 250 | return; 251 | } 252 | 253 | ref var aPrev = ref _allEdges[a.PrevEdge]; 254 | ref var bPrev = ref _allEdges[b.PrevEdge]; 255 | 256 | ref var aNext = ref _allEdges[a.NextEdge]; 257 | ref var bNext = ref _allEdges[b.NextEdge]; 258 | 259 | ref var aNew = ref _allEdges[AddEdge( mergePos, a.Tangent, bestDist, 1 )]; 260 | ref var bNew = ref _allEdges[AddEdge( mergePos, b.Tangent, bestDist, -1 )]; 261 | 262 | var aPrevi = AddVertices( ref aPrev ).Next; 263 | var ai = AddVertices( ref a ); 264 | var aNexti = AddVertices( ref aNext ).Prev; 265 | var bPrevi = AddVertices( ref bPrev ).Next; 266 | var bi = AddVertices( ref b ); 267 | var bNexti = AddVertices( ref bNext ).Prev; 268 | 269 | _activeEdges.Add( aNew.Index ); 270 | _activeEdges.Add( bNew.Index ); 271 | 272 | ConnectEdges( ref bPrev, ref aNew ); 273 | ConnectEdges( ref aNew, ref aNext ); 274 | 275 | ConnectEdges( ref aPrev, ref bNew ); 276 | ConnectEdges( ref bNew, ref bNext ); 277 | 278 | UpdateMaxDistance( ref bPrev, aNew ); 279 | UpdateMaxDistance( ref aNew, aNext ); 280 | UpdateMaxDistance( ref aNext, _allEdges[aNext.NextEdge] ); 281 | 282 | UpdateMaxDistance( ref aPrev, bNew ); 283 | UpdateMaxDistance( ref bNew, bNext ); 284 | UpdateMaxDistance( ref bNext, _allEdges[bNext.NextEdge] ); 285 | 286 | var aNewi = AddVertices( ref aNew ); 287 | var bNewi = AddVertices( ref bNew ); 288 | 289 | AddTriangle( aPrevi, bNewi.Prev, ai.Prev ); 290 | AddTriangle( ai.Next, aNewi.Next, aNexti ); 291 | AddTriangle( bPrevi, aNewi.Prev, bi.Prev ); 292 | AddTriangle( bi.Next, bNewi.Next, bNexti ); 293 | 294 | AddAllPossibleCuts( aNew.Index ); 295 | AddAllPossibleCuts( aNext.Index ); 296 | AddAllPossibleCuts( bNew.Index ); 297 | AddAllPossibleCuts( bNext.Index ); 298 | } 299 | 300 | private void Bevel_Split( int splittingEdge, int splitEdge, Vector2 splitPos, float bestDist ) 301 | { 302 | EnsureCapacity( 2 ); 303 | 304 | ref var a = ref _allEdges[splitEdge]; 305 | ref var d = ref _allEdges[splittingEdge]; 306 | ref var b = ref _allEdges[AddEdge( splitPos, a.Tangent, bestDist, 1 )]; 307 | ref var c = ref _allEdges[d.PrevEdge]; 308 | ref var e = ref _allEdges[AddEdge( splitPos, d.Tangent, bestDist, -1 )]; 309 | ref var aNext = ref _allEdges[a.NextEdge]; 310 | ref var dNext = ref _allEdges[d.NextEdge]; 311 | 312 | var ai = AddVertices( ref a ).Next; 313 | var fi = AddVertices( ref aNext ).Prev; 314 | var ci = AddVertices( ref c ).Next; 315 | var di = AddVertices( ref d ); 316 | var gi = AddVertices( ref dNext ).Prev; 317 | 318 | _activeEdges.Remove( d.Index ); 319 | _activeEdges.Add( b.Index ); 320 | _activeEdges.Add( e.Index ); 321 | 322 | ConnectEdges( ref a, ref e ); 323 | ConnectEdges( ref e, ref dNext ); 324 | 325 | ConnectEdges( ref c, ref b ); 326 | ConnectEdges( ref b, ref aNext ); 327 | 328 | UpdateMaxDistance( ref a, e ); 329 | UpdateMaxDistance( ref e, dNext ); 330 | UpdateMaxDistance( ref dNext, _allEdges[dNext.NextEdge] ); 331 | 332 | UpdateMaxDistance( ref c, b ); 333 | UpdateMaxDistance( ref b, aNext ); 334 | UpdateMaxDistance( ref aNext, _allEdges[aNext.NextEdge] ); 335 | 336 | var bi = AddVertices( ref b ); 337 | var ei = AddVertices( ref e ); 338 | 339 | AddTriangle( ai, bi.Next, fi ); 340 | AddTriangle( ci, bi.Prev, di.Prev ); 341 | AddTriangle( di.Next, ei.Next, gi ); 342 | 343 | AddAllPossibleCuts( b.Index ); 344 | AddAllPossibleCuts( dNext.Index ); 345 | AddAllPossibleCuts( e.Index ); 346 | AddAllPossibleCuts( aNext.Index ); 347 | } 348 | 349 | private void Bevel_Close( int closedEdge, Vector2 closePos, float bestDist ) 350 | { 351 | EnsureCapacity( 1 ); 352 | 353 | ref var b = ref _allEdges[closedEdge]; 354 | ref var a = ref _allEdges[b.PrevEdge]; 355 | ref var c = ref _allEdges[b.NextEdge]; 356 | ref var cNext = ref _allEdges[c.NextEdge]; 357 | ref var d = ref _allEdges[AddEdge( closePos, c.Tangent, bestDist )]; 358 | 359 | _activeEdges.Remove( b.Index ); 360 | _activeEdges.Remove( c.Index ); 361 | 362 | if ( b.PrevEdge == b.NextEdge ) 363 | { 364 | return; 365 | } 366 | 367 | _activeEdges.Add( d.Index ); 368 | 369 | ConnectEdges( ref a, ref d ); 370 | ConnectEdges( ref d, ref cNext ); 371 | 372 | UpdateMaxDistance( ref a, d ); 373 | UpdateMaxDistance( ref d, cNext ); 374 | UpdateMaxDistance( ref cNext, _allEdges[cNext.NextEdge] ); 375 | 376 | var ai = AddVertices( ref a ); 377 | var bi = AddVertices( ref b ); 378 | var ci = AddVertices( ref c ); 379 | var ei = AddVertices( ref cNext ); 380 | var di = AddVertices( ref d ); 381 | 382 | var fi = _vertices.Count; 383 | 384 | _vertices.Add( new( 385 | _vertices[di.Prev].Position, 386 | _vertices[bi.Next].Normal, 387 | _vertices[bi.Next].Tangent ) ); 388 | 389 | AddTriangle( ai.Next, di.Prev, bi.Prev ); 390 | AddTriangle( bi.Next, fi, ci.Prev ); 391 | AddTriangle( ci.Next, di.Next, ei.Prev ); 392 | 393 | AddAllPossibleCuts( d.Index ); 394 | AddAllPossibleCuts( cNext.Index ); 395 | } 396 | 397 | private void PostBevel() 398 | { 399 | _prevDistance = _nextDistance; 400 | _prevHeight = _nextHeight; 401 | _prevAngle = _nextAngle; 402 | } 403 | 404 | private void AddAllPossibleCuts( int index ) 405 | { 406 | foreach ( var otherIndex in _activeEdges ) 407 | { 408 | if ( otherIndex != index ) 409 | { 410 | PossibleCuts.Add( (index, otherIndex) ); 411 | PossibleCuts.Add( (otherIndex, index) ); 412 | } 413 | } 414 | } 415 | 416 | private static Vector3 RotateNormal( Vector3 oldNormal, float sin, float cos ) 417 | { 418 | var normal2d = new Vector2( oldNormal.x, oldNormal.y ); 419 | 420 | if ( normal2d.LengthSquared <= 0.000001f ) 421 | { 422 | return oldNormal; 423 | } 424 | 425 | normal2d = normal2d.Normal; 426 | 427 | return new Vector3( normal2d.x * cos, normal2d.y * cos, sin ).Normal; 428 | } 429 | 430 | private static float GetEpsilon( Vector2 vec, float frac = 0.0001f ) 431 | { 432 | return Math.Max( Math.Abs( vec.x ), Math.Abs( vec.y ) ) * frac; 433 | } 434 | 435 | private static float GetEpsilon( Vector2 a, Vector2 b, float frac = 0.0001f ) 436 | { 437 | return Math.Max( GetEpsilon( a ), GetEpsilon( b ) ); 438 | } 439 | 440 | private static float GetEpsilon( Vector2 a, Vector2 b, Vector3 c, float frac = 0.0001f ) 441 | { 442 | return Math.Max( GetEpsilon( a ), Math.Max( GetEpsilon( b ), GetEpsilon( c ) ) ); 443 | } 444 | 445 | private static void UpdateMaxDistance( ref Edge edge, in Edge nextEdge ) 446 | { 447 | if ( edge.NextEdge == edge.PrevEdge ) 448 | { 449 | edge.MaxDistance = edge.Distance; 450 | return; 451 | } 452 | 453 | var baseDistance = Math.Max( edge.Distance, nextEdge.Distance ); 454 | var thisOrigin = edge.Project( baseDistance ); 455 | var nextOrigin = nextEdge.Project( baseDistance ); 456 | 457 | var posDist = Vector2.Dot( nextOrigin - thisOrigin, edge.Tangent ); 458 | 459 | var dPrev = Vector2.Dot( edge.Velocity, edge.Tangent ); 460 | var dNext = Vector2.Dot( nextEdge.Velocity, edge.Tangent ); 461 | 462 | if ( dPrev - dNext <= 0.001f ) 463 | { 464 | var epsilon = GetEpsilon( thisOrigin, nextOrigin, 0.001f ); 465 | edge.MaxDistance = posDist <= epsilon ? baseDistance : float.PositiveInfinity; 466 | } 467 | else 468 | { 469 | edge.MaxDistance = baseDistance + MathF.Max( 0f, posDist / (dPrev - dNext) ); 470 | } 471 | } 472 | 473 | private static void SimpleConnectEdges( ref Edge prev, ref Edge next ) 474 | { 475 | prev.NextEdge = next.Index; 476 | next.PrevEdge = prev.Index; 477 | } 478 | 479 | private static void ConnectEdges( ref Edge prev, ref Edge next ) 480 | { 481 | SimpleConnectEdges( ref prev, ref next ); 482 | 483 | var sum = prev.Normal + next.Normal; 484 | var sqrMag = sum.LengthSquared; 485 | 486 | if ( sqrMag < 0.001f ) 487 | { 488 | next.Velocity = Vector2.Zero; 489 | } 490 | else 491 | { 492 | next.Velocity = 2f * sum / sum.LengthSquared; 493 | } 494 | } 495 | 496 | private enum MergeMode 497 | { 498 | None, 499 | Start, 500 | End 501 | } 502 | 503 | /// 504 | /// Find when the start vertex of would cut . 505 | /// 506 | private static float CalculateSplitDistance( in Edge edge, in Edge other, in Edge otherNext, 507 | out Vector2 splitPos, out MergeMode merge ) 508 | { 509 | splitPos = default; 510 | merge = MergeMode.None; 511 | 512 | if ( other.Index == edge.Index || edge.Twin == other.Index || edge.Velocity.LengthSquared <= 0f ) 513 | { 514 | return float.PositiveInfinity; 515 | } 516 | 517 | var dv0 = Vector2.Dot( other.Velocity - edge.Velocity, other.Normal ); 518 | var dv1 = Vector2.Dot( otherNext.Velocity - edge.Velocity, other.Normal ); 519 | 520 | if ( Math.Min( dv0, dv1 ) <= GetEpsilon( edge.Velocity, other.Velocity, otherNext.Velocity ) ) 521 | { 522 | return float.PositiveInfinity; 523 | } 524 | 525 | var baseDistance = Math.Max( edge.Distance, Math.Max( other.Distance, otherNext.Distance ) ); 526 | var edgeOrigin = edge.Project( baseDistance ); 527 | var otherOrigin = other.Project( baseDistance ); 528 | var otherNextOrigin = otherNext.Project( baseDistance ); 529 | 530 | var dx0 = Vector2.Dot( edgeOrigin - otherOrigin, other.Normal ); 531 | var dx1 = Vector2.Dot( edgeOrigin - otherNextOrigin, other.Normal ); 532 | 533 | if ( Math.Min( dx0, dx1 ) <= -GetEpsilon( edgeOrigin, otherOrigin, otherNextOrigin ) ) 534 | { 535 | return float.PositiveInfinity; 536 | } 537 | 538 | var t0 = dx0 / dv0; 539 | var t1 = dx1 / dv1; 540 | 541 | var t = Math.Min( t0, t1 ); 542 | 543 | if ( t < 0f ) 544 | { 545 | return float.PositiveInfinity; 546 | } 547 | 548 | if ( baseDistance + t >= edge.MaxDistance || baseDistance + t >= other.MaxDistance ) 549 | { 550 | return float.PositiveInfinity; 551 | } 552 | 553 | splitPos = edgeOrigin + edge.Velocity * t; 554 | 555 | var prevPos = otherOrigin + other.Velocity * t0; 556 | var nextPos = otherNextOrigin + otherNext.Velocity * t1; 557 | 558 | var dPrev = Vector2.Dot( splitPos - prevPos, other.Tangent ); 559 | var dNext = Vector2.Dot( splitPos - nextPos, other.Tangent ); 560 | 561 | var epsilon = GetEpsilon( prevPos, nextPos ); 562 | 563 | if ( dPrev <= -epsilon || dNext >= 0f ) 564 | { 565 | return float.PositiveInfinity; 566 | } 567 | 568 | if ( dPrev <= epsilon ) 569 | { 570 | if ( edge.NextEdge == other.Index || edge.PrevEdge == other.Index ) 571 | { 572 | return float.PositiveInfinity; 573 | } 574 | 575 | merge = MergeMode.Start; 576 | } 577 | else if ( dNext >= -epsilon ) 578 | { 579 | if ( edge.NextEdge == otherNext.Index || edge.PrevEdge == otherNext.Index ) 580 | { 581 | return float.PositiveInfinity; 582 | } 583 | 584 | merge = MergeMode.End; 585 | } 586 | 587 | return baseDistance + t; 588 | } 589 | } 590 | -------------------------------------------------------------------------------- /Code/2D/Sdf2DMeshWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Sandbox.Diagnostics; 5 | using Sandbox.Polygons; 6 | 7 | namespace Sandbox.Sdf; 8 | 9 | partial class Sdf2DMeshWriter : Pooled 10 | { 11 | private List SourceEdges { get; } = new(); 12 | 13 | public interface IVertexHelper 14 | where TVertex : unmanaged 15 | { 16 | Vector3 GetPosition( in TVertex vertex ); 17 | TVertex Lerp( in TVertex a, in TVertex b, float t ); 18 | } 19 | 20 | private abstract class MeshWriter : IMeshWriter 21 | where TVertex : unmanaged 22 | where THelper : IVertexHelper, new() 23 | { 24 | private readonly THelper _helper = new(); 25 | 26 | public List Vertices { get; } = new(); 27 | public List Indices { get; } = new(); 28 | 29 | public bool IsEmpty => Indices.Count == 0; 30 | 31 | public virtual void Clear() 32 | { 33 | Vertices.Clear(); 34 | Indices.Clear(); 35 | } 36 | 37 | public void ApplyTo( Mesh mesh ) 38 | { 39 | ThreadSafe.AssertIsMainThread(); 40 | 41 | if ( mesh == null ) 42 | { 43 | return; 44 | } 45 | 46 | if ( mesh.HasVertexBuffer ) 47 | { 48 | if ( Indices.Count > 0 ) 49 | { 50 | if ( mesh.IndexCount < Indices.Count ) 51 | { 52 | mesh.SetIndexBufferSize( Indices.Count ); 53 | } 54 | 55 | if ( mesh.VertexCount < Vertices.Count ) 56 | { 57 | mesh.SetVertexBufferSize( Vertices.Count ); 58 | } 59 | 60 | mesh.SetIndexBufferData( Indices ); 61 | mesh.SetVertexBufferData( Vertices ); 62 | } 63 | 64 | mesh.SetIndexRange( 0, Indices.Count ); 65 | } 66 | else if ( Indices.Count > 0 ) 67 | { 68 | mesh.CreateVertexBuffer( Vertices.Count, Vertex.Layout, Vertices ); 69 | mesh.CreateIndexBuffer( Indices.Count, Indices ); 70 | } 71 | } 72 | 73 | public void Clip( in WorldQuality quality ) 74 | { 75 | Clip( 0f, 0f, quality.ChunkSize, quality.ChunkSize ); 76 | } 77 | 78 | public void Clip( float xMin, float yMin, float xMax, float yMax ) 79 | { 80 | Clip( new Vector3( 1f, 0f, 0f ), xMin ); 81 | Clip( new Vector3( -1f, 0f, 0f ), -xMax ); 82 | 83 | Clip( new Vector3( 0f, 1f, 0f ), yMin ); 84 | Clip( new Vector3( 0f, -1f, 0f ), -yMax ); 85 | } 86 | 87 | private Dictionary<(int Pos, int Neg), int> ClippedEdges { get; } = new(); 88 | 89 | private int ClipEdge( Vector3 normal, float distance, int posIndex, int negIndex ) 90 | { 91 | const float epsilon = 0.001f; 92 | 93 | if ( ClippedEdges.TryGetValue( (posIndex, negIndex), out var index ) ) 94 | { 95 | return index; 96 | } 97 | 98 | var a = Vertices[posIndex]; 99 | var b = Vertices[negIndex]; 100 | 101 | var x = Vector3.Dot( _helper.GetPosition( a ), normal ) - distance; 102 | var y = Vector3.Dot( _helper.GetPosition( b ), normal ) - distance; 103 | 104 | if ( x - y <= epsilon ) 105 | { 106 | ClippedEdges.Add( (posIndex, negIndex), posIndex ); 107 | return posIndex; 108 | } 109 | 110 | var t = x / (x - y); 111 | 112 | if ( t <= epsilon ) 113 | { 114 | ClippedEdges.Add( (posIndex, negIndex), posIndex ); 115 | return posIndex; 116 | } 117 | 118 | index = Vertices.Count; 119 | Vertices.Add( _helper.Lerp( a, b, t ) ); 120 | 121 | ClippedEdges.Add( (posIndex, negIndex), index ); 122 | 123 | return index; 124 | } 125 | 126 | private void ClipOne( Vector3 normal, float distance, int negIndex, int posAIndex, int posBIndex ) 127 | { 128 | var clipAIndex = ClipEdge( normal, distance, posAIndex, negIndex ); 129 | var clipBIndex = ClipEdge( normal, distance, posBIndex, negIndex ); 130 | 131 | if ( clipAIndex != posAIndex ) 132 | { 133 | Indices.Add( clipAIndex ); 134 | Indices.Add( posAIndex ); 135 | Indices.Add( posBIndex ); 136 | } 137 | 138 | if ( clipBIndex != posBIndex ) 139 | { 140 | Indices.Add( clipAIndex ); 141 | Indices.Add( posBIndex ); 142 | Indices.Add( clipBIndex ); 143 | } 144 | } 145 | 146 | private void ClipTwo( Vector3 normal, float distance, int posIndex, int negAIndex, int negBIndex ) 147 | { 148 | var clipAIndex = ClipEdge( normal, distance, posIndex, negAIndex ); 149 | var clipBIndex = ClipEdge( normal, distance, posIndex, negBIndex ); 150 | 151 | if ( clipAIndex != posIndex && clipBIndex != posIndex ) 152 | { 153 | Indices.Add( posIndex ); 154 | Indices.Add( clipAIndex ); 155 | Indices.Add( clipBIndex ); 156 | } 157 | } 158 | 159 | public void Clip( Vector3 normal, float distance ) 160 | { 161 | const float epsilon = 0.001f; 162 | 163 | ClippedEdges.Clear(); 164 | 165 | var indexCount = Indices.Count; 166 | 167 | for ( var i = 0; i < indexCount; i += 3 ) 168 | { 169 | var ai = Indices[i + 0]; 170 | var bi = Indices[i + 1]; 171 | var ci = Indices[i + 2]; 172 | 173 | var a = _helper.GetPosition( Vertices[ai] ); 174 | var b = _helper.GetPosition( Vertices[bi] ); 175 | var c = _helper.GetPosition( Vertices[ci] ); 176 | 177 | var aNeg = Vector3.Dot( normal, a ) - distance < -epsilon; 178 | var bNeg = Vector3.Dot( normal, b ) - distance < -epsilon; 179 | var cNeg = Vector3.Dot( normal, c ) - distance < -epsilon; 180 | 181 | switch (aNeg, bNeg, cNeg) 182 | { 183 | case (false, false, false): 184 | Indices.Add( ai ); 185 | Indices.Add( bi ); 186 | Indices.Add( ci ); 187 | break; 188 | 189 | case (true, false, false): 190 | ClipOne( normal, distance, ai, bi, ci ); 191 | break; 192 | case (false, true, false): 193 | ClipOne( normal, distance, bi, ci, ai ); 194 | break; 195 | case (false, false, true): 196 | ClipOne( normal, distance, ci, ai, bi ); 197 | break; 198 | 199 | case (false, true, true): 200 | ClipTwo( normal, distance, ai, bi, ci ); 201 | break; 202 | case (true, false, true): 203 | ClipTwo( normal, distance, bi, ci, ai ); 204 | break; 205 | case (true, true, false): 206 | ClipTwo( normal, distance, ci, ai, bi ); 207 | break; 208 | } 209 | } 210 | 211 | Indices.RemoveRange( 0, indexCount ); 212 | } 213 | } 214 | 215 | private class FrontBackMeshWriter : MeshWriter 216 | { 217 | public void AddFaces( PolygonMeshBuilder builder, Vector3 offset, Vector3 scale, float texCoordSize ) 218 | { 219 | var uvScale = 1f / texCoordSize; 220 | var zScale = Math.Sign( scale.z ); 221 | 222 | var indexOffset = Vertices.Count; 223 | 224 | for ( var i = 0; i < builder.Vertices.Count; ++i ) 225 | { 226 | var vertex = builder.Vertices[i]; 227 | var pos = vertex.Position * scale; 228 | var normal = vertex.Normal; 229 | var tangent = vertex.Tangent; 230 | 231 | normal.z *= zScale; 232 | 233 | Vertices.Add( new Vertex( offset + pos, normal, tangent, pos * uvScale ) ); 234 | } 235 | 236 | if ( scale.z >= 0f ) 237 | { 238 | foreach ( var index in builder.Indices ) 239 | { 240 | Indices.Add( indexOffset + index ); 241 | } 242 | } 243 | else 244 | { 245 | for ( var i = builder.Indices.Count - 1; i >= 0; --i ) 246 | { 247 | Indices.Add( indexOffset + builder.Indices[i] ); 248 | } 249 | } 250 | } 251 | } 252 | 253 | private class CutMeshWriter : MeshWriter 254 | { 255 | private Dictionary IndexMap { get; } = new(); 256 | 257 | public void AddFaces( IReadOnlyList vertices, IReadOnlyList edgeLoops, Vector3 offset, Vector3 scale, float texCoordSize, float maxSmoothAngle ) 258 | { 259 | var minSmoothNormalDot = MathF.Cos( maxSmoothAngle * MathF.PI / 180f ); 260 | 261 | static float GetV( Vector2 pos, Vector2 normal ) 262 | { 263 | return Math.Abs( normal.y ) > Math.Abs( normal.x ) ? pos.x : pos.y; 264 | } 265 | 266 | var texCoordScale = texCoordSize == 0f ? 0f : 1f / texCoordSize; 267 | 268 | foreach ( var edgeLoop in edgeLoops ) 269 | { 270 | IndexMap.Clear(); 271 | 272 | if ( edgeLoop.Count < 3 ) 273 | { 274 | continue; 275 | } 276 | 277 | var prev = vertices[edgeLoop.FirstIndex + edgeLoop.Count - 2]; 278 | var curr = vertices[edgeLoop.FirstIndex + edgeLoop.Count - 1]; 279 | 280 | var prevNormal = Helpers.NormalizeSafe( Helpers.Rotate90( prev - curr ) ); 281 | var currIndex = edgeLoop.Count - 1; 282 | 283 | for ( var i = 0; i < edgeLoop.Count; i++ ) 284 | { 285 | var next = vertices[edgeLoop.FirstIndex + i]; 286 | var nextNormal = Helpers.NormalizeSafe( Helpers.Rotate90( curr - next ) ); 287 | 288 | var prevV = GetV( curr * (Vector2) scale, prevNormal ) * texCoordScale; 289 | var nextV = GetV( curr * (Vector2) scale, nextNormal ) * texCoordScale; 290 | 291 | var index = Vertices.Count; 292 | var frontPos = offset + new Vector3( curr.x, curr.y, 0.5f ) * scale; 293 | var backPos = offset + new Vector3( curr.x, curr.y, -0.5f ) * scale; 294 | 295 | var frontU = 0f; 296 | var backU = scale.z * texCoordScale; 297 | 298 | if ( Vector2.Dot( prevNormal, nextNormal ) >= minSmoothNormalDot && Math.Abs( prevV - nextV ) <= 0.001f ) 299 | { 300 | IndexMap.Add( currIndex, (index, index) ); 301 | 302 | var normal = Helpers.NormalizeSafe( prevNormal + nextNormal ); 303 | 304 | Vertices.Add( new Vertex( frontPos, normal, new Vector4( 0f, 0f, 1f, 1f ), new Vector2( frontU, prevV ) ) ); 305 | Vertices.Add( new Vertex( backPos, normal, new Vector4( 0f, 0f, 1f, 1f ), new Vector2( backU, prevV ) ) ); 306 | } 307 | else 308 | { 309 | IndexMap.Add( currIndex, (index, index + 2) ); 310 | 311 | Vertices.Add( new Vertex( frontPos, prevNormal, new Vector4( 0f, 0f, 1f, 1f ), new Vector2( frontU, prevV ) ) ); 312 | Vertices.Add( new Vertex( backPos, prevNormal, new Vector4( 0f, 0f, 1f, 1f ), new Vector2( backU, prevV ) ) ); 313 | 314 | Vertices.Add( new Vertex( frontPos, nextNormal, new Vector4( 0f, 0f, 1f, 1f ), new Vector2( frontU, nextV ) ) ); 315 | Vertices.Add( new Vertex( backPos, nextNormal, new Vector4( 0f, 0f, 1f, 1f ), new Vector2( backU, nextV ) ) ); 316 | } 317 | 318 | currIndex = i; 319 | prevNormal = nextNormal; 320 | prev = curr; 321 | curr = next; 322 | } 323 | 324 | var a = IndexMap[edgeLoop.Count - 1]; 325 | for ( var i = 0; i < edgeLoop.Count; i++ ) 326 | { 327 | var b = IndexMap[i]; 328 | 329 | Indices.Add( a.Next + 0 ); 330 | Indices.Add( b.Prev + 0 ); 331 | Indices.Add( a.Next + 1 ); 332 | 333 | Indices.Add( a.Next + 1 ); 334 | Indices.Add( b.Prev + 0 ); 335 | Indices.Add( b.Prev + 1 ); 336 | 337 | a = b; 338 | } 339 | } 340 | } 341 | } 342 | 343 | private readonly struct CollisionVertexHelper : IVertexHelper 344 | { 345 | public Vector3 GetPosition( in Vector3 vertex ) 346 | { 347 | return vertex; 348 | } 349 | 350 | public Vector3 Lerp( in Vector3 a, in Vector3 b, float t ) 351 | { 352 | return Vector3.Lerp( a, b, t ); 353 | } 354 | } 355 | 356 | private class CollisionMeshWriter : MeshWriter 357 | { 358 | public void AddFaces( PolygonMeshBuilder builder, Vector3 offset, Vector3 scale ) 359 | { 360 | var indexOffset = Vertices.Count; 361 | 362 | foreach ( var v in builder.Vertices ) 363 | { 364 | Vertices.Add( offset + v.Position * scale ); 365 | } 366 | 367 | if ( scale.z >= 0f ) 368 | { 369 | foreach ( var index in builder.Indices ) 370 | { 371 | Indices.Add( indexOffset + index ); 372 | } 373 | } 374 | else 375 | { 376 | for ( var i = builder.Indices.Count - 1; i >= 0; --i ) 377 | { 378 | Indices.Add( indexOffset + builder.Indices[i] ); 379 | } 380 | } 381 | } 382 | } 383 | 384 | private readonly FrontBackMeshWriter _frontMeshWriter = new(); 385 | private readonly FrontBackMeshWriter _backMeshWriter = new(); 386 | private readonly CutMeshWriter _cutMeshWriter = new(); 387 | private readonly CollisionMeshWriter _collisionMeshWriter = new(); 388 | 389 | public IMeshWriter FrontWriter => _frontMeshWriter; 390 | public IMeshWriter BackWriter => _backMeshWriter; 391 | public IMeshWriter CutWriter => _cutMeshWriter; 392 | 393 | public (List Vertices, List Indices) CollisionMesh => 394 | (_collisionMeshWriter.Vertices, _collisionMeshWriter.Indices); 395 | 396 | public byte[] Samples { get; set; } 397 | 398 | public override void Reset() 399 | { 400 | SourceEdges.Clear(); 401 | 402 | _frontMeshWriter.Clear(); 403 | _backMeshWriter.Clear(); 404 | _cutMeshWriter.Clear(); 405 | _collisionMeshWriter.Clear(); 406 | } 407 | 408 | public Vector2 DebugOffset { get; set; } 409 | public float DebugScale { get; set; } = 1f; 410 | 411 | public void AddEdgeLoops( Sdf2DArrayData data, float maxSmoothAngle ) 412 | { 413 | SourceEdges.Clear(); 414 | 415 | var size = data.Size; 416 | 417 | // Find edges between solid and empty 418 | 419 | for ( var y = -2; y <= size + 2; ++y ) 420 | { 421 | for ( int x = -2; x <= size + 2; ++x ) 422 | { 423 | var aRaw = data[x + 0, y + 0]; 424 | var bRaw = data[x + 1, y + 0]; 425 | var cRaw = data[x + 0, y + 1]; 426 | var dRaw = data[x + 1, y + 1]; 427 | 428 | AddSourceEdges( x, y, aRaw, bRaw, cRaw, dRaw ); 429 | } 430 | } 431 | 432 | FindEdgeLoops( data, maxSmoothAngle, 0.25f ); 433 | } 434 | 435 | public void Write( Sdf2DArrayData data, Sdf2DLayer layer, bool renderMesh, bool collisionMesh ) 436 | { 437 | var quality = layer.Quality; 438 | 439 | AddEdgeLoops( data, layer.MaxSmoothAngle ); 440 | 441 | try 442 | { 443 | if ( renderMesh ) 444 | { 445 | WriteRenderMesh( layer ); 446 | } 447 | 448 | if ( collisionMesh ) 449 | { 450 | WriteCollisionMesh( layer ); 451 | } 452 | } 453 | catch (Exception e ) 454 | { 455 | var scale = quality.UnitSize; 456 | var bevelScale = layer.EdgeRadius / scale; 457 | 458 | Log.Error( $"Internal error in PolygonMeshBuilder!\n\n" + 459 | $"Please paste the info below in this thread:\nhttps://github.com/Facepunch/sbox-sdf/issues/17\n\n" + 460 | $"{Json.Serialize( new DebugDump( e.ToString(), 461 | SerializeEdgeLoops( 0, EdgeLoops.Count ), 462 | new SdfDataDump( data ), 463 | layer.EdgeStyle, 464 | bevelScale, 465 | layer.EdgeFaces ) )}" ); 466 | } 467 | } 468 | 469 | private bool NextPolygon( ref int index, out int offset, out int count ) 470 | { 471 | if ( index >= EdgeLoops.Count ) 472 | { 473 | offset = count = default; 474 | return false; 475 | } 476 | 477 | // TODO: this seemed to leave some negative polys on their own, so just doing them all now 478 | 479 | offset = index; 480 | count = EdgeLoops.Count - index; 481 | 482 | index += count; 483 | 484 | return count > 0; 485 | 486 | offset = index; 487 | count = 1; 488 | 489 | Assert.True( EdgeLoops[offset].Area > 0f ); 490 | 491 | while ( offset + count < EdgeLoops.Count && EdgeLoops[offset + count].Area < 0f ) 492 | { 493 | ++count; 494 | } 495 | 496 | index += count; 497 | 498 | return count > 0; 499 | } 500 | 501 | private void InitPolyMeshBuilder( PolygonMeshBuilder builder, int offset, int count ) 502 | { 503 | builder.Clear(); 504 | 505 | for ( var i = 0; i < count; ++i ) 506 | { 507 | var edgeLoop = EdgeLoops[offset + i]; 508 | builder.AddEdgeLoop( SourceVertices, edgeLoop.FirstIndex, edgeLoop.Count ); 509 | } 510 | } 511 | 512 | public string SerializeEdgeLoops( int offset, int count ) 513 | { 514 | var writer = new StringWriter(); 515 | 516 | for ( var i = 0; i < count; ++i ) 517 | { 518 | var edgeLoop = EdgeLoops[offset + i]; 519 | 520 | for ( var j = 0; j < edgeLoop.Count; ++j ) 521 | { 522 | var vertex = SourceVertices[edgeLoop.FirstIndex + j]; 523 | writer.Write( $"{vertex.x:R},{vertex.y:R};" ); 524 | } 525 | 526 | writer.Write( "\n" ); 527 | } 528 | 529 | return writer.ToString(); 530 | } 531 | 532 | private void WriteRenderMesh( Sdf2DLayer layer ) 533 | { 534 | var quality = layer.Quality; 535 | var scale = quality.UnitSize; 536 | var edgeRadius = layer.EdgeStyle == EdgeStyle.Sharp ? 0f : layer.EdgeRadius; 537 | var allSameMaterial = layer.FrontFaceMaterial == layer.BackFaceMaterial && layer.BackFaceMaterial == layer.CutFaceMaterial; 538 | 539 | if ( !allSameMaterial && layer.CutFaceMaterial != null ) 540 | { 541 | _cutMeshWriter.AddFaces( SourceVertices, EdgeLoops, 542 | new Vector3( 0f, 0f, layer.Offset ), 543 | new Vector3( scale, scale, layer.Depth - edgeRadius * 2f ), 544 | layer.TexCoordSize, layer.MaxSmoothAngle ); 545 | 546 | _cutMeshWriter.Clip( quality ); 547 | } 548 | 549 | if ( layer.FrontFaceMaterial == null && layer.BackFaceMaterial == null ) return; 550 | 551 | using var polyMeshBuilder = PolygonMeshBuilder.Rent(); 552 | 553 | polyMeshBuilder.MaxSmoothAngle = layer.MaxSmoothAngle * MathF.PI / 180f; 554 | 555 | var bevelScale = layer.EdgeRadius / scale; 556 | 557 | var index = 0; 558 | while ( NextPolygon( ref index, out var offset, out var count ) ) 559 | { 560 | InitPolyMeshBuilder( polyMeshBuilder, offset, count ); 561 | 562 | if ( allSameMaterial ) 563 | { 564 | polyMeshBuilder.Extrude( (layer.Depth * 0.5f - edgeRadius) / scale ); 565 | } 566 | 567 | switch ( layer.EdgeStyle ) 568 | { 569 | case EdgeStyle.Sharp: 570 | polyMeshBuilder.Fill(); 571 | break; 572 | 573 | case EdgeStyle.Bevel: 574 | polyMeshBuilder.Bevel( bevelScale, bevelScale ); 575 | polyMeshBuilder.Fill(); 576 | break; 577 | 578 | case EdgeStyle.Round: 579 | polyMeshBuilder.Arc( bevelScale, bevelScale, layer.EdgeFaces ); 580 | polyMeshBuilder.Fill(); 581 | break; 582 | } 583 | 584 | if ( allSameMaterial ) 585 | { 586 | polyMeshBuilder.Mirror(); 587 | 588 | _frontMeshWriter.AddFaces( polyMeshBuilder, 589 | new Vector3( 0f, 0f, layer.Offset ), 590 | new Vector3( scale, scale, scale ), 591 | layer.TexCoordSize ); 592 | 593 | continue; 594 | } 595 | 596 | if ( layer.FrontFaceMaterial != null ) 597 | { 598 | _frontMeshWriter.AddFaces( polyMeshBuilder, 599 | new Vector3( 0f, 0f, layer.Depth * 0.5f + layer.Offset - edgeRadius ), 600 | new Vector3( scale, scale, scale ), 601 | layer.TexCoordSize ); 602 | } 603 | 604 | if ( layer.BackFaceMaterial != null ) 605 | { 606 | _backMeshWriter.AddFaces( polyMeshBuilder, 607 | new Vector3( 0f, 0f, layer.Depth * -0.5f + layer.Offset + edgeRadius ), 608 | new Vector3( scale, scale, -scale ), 609 | layer.TexCoordSize ); 610 | } 611 | } 612 | 613 | _frontMeshWriter.Clip( quality ); 614 | _backMeshWriter.Clip( quality ); 615 | } 616 | 617 | private void WriteCollisionMesh( Sdf2DLayer layer ) 618 | { 619 | var quality = layer.Quality; 620 | var scale = quality.UnitSize; 621 | 622 | using var polyMeshBuilder = PolygonMeshBuilder.Rent(); 623 | 624 | polyMeshBuilder.SkipNormals = true; 625 | 626 | var index = 0; 627 | while ( NextPolygon( ref index, out var offset, out var count ) ) 628 | { 629 | InitPolyMeshBuilder( polyMeshBuilder, offset, count ); 630 | 631 | polyMeshBuilder.Extrude( layer.Depth * 0.5f / scale ); 632 | polyMeshBuilder.Fill(); 633 | polyMeshBuilder.Mirror(); 634 | 635 | _collisionMeshWriter.AddFaces( polyMeshBuilder, 636 | new Vector3( 0f, 0f, layer.Offset ), 637 | new Vector3( scale, scale, scale ) ); 638 | } 639 | 640 | _collisionMeshWriter.Clip( quality ); 641 | } 642 | 643 | public void DrawGizmos() 644 | { 645 | foreach ( var edgeLoop in EdgeLoops ) 646 | { 647 | var prev = SourceVertices[edgeLoop.FirstIndex + edgeLoop.Count - 1]; 648 | 649 | for ( var i = 0; i < edgeLoop.Count; i++ ) 650 | { 651 | var next = SourceVertices[edgeLoop.FirstIndex + i]; 652 | 653 | Gizmo.Draw.Line( prev, next ); 654 | 655 | prev = next; 656 | } 657 | } 658 | } 659 | } 660 | --------------------------------------------------------------------------------