/******************************************************************************/ /* Project - MudBun Publisher - Long Bunny Labs http://LongBunnyLabs.com Author - Ming-Lun "Allen" Chou http://AllenChou.net */ /******************************************************************************/ using System.Collections.Generic; using System.Linq; #if UNITY_EDITOR using UnityEditor; #endif using Unity.Collections; using Unity.Jobs; using UnityEngine; using UnityEngine.Profiling; namespace MudBun { /// /// A renderer generates and renders dynamic meshes based on the brushes under its transform hierarchy. ///

/// It can generate SDF texture generations at dynamically at run-time, which can be combined with Unity's VFX graph for things like GPU particle collision detection. ///

/// It also provides various CPU-based utilities that can used for gameplay purposes without needing GPU readbacks: /// /// SDF evaluation. /// SDF normal (normalized gradient) evaluation. /// Raycasts against mesh surface. /// ///

[ExecuteInEditMode] public class MudRenderer : MudRendererBase { public delegate void MeshGenerated(Mesh mesh); public static event MeshGenerated OnMeshGenerated; public override void InvokeOnMeshGenerated(Mesh mesh) { OnMeshGenerated?.Invoke(mesh); } protected override void OnSharedMaterialChanged(UnityEngine.Object material) { foreach (var renderer in s_renderers) { if (renderer.SharedMaterial == material) renderer.MarkNeedsCompute(); foreach (var b in renderer.Brushes) { var m = b.GetComponent(); if (m != null && m.SharedMaterial != null && m.SharedMaterial == material) b.MarkDirty(); } } } protected override void OnValidate() { base.OnValidate(); #if UNITY_EDITOR EditorApplication.QueuePlayerLoopUpdate(); #endif } protected override bool PreUpdateValidate() { if (!base.PreUpdateValidate()) return false; /* #if UNITY_EDITOR if (RenderMode == RenderModeEnum.RayMarchedSurface && RenderPipeline != ResourcesUtil.RenderPipelineEnum.URP) { Debug.LogWarning("The Ray-Marched Surface render mode is experimental and works in early URP only."); return false; } #endif */ return true; } public override void NotifyHierarchyChange() { base.NotifyHierarchyChange(); #if UNITY_EDITOR EditorApplication.QueuePlayerLoopUpdate(); #endif } // TODO: WIP /* override public void RectifyNonUnitScaledParents() { #if UNITY_EDITOR var goStack = new Stack(); var goList = new List(); // collect objects goStack.Push(gameObject); while (goStack.Count > 0) { var go = goStack.Pop(); goList.Add(go); for (int i = 0; i < go.transform.childCount; ++i) { var childGo = go.transform.GetChild(i).gameObject; goStack.Push(childGo); } } // record transforms var positionMap = new Dictionary(); var rotationMap = new Dictionary(); var scaleMap = new Dictionary(); foreach (var go in goList) { positionMap.Add(go, go.transform.position); rotationMap.Add(go, go.transform.rotation); scaleMap.Add(go, go.transform.localScale); } // rectify non-unit-scaled parents goStack.Push(gameObject); while (goStack.Count > 0) { var go = goStack.Pop(); bool isBrush = (go.GetComponent() != null); bool isUnitScaled = VectorUtil.MaxComp(go.transform.localScale) - VectorUtil.MinComp(go.transform.localScale) > MathUtil.Epsilon; bool shouldRectify = isBrush && !isUnitScaled && go.transform.childCount > 0; GameObject newParentGo = null; if (shouldRectify) { newParentGo = new GameObject(go.name + "(Rectified)"); newParentGo.transform.position = go.transform.position; newParentGo.transform.rotation = go.transform.rotation; newParentGo.transform.SetParent(go.transform.parent, true); newParentGo.transform.SetSiblingIndex(go.transform.GetSiblingIndex() + 1); } for (int i = 0; i < go.transform.childCount; ++i) { var childGo = go.transform.GetChild(i).gameObject; goStack.Push(childGo); if (shouldRectify) { childGo.transform.SetParent(newParentGo.transform, true); } } } // restore transforms foreach (var go in goList) { go.transform.position = positionMap[go]; go.transform.rotation = rotationMap[go]; go.transform.localScale = scaleMap[go]; } #endif } */ private T AddComponentHelper(GameObject go) where T : Component { var comp = go.GetComponent(); if (comp == null) { #if UNITY_EDITOR comp = Undo.AddComponent(go); #else comp = go.AddComponent(); #endif } else { #if UNITY_EDITOR Undo.RecordObject(comp, comp.name); #endif } if (m_addedComponents == null) m_addedComponents = new List(); var typeName = typeof(T).FullName; if (!m_addedComponents.Contains(typeName)) m_addedComponents.Add(typeName); return comp; } private void RemoveComponentHelper(GameObject go) where T : Component { // if not added, don't remove it var typeName = typeof(T).FullName; if (m_addedComponents == null || !m_addedComponents.Contains(typeName)) return; var comp = go.GetComponent(); if (comp != null) { #if UNITY_EDITOR Undo.DestroyObjectImmediate(comp); #else Destroy(comp); #endif } } public override Mesh AddCollider ( GameObject go, bool async, Mesh mesh = null, bool forceConvexCollider = false, bool makeRigidBody = false ) { var comp = AddComponentHelper(go); mesh = GenerateMesh(GeneratedMeshType.Collider, async, mesh); comp.sharedMesh = mesh; if (forceConvexCollider || makeRigidBody) { comp.convex = true; } if (makeRigidBody) { AddComponentHelper(go); } return mesh; } public override Mesh AddLockedStandardMesh ( GameObject go, bool autoRigging, bool async, Mesh mesh = null, bool generateTextureUV = false, bool generateLightMapUV = false, bool weldVertices = false, bool optimizeMeshForRendering = false ) { #if UNITY_EDITOR Undo.RecordObject(this, name); #endif var transformStack = new Stack(); transformStack.Push(transform); while (transformStack.Count > 0) { var t = transformStack.Pop(); if (t == null) continue; #if UNITY_EDITOR Undo.RecordObject(t, t.name); #endif for (int i = 0; i < t.childCount; ++i) transformStack.Push(t.GetChild(i)); } m_doRigging = autoRigging; Transform [] aBone; mesh = GenerateMesh(GeneratedMeshType.Standard, go.transform, out aBone, async, mesh, generateTextureUV, generateLightMapUV, weldVertices, optimizeMeshForRendering); m_doRigging = false; Material material = (m_lastLockedMeshMaterial == null) ? ResourcesUtil.DefaultLockedMeshMaterial : m_lastLockedMeshMaterial; if (autoRigging) { var meshRenderer = AddComponentHelper(go); meshRenderer.sharedMesh = mesh; meshRenderer.sharedMaterial = material; meshRenderer.bones = aBone; meshRenderer.rootBone = go.transform; } else { var meshFilter = AddComponentHelper(go); var meshRenderer = AddComponentHelper(go); meshFilter.sharedMesh = mesh; meshRenderer.sharedMaterial = material; } m_lastLockedMeshMaterial = material; #if UNITY_EDITOR EditorApplication.QueuePlayerLoopUpdate(); #endif return mesh; } private LockMeshIntermediateStateEnum m_lockMeshIntermediateState = LockMeshIntermediateStateEnum.Idle; protected override LockMeshIntermediateStateEnum LockMeshIntermediateState => m_lockMeshIntermediateState; [SerializeField] [HideInInspector] private List m_addedComponents; public override void LockMesh ( bool autoRigging, bool async, Mesh mesh = null, bool generateTextureUV = false, bool generateLightMapUV = false, bool weldVertices = false, bool optimizeMeshForRendering = false ) { m_lockMeshIntermediateState = LockMeshIntermediateStateEnum.PreLock; #if UNITY_EDITOR Undo.RecordObject(this, "Lock Mesh (" + name + ")"); #endif base.LockMesh(autoRigging, async, mesh, generateTextureUV, generateLightMapUV, weldVertices, optimizeMeshForRendering); #if UNITY_EDITOR Undo.FlushUndoRecordObjects(); #endif switch (MeshGenerationRenderableMeshMode) { case RenderableMeshMode.None: break; case RenderableMeshMode.Procedural: MarkNeedsCompute(); break; case RenderableMeshMode.MeshRenderer: AddLockedStandardMesh(gameObject, autoRigging, async, mesh, generateTextureUV, generateLightMapUV, weldVertices, optimizeMeshForRendering); if (!async) DisposeLocalResources(); break; } m_lockMeshIntermediateState = LockMeshIntermediateStateEnum.PostLock; } public override void UnlockMesh() { m_lockMeshIntermediateState = LockMeshIntermediateStateEnum.PreUnlock; #if UNITY_EDITOR Undo.RecordObject(this, "Unlock Mesh (" + name + ")"); #endif base.UnlockMesh(); #if UNITY_EDITOR Undo.FlushUndoRecordObjects(); #endif RemoveComponentHelper(gameObject); RemoveComponentHelper(gameObject); RemoveComponentHelper(gameObject); RemoveComponentHelper(gameObject); RemoveComponentHelper(gameObject); RemoveComponentHelper(gameObject); RemoveComponentHelper(gameObject); m_lockMeshIntermediateState = LockMeshIntermediateStateEnum.Idle; MeshGenerationLockOnStartByEditor = false; m_addedComponents = null; } protected override bool GenerateUV(Mesh mesh, bool generateTextureUV, bool generateLightMapUV) { #if UNITY_EDITOR if (generateTextureUV || generateLightMapUV) { Unwrapping.GenerateSecondaryUVSet(mesh); if (generateTextureUV) mesh.uv = mesh.uv2; if (!generateLightMapUV) mesh.uv2 = null; } return true; #else return false; #endif } //------------------------------------------------------------------------- private static NativeArray s_aSingleSampleSync; private static NativeArray s_aSingleRaySync; private static NativeArray s_aSingleResultSync; private static NativeArray s_aSingleContactSync; private static void InitSyncJobData() { s_aSingleSampleSync = new NativeArray(1, Allocator.Persistent); s_aSingleRaySync = new NativeArray(1, Allocator.Persistent); s_aSingleResultSync = new NativeArray(1, Allocator.Persistent); s_aSingleContactSync = new NativeArray(1, Allocator.Persistent); } private static void DisposeSyncJobData() { s_aSingleSampleSync.Dispose(); s_aSingleRaySync.Dispose(); s_aSingleResultSync.Dispose(); s_aSingleContactSync.Dispose(); } //------------------------------------------------------------------------- /// /// Generates an SDF texture into a RenderTexture object. /// /// The output texture. /// Point in renderer space mapped to the center of output texture. You can use the renderer's inverse transform to convert a point in world space into the renderer's local space. /// Dimensions in renderer space mapped to the size output texture. public override void GenerateSdf(RenderTexture sdf, Vector3 origin, Vector3 dimension) { base.GenerateSdf(sdf, origin, dimension); } /// /// Generates an SDF texture into a Texture3D object. /// /// The output texture. /// Point in renderer space mapped to the center of output texture. You can use the renderer's inverse transform to convert a point in world space into the renderer's local space. /// Dimensions in renderer space mapped to the size output texture. public override void GenerateSdf(Texture3D sdf, Vector3 origin, Vector3 dimension) { base.GenerateSdf(sdf, origin, dimension); } /// /// Synchronous CPU-based SDF evaluation that takes a single sample position and returns a single result. Function computes on the main thread and only returns when the entire computation is done. ///

/// The result is inaccurate for scaled renderers, and is likely to be less accurate when non-union/blended brushes are involved due to SDF approximation (that still works for meshing algorithms and SDF raycasts). In these cases, use the more expensive EvaluateSnapToSurface and EvaluateSnapToSurfaceAsync and compute the distance between the sample point and contact point. ///

/// The sample position. /// The range of evaluation from the sample position. Only brushes within this range are considered for evaluation. Use a conservative estimate of distance from the SDF's zero isosurface. /// Whether to compute the material at the sample point as well. /// Result containing the SDF value, plus material if computeMaterial is true. The normal field of the result would be left empty. public Sdf.Result EvaluateSdf(Vector3 p, float maxSurfaceDistance, bool computeMaterials) { UpdateComputeData(); s_aSingleSampleSync[0] = p; Sdf.EvaluateSdf(Sdf.AsyncMode.None, this, s_aSingleSampleSync, s_aSingleResultSync, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, maxSurfaceDistance, computeMaterials, SurfaceShift); return s_aSingleResultSync[0]; } /// /// Asynchronous CPU-based SDF evaluation. Function schedules a job that will compute on multiple threads. ///

/// Results are inaccurate for scaled renderers, and are likely to be less accurate when non-union/blended brushes are involved due to SDF approximation (that still works for meshing algorithms and SDF raycasts). In these cases, use the more expensive EvaluateSnapToSurface and EvaluateSnapToSurfaceAsync and compute the distances between the samples point and contact points. ///

/// You must call Complete() on the returned job handle no later than the next MudRenderer.LateUpdate, which could alter input data used by the evaluation job. ///

/// Array of sample position /// Array of output results containing the SDF values, plus materials if computeMaterial is true. /// The range of evaluation from the sample positions. Only brushes within this range are considered for evaluation. Use a conservative estimate of distance from the SDF's zero isosurface. /// Whether to compute the materials at sample points as well. /// A job handle that can be waited on for completion. The normal field of the result would be left empty. public Sdf.EvalJobHandle EvaluateSdfAsync(NativeArray samples, NativeArray results, float maxSurfaceDistance, bool computeMaterials) { UpdateComputeData(); var hJob = Sdf.EvaluateSdf(Sdf.AsyncMode.Async, this, samples, results, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, maxSurfaceDistance, computeMaterials, SurfaceShift); m_jobQueue.Add(hJob); return hJob; } /* public Sdf.EvalJobHandle EvaluateSdfAsyncInputCopied(NativeArray samples, NativeArray results, float maxSurfaceDistance, bool computeMaterials) { UpdateComputeData(); var hJob = Sdf.EvaluateSdf(Sdf.AsyncMode.AsyncInputCopied, this, samples, results, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, maxSurfaceDistance, computeMaterials, SurfaceShift); m_jobQueue.Add(hJob); return hJob; } */ /// /// Synchronous CPU-based SDF normal (normalized gradient) evaluation that takes a single sample position and returns a single result. Function computes on the main thread and only returns when the entire computation is done. /// /// The sample position. /// The range of evaluation from the sample position. Only brushes within this range are considered for evaluation. Use a conservative estimate of distance from the SDF's zero isosurface. /// Result containing the SDf normal. The SDF value field of the result would be left empty. public Sdf.Result EvaluateNormal(Vector3 p, float maxSurfaceDistance) { UpdateComputeData(); s_aSingleSampleSync[0] = p; Sdf.EvaluateNormal(Sdf.AsyncMode.None, this, s_aSingleSampleSync, s_aSingleResultSync, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, maxSurfaceDistance, SurfaceShift, 1e-3f); return s_aSingleResultSync[0]; } /// /// Asynchronous CPU-based SDF normal (normalized gradient) evaluation. Function schedules a job that will compute on multiple threads. ///

/// You must call Complete() on the returned job handle no later than the next MudRenderer.LateUpdate, which could alter input data used by the evaluation job. ///

/// Array of sample positions. /// Array of output results containing the SDF normals. /// Whether to compute the materials at sample points as well. /// A job handle that can be waited on for completion. The SDF value field of the result would be left empty. public Sdf.EvalJobHandle EvaluateNormalAsync(NativeArray samples, NativeArray results, float maxSurfaceDistance) { UpdateComputeData(); var hJob = Sdf.EvaluateNormal(Sdf.AsyncMode.Async, this, samples, results, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, maxSurfaceDistance, SurfaceShift, 1e-3f); m_jobQueue.Add(hJob); return hJob; } /* public Sdf.EvalJobHandle EvaluateNormalAsyncInputCopied(NativeArray samples, NativeArray results, float maxSurfaceDistance) { UpdateComputeData(); var hJob = Sdf.EvaluateNormal(Sdf.AsyncMode.AsyncInputCopied, this, samples, results, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, maxSurfaceDistance, SurfaceShift, 1e-3f); m_jobQueue.Add(hJob); return hJob; } */ /// /// Synchronous CPU-based SDF value and normal (normalized gradient) evaluation that takes a single sample position and returns a single result. Function computes on the main thread and only returns when the entire computation is done. ///

/// The SDF result is inaccurate for scaled renderers, and is likely to be less accurate when non-union/blended brushes are involved due to SDF approximation (that still works for meshing algorithms and SDF raycasts). In these cases, use the more expensive EvaluateSnapToSurface and EvaluateSnapToSurfaceAsync and compute the distance between the sample point and contact point. ///

/// The sample position /// The range of evaluation from the sample position. Only brushes within this range are considered for evaluation. Use a conservative estimate of distance from the SDF's zero isosurface. /// Whether to compute the materials at sample points as well. /// Result containing the SDF value and normal, plus material if computeMaterial is true. public Sdf.Result EvaluateSdfAndNormal(Vector3 p, float maxSurfaceDistance, bool computeMaterials) { UpdateComputeData(); s_aSingleSampleSync[0] = p; Sdf.EvaluateSdfAndNormal(Sdf.AsyncMode.None, this, s_aSingleSampleSync, s_aSingleResultSync, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, maxSurfaceDistance, computeMaterials, SurfaceShift, 1e-3f); return s_aSingleResultSync[0]; } /// /// Asynchronous CPU-based SDF value and normal (normalized gradient) evaluation. Function schedules a job that will compute on multiple threads. ///

/// SDF results are inaccurate for scaled renderers, and are likely to be less accurate when non-union/blended brushes are involved due to SDF approximation (that still works for meshing algorithms and SDF raycasts). In these cases, use the more expensive EvaluateSnapToSurface and EvaluateSnapToSurfaceAsync and compute the distances between the samples point and contact points. ///

/// You must call Complete() on the returned job handle no later than the next MudRenderer.LateUpdate, which could alter input data used by the evaluation job. ///

/// Array of sample positions /// Array of output results containing the SDF values and normals, plus materials if computeMaterial is true. /// The range of evaluation from the sample positions. Only brushes within this range are considered for evaluation. Use a conservative estimate of distance from the SDF's zero isosurface. /// Whether to compute the materials at sample points as well. /// A job handle that can be waited on for completion. public Sdf.EvalJobHandle EvaluateSdfAndNormalAsync(NativeArray samples, NativeArray results, float maxSurfaceDistance, bool computeMaterials) { UpdateComputeData(); var hJob = Sdf.EvaluateSdfAndNormal(Sdf.AsyncMode.Async, this, samples, results, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, maxSurfaceDistance, computeMaterials, SurfaceShift, 1e-3f); m_jobQueue.Add(hJob); return hJob; } /* public Sdf.EvalJobHandle EvaluateSdfAndNormalAsyncInputCopied(NativeArray samples, NativeArray results, float maxSurfaceDistance, bool computeMaterials) { UpdateComputeData(); var hJob = Sdf.EvaluateSdfAndNormal(Sdf.AsyncMode.AsyncInputCopied, this, samples, results, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, maxSurfaceDistance, computeMaterials, SurfaceShift, 1e-3f); m_jobQueue.Add(hJob); return hJob; } */ /// /// Synchronous CPU-based SDF raycast that takes a single ray and returns the raycast result. Function computes on the main thread and only returns when the entire computation is done. /// /// The starting point of the ray. /// The direction of the ray. /// The maximum travel distance the ray. /// Whether to compute the material at the contact point (if hit) as well. /// The maximum number of iterations to step the ray. If a hit hasn't been found after the maximum steps have been taken, then the raycast is a miss. /// The raycast is considered a hit if it ever reaches a point where the absolute SDF value is less than the margin. Smaller margin typically requires more steps to find a hit. /// Whether to force all brushes to be treated as if they have zero blends with union operators. Useful for when needing to raycast against each brush's raw shape (e.g. for click-selection of mostly subtractive brushes). /// Raycast result. public Sdf.Contact Raycast(Vector3 from, Vector3 direction, float maxDistance, bool computeMaterials, int maxSteps = 128, float margin = 1e-2f, bool forceZeroBlendUnion = false) { UpdateComputeData(); Sdf.Ray ray; ray.From = from; ray.Direction = direction; ray.MaxDistance = maxDistance; s_aSingleRaySync[0] = ray; Sdf.EvaluateRaycast(Sdf.AsyncMode.None, this, s_aSingleRaySync, s_aSingleContactSync, margin, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, computeMaterials, maxSteps, SurfaceShift, forceZeroBlendUnion); return s_aSingleContactSync[0]; } /// /// Asynchronous CPU-based raycasts. Function schedules a job that will compute on multiple threads. ///

/// You must call Complete() on the returned job handle no later than the next MudRenderer.LateUpdate, which could alter input data used by the evaluation job. ///

/// Array of rays. /// Array of raycast results. /// Whether to compute the materials at contact points (if hit) as well. /// The maximum number of iterations to step a ray. If a hit hasn't been found after the maximum steps have been taken, then the raycast is a miss. /// A raycast is considered a hit if it ever reaches a point where the absolute SDF value is less than the margin. Smaller margin typically requires more steps to find a hit. /// Whether to force all brushes to be treated as if they have zero blends with union operators. Useful for when needing to raycast against each brush's raw shape (e.g. for click-selection of mostly subtractive brushes). /// A job handle that can be waited on for completion. public Sdf.EvalJobHandle RaycastAsync(NativeArray casts, NativeArray results, bool computeMaterials, int maxSteps = 128, float margin = 1e-2f, bool forceZeroBlendUnion = false) { UpdateComputeData(); var hJob = Sdf.EvaluateRaycast(Sdf.AsyncMode.Async, this, casts, results, margin, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, computeMaterials, maxSteps, SurfaceShift, forceZeroBlendUnion); m_jobQueue.Add(hJob); return hJob; } /* public Sdf.EvalJobHandle RaycastAsyncInputCopied(NativeArray casts, NativeArray results, bool computeMaterials, int maxSteps = 128, float margin = 1e-2f, bool forceZeroBlendUnion = false) { UpdateComputeData(); var hJob = Sdf.EvaluateRaycast(Sdf.AsyncMode.AsyncInputCopied, this, casts, results, margin, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, computeMaterials, maxSteps, SurfaceShift, forceZeroBlendUnion); m_jobQueue.Add(hJob); return hJob; } */ /// /// Synchronous CPU-based SDF raycast chain takes a single chain of sample points and returns a raycast result. Function computes on the main thread and only returns when the entire computation is done. /// /// Array of points representing a series of consecutive rays that will be cast sequentially until a hit is found or if the array is exhausted. /// Whether to compute the materials at the contact point (if hit) as well. /// The maximum number of iterations to step all the rays. If a hit hasn't been found after the maximum steps have been taken, then the raycast chain is a miss. /// A raycast chain is considered a hit if it ever reaches a point where the absolute SDF value is less than the margin. Smaller margin typically requires more steps to find a hit. /// Raycast chain result. public Sdf.Contact RaycastChain(NativeArray chain, bool computeMaterials, int maxSteps = 512, float margin = 1e-2f) { UpdateComputeData(); Sdf.EvaluateRaycastChain(Sdf.AsyncMode.None, this, chain, s_aSingleContactSync, margin, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, computeMaterials, maxSteps, SurfaceShift); return s_aSingleContactSync[0]; } /// /// Asynchronous CPU-based raycast chain. Function schedules a job that will compute on multiple threads. ///

/// You must call Complete() on the returned job handle no later than the next MudRenderer.LateUpdate, which could alter input data used by the evaluation job. ///

/// Array of points representing a series of consecutive rays that will be cast sequentially until a hit is found or if the array is exhausted. /// Array of a single raycast result for the entire raycast chain. /// Whether to compute the materials at the contact point (if hit) as well. /// The maximum number of iterations to step all the rays. If a hit hasn't been found after the maximum steps have been taken, then the raycast chain is a miss. /// A raycast chain is considered a hit if it ever reaches a point where the absolute SDF value is less than the margin. Smaller margin typically requires more steps to find a hit. /// A job handle that can be waited on for completion. public Sdf.EvalJobHandle RaycastChainAsync(NativeArray chain, NativeArray result, bool computeMaterials, int maxSteps = 128, float margin = 1e-2f) { UpdateComputeData(); var hJob = Sdf.EvaluateRaycastChain(Sdf.AsyncMode.Async, this, chain, result, margin, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, computeMaterials, maxSteps, SurfaceShift); m_jobQueue.Add(hJob); return hJob; } /* public Sdf.EvalJobHandle RaycastChainAsyncInputCopied(NativeArray chain, NativeArray result, bool computeMaterials, int maxSteps = 128, float margin = 1e-2f) { UpdateComputeData(); var hJob = Sdf.EvaluateRaycastChain(Sdf.AsyncMode.AsyncInputCopied, this, chain, result, margin, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, computeMaterials, maxSteps, SurfaceShift); m_jobQueue.Add(hJob); return hJob; } */ /// /// Synchronous CPU-based SDF zero isosurface snapping that takes a single sample point and returns a potentially hit contact. Function computes on the main thread and only returns when the entire computation is done. ///

/// Snapping is done by first evaluating the normal (normalized gradient) at the sample point, and then raycasting towards the SDF zero isosurface. ///

/// The sample position. /// The range of evaluation from the sample position. Only brushes within this range are considered for evaluation. Use a conservative estimate of distance from the SDF's zero isosurface. /// Whether to compute the materials at the contact point (if snapping is successful) as well. /// The maximum number of iterations to step the ray. If a hit hasn't been found after the maximum steps have been taken, then the raycast is a miss. /// The raycast is considered a hit if it ever reaches a point where the absolute SDF value is less than the margin. Smaller margin typically requires more steps to find a hit. /// Surface snapping result. public Sdf.Contact SnapToSurface(Vector3 p, float maxSurfaceDistance, bool computeMaterials, int maxSteps = 128, float margin = 1e-2f) { UpdateComputeData(); var samples = new NativeArray(1, Allocator.Temp); var results = new NativeArray(1, Allocator.Temp); samples[0] = p; var hJob = Sdf.EvaluateSnapToSurface(Sdf.AsyncMode.None, this, samples, results, margin, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, computeMaterials, maxSurfaceDistance, maxSteps, SurfaceShift); hJob.Complete(); var result = results[0]; samples.Dispose(); results.Dispose(); return result; } /// /// Asynchronous CPU-based SDF zero isosurface snapping. Function schedules a job that will compute on multiple threads. ///

/// Snapping is done by first evaluating normals (normalized gradients) at the sample points, and then raycasting towards the SDF zero isosurface. ///

/// You must call Complete() on the returned job handle no later than the next MudRenderer.LateUpdate, which could alter input data used by the evaluation job. ///

/// Array of sample points. /// Array of surface snapping results. /// The range of evaluation from the sample positions. Only brushes within this range are considered for evaluation. Use a conservative estimate of distance from the SDF's zero isosurface. /// Whether to compute the materials at the contact point (if snapping is successful) as well. /// The maximum number of iterations to step a ray. If a hit hasn't been found after the maximum steps have been taken, then the raycast is a miss. /// A raycast is considered a hit if it ever reaches a point where the absolute SDF value is less than the margin. Smaller margin typically requires more steps to find a hit. /// A job handle that can be waited on for completion. public Sdf.EvalJobHandle SnapToSurfaceAsync(NativeArray samples, NativeArray results, float maxSurfaceDistance, bool computeMaterials, int maxSteps = 128, float margin = 1e-2f) { UpdateComputeData(); var hJob = Sdf.EvaluateSnapToSurface(Sdf.AsyncMode.Async, this, samples, results, margin, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, computeMaterials, maxSurfaceDistance, maxSteps, SurfaceShift); m_jobQueue.Add(hJob); return hJob; } /* public Sdf.EvalJobHandle SnapToSurfaceAsyncInputCopied(NativeArray samples, NativeArray results, float maxSurfaceDistance, bool computeMaterials, int maxSteps = 128, float margin = 1e-2f) { UpdateComputeData(); var hJob = Sdf.EvaluateSnapToSurface(Sdf.AsyncMode.AsyncInputCopied, this, samples, results, margin, m_aSdfBrush, m_numSdfBrushes, m_aSdfBrushMaterial, m_aabbTree.NodePods, m_aabbTree.Root, computeMaterials, maxSurfaceDistance, maxSteps, SurfaceShift); m_jobQueue.Add(hJob); return hJob; } */ /// /// Raycast against all renderers. /// /// The starting point of the ray. /// The direction of the ray. /// The maximum distance traveldd by the ray. /// Raycast result. public static Sdf.Contact Raycast(Vector3 from, Vector3 direction, float maxDistance) { Sdf.Contact ret = Sdf.Contact.New; foreach (var renderer in s_renderers) { var contact = ((MudRenderer) renderer).Raycast(from, direction, maxDistance, true); if (!contact.Hit) continue; if (ret.Hit && contact.GlobalT > ret.GlobalT) continue; ret = contact; } return ret; } internal static Sdf.Contact RaycastClickSelection(Vector3 from, Vector3 direction, float maxDistance) { Sdf.Contact ret = Sdf.Contact.New; foreach (var renderer in s_renderers) { switch (renderer.ClickSelection) { case ClickSelectionEnum.None: case ClickSelectionEnum.Gizmos: continue; } if (renderer.MeshLocked) continue; bool forceZeroBlendUnion = (renderer.ClickSelection == ClickSelectionEnum.RaycastForcedZeroBlendUnion); var contact = ((MudRenderer) renderer).Raycast(from, direction, maxDistance, true, 256, 1e-2f, forceZeroBlendUnion); if (!contact.Hit) continue; if (ret.Hit && contact.GlobalT > ret.GlobalT) continue; ret = contact; } return ret; } //------------------------------------------------------------------------- protected override void InitBeforeFirstRenderer() { base.InitBeforeFirstRenderer(); InitSyncJobData(); Sdf.InitAsyncJobData(); #if UNITY_EDITOR SelectionManager.Init(); #endif } protected override void CleanUpAfterLastRenderer() { base.CleanUpAfterLastRenderer(); DisposeSyncJobData(); Sdf.DisposeAsyncJobData(); #if UNITY_EDITOR SelectionManager.Dispose(); #endif } protected override void OnEnable() { base.OnEnable(); #if UNITY_EDITOR RegisterEditorEvents(); SelectionManager.Init(); #endif } protected override void OnDisable() { base.OnDisable(); #if UNITY_EDITOR UnregisterEditorEvents(); SelectionManager.NotifyRendererDisabled(this); #endif } #if UNITY_EDITOR protected override bool ValidateLocalResources() { bool res = base.ValidateLocalResources(); if (!res) return false; Profiler.BeginSample("ValidateLocalResources (Renderer)"); // clear all defaults if (RenderMaterialMesh == ResourcesUtilEditor.DefaultMeshMaterial) RenderMaterialMesh = null; if (RenderMaterialSplats == ResourcesUtilEditor.DefaultSplatMaterial) RenderMaterialSplats = null; if (RenderMaterialDecal == ResourcesUtilEditor.DefaultDecalMaterial) RenderMaterialDecal = null; /* if (RenderMaterialRayMarchedSurface == ResourcesUtilEditor.DefaultRayMarchedSurfaceMaterial) RenderMaterialRayMarchedSurface = null; */ // only assign default where/when necessary switch (RenderMode) { case RenderModeEnum.FlatMesh: case RenderModeEnum.SmoothMesh: if (RenderMaterialMesh == null) RenderMaterialMesh = ResourcesUtilEditor.DefaultMeshMaterial; break; case RenderModeEnum.CircleSplats: case RenderModeEnum.QuadSplats: if (RenderMaterialSplats == null) RenderMaterialSplats = ResourcesUtilEditor.DefaultSplatMaterial; break; case RenderModeEnum.Decal: if (RenderMaterialDecal == null) RenderMaterialDecal = ResourcesUtilEditor.DefaultDecalMaterial; break; /* case RenderModeEnum.RayMarchedSurface: if (RenderMaterialRayMarchedSurface == null) RenderMaterialRayMarchedSurface = ResourcesUtilEditor.DefaultRayMarchedSurfaceMaterial; break; */ } Profiler.EndSample(); return true; } protected override bool ShouldHighlightBrushFromSelection(MudBrushBase brush) { if (Selection.Contains(brush.gameObject)) return false; if (HoveredBrush == null) return false; if (HoveredBrush == brush) return true; if (!Selection.Contains(brush.Renderer.gameObject) && HoveredBrush.Renderer == brush.Renderer && !Selection.objects.Any(x => (x as GameObject)?.GetComponent()?.Renderer == brush.Renderer)) return true; /* if (Selection.Contains(brush.Renderer.gameObject)) { return (HoveredBrush == brush); } else { return (HoveredBrush.Renderer == brush.Renderer); } */ return false; } internal static MudBrushBase HoveredBrush; private void OnHierarchyChanged() { if (MeshLocked) return; NotifyHierarchyChange(); } private void OnEditorUpdate() { if (IsAnyMeshGenerationPending) EditorApplication.QueuePlayerLoopUpdate(); } private void OnVisibilityChanged() { bool needsCompute = false; foreach (var b in Brushes) { bool isHidden = SceneVisibilityManager.instance.IsHidden(b.gameObject); if (isHidden != b.Hidden) needsCompute = true; b.Hidden = isHidden; } if (needsCompute) { ForceCompute(); EditorApplication.QueuePlayerLoopUpdate(); } } private void OnSceneSaved(UnityEngine.SceneManagement.Scene scene) { MarkNeedsCompute(); } private void OnUndoPerformed() { MarkNeedsCompute(); } private void OnBeforeAssemblyReload() { DisposeGlobalResources(); DisposeLocalResources(); } private void OnAfterAssemblyReload() { } private void RegisterEditorEvents() { EditorApplication.hierarchyChanged += OnHierarchyChanged; EditorApplication.update += OnEditorUpdate; SceneVisibilityManager.visibilityChanged += OnVisibilityChanged; UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += OnSceneSaved; Undo.undoRedoPerformed += OnUndoPerformed; AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; } private void UnregisterEditorEvents() { EditorApplication.hierarchyChanged -= OnHierarchyChanged; EditorApplication.update -= OnEditorUpdate; SceneVisibilityManager.visibilityChanged -= OnVisibilityChanged; UnityEditor.SceneManagement.EditorSceneManager.sceneSaved -= OnSceneSaved; Undo.undoRedoPerformed -= OnUndoPerformed; AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } protected override bool IsEditorBusy() { if (EditorApplication.isCompiling) return true; if (EditorApplication.isUpdating) return true; return false; } public override void ReloadShaders() { base.ReloadShaders(); EditorApplication.QueuePlayerLoopUpdate(); SceneView.RepaintAll(); } bool BrushesSelected { get { foreach (var b in Brushes) if (b.IsSelected()) return true; return false; } } private void OnDrawGizmosSelected() { if (AlwaysDrawGizmos && BrushesSelected) return; DrawGizmos(); } private void OnDrawGizmos() { if (!AlwaysDrawGizmos && !BrushesSelected) return; DrawGizmos(); } private void DrawGizmos() { if (IsEditorBusy()) return; if (MeshLocked) return; Color prevColor = Gizmos.color; Gizmos.matrix = transform.localToWorldMatrix; foreach (var b in Brushes) { Gizmos.color = GizmosUtil.OutlineDefault; b.DrawGizmosRs(); } if (DrawRawBrushBounds) { Gizmos.color = Color.white; foreach (var b in Brushes) { Aabb bounds = b.BoundsRs; Gizmos.DrawWireCube(bounds.Center, bounds.Size); } } if (DrawComputeBrushBounds) { Gizmos.color = Color.yellow; m_aabbTree.ForEach(bounds => Gizmos.DrawWireCube(bounds.Center, bounds.Size)); } if (DrawVoxelNodes) { Gizmos.color = Color.gray; var aNumAllocated = new int[m_numNodesAllocatedBuffer.count]; m_numNodesAllocatedBuffer.GetData(aNumAllocated); int numTotalNodes = aNumAllocated[0]; var aNode = new VoxelNode[numTotalNodes]; m_nodePoolBuffer.GetData(aNode); var aNodeSize = NodeSizes; int iNode = 0; for (int depth = 0; depth <= VoxelNodeDepth; ++depth) { int numNodesInDepth = Mathf.Min(aNumAllocated[depth + 1], aNode.Length); if (DrawVoxelNodesDepth >= 0 && depth != DrawVoxelNodesDepth) { iNode += numNodesInDepth; continue; } float nodeSize = aNodeSize[depth]; for (int i = 0; i < numNodesInDepth && iNode < aNode.Length; ++i, ++iNode) { Gizmos.DrawWireCube(aNode[iNode].Center, DrawVoxelNodesScale * nodeSize * Vector3.one); } } } if (UseCutoffVolume) { Vector3 centerRs = CutoffVolumeCenter != null ? transform.InverseTransformPoint(CutoffVolumeCenter.position) : Vector3.zero; GizmosUtil.DrawWireBox(centerRs, CutoffVolumeSize, Quaternion.identity); } Gizmos.matrix = Matrix4x4.identity; if (DrawRenderBounds) { Gizmos.color = Color.cyan; Aabb bounds = RenderBounds; Gizmos.DrawWireCube(bounds.Center, bounds.Size); } if (DrawGenerateSdfGizmos) { Gizmos.color = Color.white; Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, transform.localScale); Gizmos.DrawWireSphere(GenerateSdfCenter, 0.1f); Gizmos.DrawWireCube(GenerateSdfCenter, GenerateSdfDimension); Gizmos.matrix = Matrix4x4.identity; } Gizmos.color = prevColor; } #endif } }