//NOTE: Burst + Unity.Mathematics is ~2x faster //if not using comment this out #define USE_BURST_AND_MATH using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; using Object = UnityEngine.Object; #if USE_BURST_AND_MATH using Unity.Mathematics; using Unity.Burst; // Aliases using Random = Unity.Mathematics.Random; using vec3 = Unity.Mathematics.float3; using vec4 = Unity.Mathematics.float4; #else // Aliases using Random = UnityEngine.Random; using vec3 = UnityEngine.Vector3; using vec4 = UnityEngine.Vector4; #endif namespace SDFr { public class SDFVolume : AVolume { #if USE_BURST_AND_MATH private const string strProgressTitle = "SDFr [Burst]"; #else private const string strProgressTitle = "SDFr"; #endif private const string strProgress = "Generating signed distance field..."; private const int LAYER_FRONT_GEO = 4; //"water" builtin private int LAYER_MASK_FRONT = 1 << LAYER_FRONT_GEO; private const int LAYER_BACK_GEO = 5; //"UI" builtin private int LAYER_MASK_BACK = 1 << LAYER_BACK_GEO; private Action onBakeComplete; protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!disposing) return; onBakeComplete = null; } public void Bake( int raySamples, List renderers, Action bakeComplete, object passthrough = null ) { onBakeComplete = bakeComplete; int progressInterval = _settings.CellCount / 4; int progress = 0; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); vec3 halfVoxel = _settings.HalfVoxel; float maxDistance = 0f; //adjusted to best timings from testing but it could vary by CPU int calcRayLengthBatchCount = 32; calcRayLengthBatchCount = Mathf.Clamp(calcRayLengthBatchCount,1,raySamples); int raycastBatchCount = 8; raycastBatchCount = Mathf.Clamp(raycastBatchCount,1,raySamples); int prepareRaysBatchCount = 64; prepareRaysBatchCount = Mathf.Clamp(prepareRaysBatchCount,1,raySamples); int compareBatchCount = 128; //for raycast method front facing geo and flipped backfacing geo is required List geoFront = new List(); List geoBack = new List(); CreateColliders( ref renderers, ref geoFront, ref geoBack ); //prepare data NativeArray distances = new NativeArray(_settings.CellCount, Allocator.TempJob); NativeArray allResults = new NativeArray(_settings.CellCount,Allocator.TempJob); //constant for all cells NativeArray sphereSamples = new NativeArray(raySamples, Allocator.TempJob); //NativeArray randomDirections = new NativeArray(raySamples, Allocator.TempJob); NativeArray volumePlanes = new NativeArray(6, Allocator.TempJob); GetUniformPointsOnSphereNormalized(ref sphereSamples); //GetRandomDirections( _halfVoxel*settings.JitterScale, settings.JitterSeed, ref randomDirections); vec3 aabbMin = BoundsWorldAABB.min; vec3 aabbMax = BoundsWorldAABB.max; //the max ray length, used to normalize all resulting distances //so they are treated as 0 to 1 within a volume float aabbMagnitude = BoundsWorldAABB.size.magnitude; Plane pl = new Plane(Vector3.right,aabbMin); Plane pr = new Plane(Vector3.left,aabbMax); Plane pd = new Plane(Vector3.up,aabbMin); Plane pu = new Plane(Vector3.down,aabbMax); Plane pb = new Plane(Vector3.forward,aabbMin); Plane pf = new Plane(Vector3.back,aabbMax); volumePlanes[0] = new vec4( pl.normal.x, pl.normal.y, pl.normal.z, pl.distance); volumePlanes[1] = new vec4( pr.normal.x, pr.normal.y, pr.normal.z, pr.distance); volumePlanes[2] = new vec4( pd.normal.x, pd.normal.y, pd.normal.z, pd.distance); volumePlanes[3] = new vec4( pu.normal.x, pu.normal.y, pu.normal.z, pu.distance); volumePlanes[4] = new vec4( pb.normal.x, pb.normal.y, pb.normal.z, pb.distance); volumePlanes[5] = new vec4( pf.normal.x, pf.normal.y, pf.normal.z, pf.distance); //iterate each cell performing raycasted samples for (int i = 0; i < _settings.CellCount; i++) { #if UNITY_EDITOR if (i % progressInterval == 0) { EditorUtility.DisplayProgressBar(strProgressTitle,strProgress,i/(float)_settings.CellCount); } #endif vec3 positionWS = _settings.ToPositionWS(i,LocalToWorldNoScale); vec3 centerVoxelWS = positionWS + halfVoxel; NativeArray rayLengths = new NativeArray(raySamples, Allocator.TempJob); NativeArray allRaycastsFront = new NativeArray(raySamples, Allocator.TempJob); NativeArray allRaycastsBack = new NativeArray(raySamples, Allocator.TempJob); NativeArray frontHits = new NativeArray(raySamples, Allocator.TempJob); NativeArray backHits = new NativeArray(raySamples, Allocator.TempJob); //calculate the ray lengths, just so rays are clipped within the volume when raycasting CalculateRayLengths calcRayLengths = new CalculateRayLengths { Samples = sphereSamples, VolumePlanes = volumePlanes, RayLengths = rayLengths, RayLength = aabbMagnitude, RayOrigin = centerVoxelWS }; JobHandle rayLengthHandle = calcRayLengths.Schedule(raySamples,calcRayLengthBatchCount); //prepare raycasts front PrepareRaycastCommands frontPrc = new PrepareRaycastCommands { Samples = sphereSamples, RayLengths = rayLengths, LayerMask = LAYER_MASK_FRONT, Raycasts = allRaycastsFront, RayOrigin = centerVoxelWS, }; //prepare raycasts back PrepareRaycastCommands backPrc = new PrepareRaycastCommands { Samples = sphereSamples, RayLengths = rayLengths, LayerMask = LAYER_MASK_BACK, Raycasts = allRaycastsBack, RayOrigin = centerVoxelWS, }; //schedule front raycasts JobHandle prepareFrontHandle = frontPrc.Schedule( raySamples, prepareRaysBatchCount, rayLengthHandle); JobHandle scheduleFrontHandle = RaycastCommand.ScheduleBatch( allRaycastsFront, frontHits, raycastBatchCount, prepareFrontHandle); //schedule back raycasts JobHandle prepareBackHandle = backPrc.Schedule( raySamples, prepareRaysBatchCount, rayLengthHandle); JobHandle scheduleBackHandle = RaycastCommand.ScheduleBatch( allRaycastsBack, backHits, raycastBatchCount, prepareBackHandle); //combine handles JobHandle frontBackHandle = JobHandle.CombineDependencies(scheduleFrontHandle, scheduleBackHandle); //process results and put into current cell index ProcessHits processHits = new ProcessHits { FrontHits = frontHits, BackHits = backHits, Results = allResults.Slice(i,1), }; JobHandle cellHandle = processHits.Schedule(frontBackHandle); cellHandle.Complete(); rayLengths.Dispose(); allRaycastsFront.Dispose(); allRaycastsBack.Dispose(); frontHits.Dispose(); backHits.Dispose(); } //for each cell //final distances CompareDistances compareDistances = new CompareDistances { Distances = distances, Results = allResults }; JobHandle compareDistancesHandle = compareDistances.Schedule(_settings.CellCount,compareBatchCount); compareDistancesHandle.Complete(); stopwatch.Stop(); Debug.Log("SDF bake completed in "+stopwatch.Elapsed.ToString("mm\\:ss\\.ff")); #if UNITY_EDITOR EditorUtility.ClearProgressBar(); #endif float[] distancesOut = new float[_settings.CellCount]; distances.CopyTo(distancesOut); //cleanup all the temp arrays distances.Dispose(); allResults.Dispose(); sphereSamples.Dispose(); //randomDirections.Dispose(); volumePlanes.Dispose(); foreach (var c in geoFront) { Object.DestroyImmediate(c.gameObject); } foreach (var c in geoBack) { Object.DestroyImmediate(c.gameObject); } //NOTE do not use max distance, instead use aabbMagnitude so distance fields are interchangeable bakeComplete?.Invoke( this, distancesOut, aabbMagnitude, passthrough ); } #if USE_BURST_AND_MATH [BurstCompile] #endif struct CalculateRayLengths : IJobParallelFor { [ReadOnly] public NativeArray Samples; [ReadOnly] public NativeArray VolumePlanes; [NativeDisableContainerSafetyRestriction] //NOTE should be SAFE because it is using slices public NativeSlice RayLengths; public vec3 RayOrigin; public float RayLength; //default ray length [MethodImpl(MethodImplOptions.AggressiveInlining)] bool PlaneRaycast( vec3 ro, vec3 rd, vec4 plane, out float distance ) { #if USE_BURST_AND_MATH float a = dot(rd, plane.xyz); float num = -dot(ro, plane.xyz) - plane.w; #else float a = dot(rd, plane); float num = -dot(ro, plane) - plane.w; #endif if ( abs(a) < EPSILON ) { distance = 0.0f; return false; } distance = num / a; return distance > 0.0; } public void Execute(int index) { vec3 rd = Samples[index]; vec3 ro = RayOrigin; float nearest = RayLength; for (int v = 0; v < 6; v++) { if ( PlaneRaycast(ro,rd,VolumePlanes[v],out float d)) nearest = min(nearest, d); } RayLengths[index] = nearest; } } #if USE_BURST_AND_MATH [BurstCompile] #endif struct PrepareRaycastCommands : IJobParallelFor { [ReadOnly] public NativeArray Samples; [NativeDisableContainerSafetyRestriction] //NOTE should be SAFE because it is using slices [ReadOnly] public NativeSlice RayLengths; public NativeArray Raycasts; public vec3 RayOrigin; public int LayerMask; public void Execute(int index) { vec3 rd = Samples[index]; vec3 ro = RayOrigin; Raycasts[index] = new RaycastCommand(ro, rd, RayLengths[index], LayerMask ); } } //TODO SIMD-ize private struct CellResults { public float FrontMinDistance; public int FrontTotalHits; public float BackMinDistance; public int BackTotalHits; } #if USE_BURST_AND_MATH [BurstCompile] #endif struct ProcessHits : IJob { [NativeDisableContainerSafetyRestriction] [ReadOnly] public NativeSlice FrontHits; [NativeDisableContainerSafetyRestriction] [ReadOnly] public NativeSlice BackHits; [NativeDisableContainerSafetyRestriction] public NativeSlice Results; //front and back public void Execute() { int samples = FrontHits.Length; int frontHits = 0; int backHits = 0; float minFront = float.MaxValue; float minBack = float.MaxValue; for (int i = 0; i < samples; i++) { RaycastHit frontHit = FrontHits[i]; RaycastHit backHit = BackHits[i]; float frontHitDistance = abs(frontHit.distance); float backHitDistance = abs(backHit.distance); //workaround to avoid using Collider (main thread only) bool didFrontHit = frontHitDistance > 0f; bool didBackHit = backHitDistance > 0f; if (didFrontHit) { if (didBackHit && backHitDistance < frontHitDistance) { backHits++; minBack = min(minBack, backHitDistance); } else { frontHits++; minFront = min(minFront, frontHitDistance); } } else if ( didBackHit ) { backHits++; minBack = min(minBack, backHitDistance); } } CellResults results; results.FrontTotalHits = frontHits; results.FrontMinDistance = minFront; results.BackTotalHits = backHits; results.BackMinDistance = minBack; //using native slice, only write to the one index allowed! Results[0] = results; } } struct CompareDistances : IJobParallelFor { [ReadOnly] public NativeArray Results; public NativeArray Distances; public void Execute( int index ) { //now based on results determine if cell is front or back facing CellResults results = Results[index]; int combinedHits = results.FrontTotalHits + results.BackTotalHits; float finalDistance = 0f; float sign = 1f; float fh = max(0.000001f, results.FrontTotalHits); float bh = max(0.000001f, results.BackTotalHits); //NOTE only take if combined hits are greater than 0, otherwise no hit if (combinedHits > 0) { if (fh / bh >= 1f ) //more front hits { finalDistance = results.FrontMinDistance; sign = 1f; } else //more back { finalDistance = results.BackMinDistance; sign = -1f; } } //extra safety if (float.IsInfinity(finalDistance) || float.IsNaN(finalDistance)) { finalDistance = float.MaxValue; } //store signed distance Distances[index] = finalDistance * sign; } } private static void CreateColliders( ref List renderers, ref List geoFront, ref List geoBack) { foreach (var r in renderers) { Mesh mesh = null; if (r is MeshRenderer) { MeshFilter f = r.GetComponent(); if (f == null) continue; if (f.sharedMesh == null) continue; mesh = f.sharedMesh; } else if (r is SkinnedMeshRenderer) { if ((r as SkinnedMeshRenderer).sharedMesh == null) continue; mesh = (r as SkinnedMeshRenderer).sharedMesh; } else { continue; } //the renderer's transform Transform t = r.transform; vec3 tPosition = t.position; Quaternion tRotation = t.rotation; vec3 tScale = t.lossyScale; //front facing collision GameObject front = new GameObject {layer = LAYER_FRONT_GEO}; front.transform.position = tPosition; front.transform.rotation = tRotation; front.transform.localScale = tScale; MeshCollider frontCollider = front.AddComponent(); frontCollider.cookingOptions = MeshColliderCookingOptions.None; frontCollider.convex = false; frontCollider.sharedMesh = mesh; geoFront.Add( frontCollider ); //back facing collision Mesh flippedMesh = FlipTriangles( mesh ); GameObject back = new GameObject {layer = LAYER_BACK_GEO}; back.transform.position = tPosition; back.transform.rotation = tRotation; back.transform.localScale = tScale; MeshCollider backCollider = back.AddComponent(); backCollider.cookingOptions = MeshColliderCookingOptions.None; backCollider.convex = false; backCollider.sharedMesh = flippedMesh; geoBack.Add( backCollider ); } } private static Mesh FlipTriangles(Mesh mesh) { if (mesh == null) return null; Mesh flipped = new Mesh { vertices = mesh.vertices, uv = mesh.uv, uv2 = mesh.uv2, subMeshCount = mesh.subMeshCount }; for (int s = 0; s < mesh.subMeshCount; s++) { int[] triangles = mesh.GetTriangles(s); for (int i = 0; i < triangles.Length / 3; i++) { int a = triangles[i * 3 + 0]; int c = triangles[i * 3 + 2]; triangles[i * 3 + 0] = c; triangles[i * 3 + 2] = a; } flipped.SetTriangles(triangles, s); } flipped.RecalculateNormals(); return flipped; } #if USE_BURST_AND_MATH [BurstCompile] #endif private static void GetUniformPointsOnSphereNormalized( ref NativeArray samples ) { int count = samples.Length; float fPoints = count; float pi = PI; float inc = pi * (3f - sqrt(5f)); float off = 2f / fPoints; for (int k = 0; k < count; k++) { float y = k * off - 1f + (off / 2f); float r = sqrt(1f - y * y); float phi = k * inc; samples[k] = normalize(new vec3(cos(phi) * r, y, sin(phi) * r)); } } #if USE_BURST_AND_MATH [BurstCompile] #endif private static void GetRandomDirections( vec3 halfVoxel, uint seed, ref NativeArray samples) { int count = samples.Length; #if USE_BURST_AND_MATH Random random = new Random(seed); #endif for (int k = 0; k < count; k++) { #if USE_BURST_AND_MATH samples[k] = random.NextFloat3Direction() * halfVoxel; #else samples[k] = Vector3.Scale(Random.onUnitSphere,halfVoxel); #endif } } #if USE_BURST_AND_MATH private static readonly float PI = (float)math.PI; private static readonly float EPSILON = math.FLT_MIN_NORMAL; #else private const float PI = Mathf.PI; private static readonly float EPSILON = Mathf.Epsilon; #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] static float max(float a, float b) { #if USE_BURST_AND_MATH return math.max(a,b); #else return Mathf.Max(a, b); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] static float min(float a, float b) { #if USE_BURST_AND_MATH return math.min(a,b); #else return Mathf.Min(a, b); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] static int min(int a, int b) { #if USE_BURST_AND_MATH return math.min(a,b); #else return Mathf.Min(a, b); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] static float abs(float a) { #if USE_BURST_AND_MATH return math.abs(a); #else return Mathf.Abs(a); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] static float sqrt(float a) { #if USE_BURST_AND_MATH return math.sqrt(a); #else return Mathf.Sqrt(a); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] static float sin(float a) { #if USE_BURST_AND_MATH return math.sin(a); #else return Mathf.Sin(a); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] static float cos(float a) { #if USE_BURST_AND_MATH return math.cos(a); #else return Mathf.Cos(a); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] static float dot(vec3 a, vec3 b) { #if USE_BURST_AND_MATH return math.dot(a,b); #else return Vector3.Dot(a,b); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] static vec3 normalize(vec3 a) { #if USE_BURST_AND_MATH return math.normalize(a); #else return Vector3.Normalize(a); #endif } } }