diff --git a/Assets/AirSticker/Runtime/AirSticker.asmdef b/Assets/AirSticker/Runtime/AirSticker.asmdef index 1bb58c0..2dcb3a2 100644 --- a/Assets/AirSticker/Runtime/AirSticker.asmdef +++ b/Assets/AirSticker/Runtime/AirSticker.asmdef @@ -1,3 +1,7 @@ { - "name": "AirSticker" + "name": "AirSticker", + "references": [ + "Unity.Burst", + "Unity.Mathematics" + ] } diff --git a/Assets/AirSticker/Runtime/Scripts/AirStickerProjector.cs b/Assets/AirSticker/Runtime/Scripts/AirStickerProjector.cs index c3c0915..5ee0505 100644 --- a/Assets/AirSticker/Runtime/Scripts/AirStickerProjector.cs +++ b/Assets/AirSticker/Runtime/Scripts/AirStickerProjector.cs @@ -1,12 +1,11 @@ -using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Threading; using AirSticker.Runtime.Scripts.Core; +using AirSticker.Runtime.Scripts.Core.Jobs; +using Unity.Jobs; using UnityEngine; using UnityEngine.Events; -using UnityEngine.Serialization; namespace AirSticker.Runtime.Scripts { @@ -52,42 +51,26 @@ private bool [SerializeField] private UnityEvent onFinishedLaunch; // The event is called when decal projection is finished. - private readonly Vector4[] _clipPlanes = new Vector4[(int)ClipPlane.Num]; - private List _broadPhaseConvexPolygonInfos = new List(); - private List _convexPolygonInfos; private DecalSpace _decalSpace; - // volatile is required because the worker thread writes these flags and the main thread polls them. - private volatile bool _executeLaunchingOnWorkerThread; - private volatile bool _workerThreadFailed; - // Main thread only: true while the worker thread's appended geometry has not been uploaded yet. - private bool _hasPendingAppendedGeometry; - /// - /// True while the worker thread of this projector is running. - /// - /// - /// Destroying this projector stops its coroutine but not the queued ThreadPool work item, - /// so DecalProjectorLauncher must not start the next launch while this flag is true. - /// - internal bool IsWorkerThreadRunning => _executeLaunchingOnWorkerThread; + // The handle of the currently scheduled job stage. The main thread polls it (or completes it + // synchronously when measuring). See IsWorkerThreadRunning for why the launcher needs it. + private JobHandle _currentJobHandle; + private bool _jobsScheduled; + // The pooled source mesh the current launch's jobs are reading, pinned so the pool does not dispose + // its NativeArrays while the jobs run. + private ReceiverConvexPolygonsMesh _currentSource; /// - /// Discard the geometry that the worker thread appended to the pooled decal meshes - /// if the launch was canceled before the geometry was uploaded. + /// True while this projector's jobs are scheduled and not yet completed. /// /// - /// Destroying the projector stops its coroutine but not the ThreadPool work item, - /// so without this rollback the canceled geometry would stay in the pooled DecalMesh - /// buffers and be uploaded by the next launch that shares the same decal mesh. - /// Must be called on the main thread after the worker thread has finished. + /// Destroying the projector stops its coroutine but not the scheduled jobs, which write to the + /// pipeline's pooled buffers. Starting the next launch in that state would corrupt those buffers, + /// so DecalProjectorLauncher must wait until the jobs finish. The C# instance is still readable + /// after the Unity object is destroyed. OnDestroy also completes the jobs to release the pool. /// - internal void RollbackAppendedGeometryIfPending() - { - if (!_hasPendingAppendedGeometry) return; - - foreach (var decalMesh in DecalMeshes) decalMesh.RollbackAppendedGeometry(); - _hasPendingAppendedGeometry = false; - } + internal bool IsWorkerThreadRunning => _jobsScheduled && !_currentJobHandle.IsCompleted; /// /// State of decal projector. @@ -95,7 +78,7 @@ internal void RollbackAppendedGeometryIfPending() public State NowState { get; private set; } = State.NotLaunch; /// - /// The list of decal mesh that has been generated by the projector. + /// The list of decal mesh that has been generated by the projector. /// public List DecalMeshes { get; } = new List(); @@ -106,8 +89,24 @@ private void Start() private void OnDestroy() { - // It may be deleted without completing the projection - // So we`ll delete it here too. + // The coroutine is stopped by the destruction, but the scheduled jobs are not, and they write to + // the pipeline's pooled buffers. Complete them here so the next launch can reuse the pool safely. + if (_jobsScheduled) + { + _currentJobHandle.Complete(); + _jobsScheduled = false; + } + + // Release the pinned source so the pool can dispose it. + if (_currentSource != null) + { + _currentSource.InUse = false; + _currentSource = null; + } + + // It may be deleted without completing the projection, so we finish it here too. + // No geometry rollback is needed: the new pipeline appends to the decal meshes only on the main + // thread after the jobs complete, so a canceled launch never leaves half-appended geometry. OnFinished(State.LaunchingCanceled); } @@ -149,232 +148,133 @@ private void OnFinished(State finishedState) onFinishedLaunch = null; } - private static Matrix4x4[][] CalculateMatricesPallet(SkinnedMeshRenderer[] skinnedMeshRenderers) - { - var boneMatricesPallet = new Matrix4x4[skinnedMeshRenderers.Length][]; - var skinnedMeshRendererNo = 0; - foreach (var skinnedMeshRenderer in skinnedMeshRenderers) - { - if (!skinnedMeshRenderer) continue; - if (skinnedMeshRenderer.rootBone != null) - { - var mesh = skinnedMeshRenderer.sharedMesh; - var numBone = skinnedMeshRenderer.bones.Length; - - var boneMatrices = new Matrix4x4[numBone]; - for (var boneNo = 0; boneNo < numBone; boneNo++) - boneMatrices[boneNo] = skinnedMeshRenderer.bones[boneNo].localToWorldMatrix - * mesh.bindposes[boneNo]; - - boneMatricesPallet[skinnedMeshRendererNo] = boneMatrices; - } - - skinnedMeshRendererNo++; - } - - return boneMatricesPallet; - } - /// /// Execute projection decal to mesh. /// /// /// This process is performed over multiple frames. - /// Projection completion can be monitored using callback functions or by checking the IsFinishedLaunch property. + /// Projection completion can be monitored using callback functions or by checking the NowState property. /// - /// private IEnumerator ExecuteLaunch() { InitializeOriginAxisInDecalSpace(); foreach (var receiverObject in receiverObjects) { - if (receiverObject == null || !receiverObject) - { - continue; - } - - AirStickerSystem.CollectEditDecalMeshes(DecalMeshes, receiverObject, decalMaterial, groupId); + if (receiverObject == null || !receiverObject) continue; - var skinnedMeshRenderers = receiverObject.GetComponentsInChildren(); - skinnedMeshRenderers = skinnedMeshRenderers.Where(s => s.name != "AirStickerRenderer").ToArray(); + var receiverDecalMeshes = new List(); + AirStickerSystem.CollectEditDecalMeshes(receiverDecalMeshes, receiverObject, decalMaterial, groupId); + DecalMeshes.AddRange(receiverDecalMeshes); + var skinnedMeshRenderers = receiverObject.GetComponentsInChildren() + .Where(s => s.name != "AirStickerRenderer").ToArray(); var terrains = receiverObject.GetComponentsInChildren(); if (AirStickerSystem.ReceiverObjectTrianglePolygonsPool.Contains(receiverObject) == false) { - // new receiver object. - _convexPolygonInfos = new List(); - // Collect triangle polygon data. + // New receiver object: extract its source triangle polygons (frame-sliced). + var resultHolder = new ReceiverConvexPolygonsMesh[1]; yield return AirStickerSystem.BuildTrianglePolygonsFromReceiverObject( - receiverObject.GetComponentsInChildren(), receiverObject.GetComponentsInChildren(), skinnedMeshRenderers, terrains, - _convexPolygonInfos); - AirStickerSystem.ReceiverObjectTrianglePolygonsPool.RegisterTrianglePolygons(receiverObject, - _convexPolygonInfos); + resultHolder); + + if (resultHolder[0] == null) + { + // A receiver mesh is not Read/Write enabled, so there is nothing to build. + OnFinished(State.LaunchingCanceled); + yield break; + } + + AirStickerSystem.ReceiverObjectTrianglePolygonsPool.RegisterTrianglePolygons( + receiverObject, resultHolder[0]); } if (!receiverObject) { - // Receiver object is already dead. - // So the process will be finished. OnFinished(State.LaunchingCanceled); yield break; } - #region Prepare to run on worker threads. - - _convexPolygonInfos = AirStickerSystem.GetTrianglePolygonsFromPool( - receiverObject); - - System.Diagnostics.Stopwatch swPrepare = null; - if (AirStickerPerformanceLog.Enabled) swPrepare = System.Diagnostics.Stopwatch.StartNew(); - - for (var polyNo = 0; polyNo < _convexPolygonInfos.Count; polyNo++) + var source = AirStickerSystem.GetTrianglePolygonsFromPool(receiverObject); + if (source == null) { - if (polyNo != 0 && polyNo % TrianglePolygonsFactory.MaxGeneratedPolygonPerFrame == 0) - { - // Maximum number of polygons processed per frame is MaxGeneratedPolygonPerFrame. - swPrepare?.Stop(); - yield return null; - if (!receiverObject) - { - // Receiver object died while waiting for the next frame. - OnFinished(State.LaunchingCanceled); - yield break; - } - - swPrepare?.Start(); - } - - _convexPolygonInfos[polyNo].ConvexPolygon.PrepareToRunOnWorkerThread(); + OnFinished(State.LaunchingCanceled); + yield break; } - if (swPrepare != null) - Debug.Log($"[AirSticker][Perf] PrepareToRunOnWorkerThread: {swPrepare.Elapsed.TotalMilliseconds:F2} ms ({_convexPolygonInfos.Count} polygons)"); - - // Calculate bone matrix pallet. - // This must be done after the prepare loop so that the bone matrices are sampled - // on the same frame that the worker thread starts, even if the loop spans frames. - var boneMatricesPallet = CalculateMatricesPallet(skinnedMeshRenderers); - - var transform1 = transform; - var projectorPosition = transform1.position; - // basePosition is center of the decal box. - var centerPositionOfDecalBox = projectorPosition + transform1.forward * (depth * 0.5f); - - #endregion // Prepare to run on worker threads. - - #region Run worker thread. + var pipeline = AirStickerSystem.JobPipeline; + var trans = transform; + // basePosition is the center of the decal box. + var centerPositionOfDecalBox = trans.position + trans.forward * (depth * 0.5f); + + // Segment 1: skinning + broad phase + clip (parallel jobs). + source.InUse = true; + _currentSource = source; + _currentJobHandle = pipeline.ScheduleClipStage( + source, centerPositionOfDecalBox, + _decalSpace.Ex, _decalSpace.Ey, _decalSpace.Ez, + width, height, depth, projectionBackside); + _jobsScheduled = true; + JobHandle.ScheduleBatchedJobs(); + yield return CompleteJobHandle("clip stage (skinning + broad phase + clip)"); - // Snapshot the decal mesh buffers so that the geometry appended by the worker - // thread can be discarded if this launch is canceled before the upload. - foreach (var decalMesh in DecalMeshes) decalMesh.SnapshotBufferSizes(); - _hasPendingAppendedGeometry = true; - - // Split Convex Polygon. - _executeLaunchingOnWorkerThread = true; - ThreadPool.QueueUserWorkItem(RunActionByWorkerThread, new Action(() => - { - try - { - System.Diagnostics.Stopwatch swSkinning = null; - if (AirStickerPerformanceLog.Enabled) swSkinning = System.Diagnostics.Stopwatch.StartNew(); - - var localToWorldMatrices = new Matrix4x4[3]; - var boneWeights = new BoneWeight[3]; - for (var polyNo = 0; polyNo < _convexPolygonInfos.Count; polyNo++) - _convexPolygonInfos[polyNo].ConvexPolygon.CalculatePositionsAndNormalsInWorldSpace( - boneMatricesPallet, localToWorldMatrices, boneWeights); - - System.Diagnostics.Stopwatch swBroadPhase = null; - if (swSkinning != null) - { - swSkinning.Stop(); - swBroadPhase = System.Diagnostics.Stopwatch.StartNew(); - } - - _broadPhaseConvexPolygonInfos = BroadPhaseConvexPolygonsDetection.Execute( - centerPositionOfDecalBox, - _decalSpace.Ez, - width, - height, - depth, - _convexPolygonInfos, - projectionBackside); - - System.Diagnostics.Stopwatch swClip = null; - if (swBroadPhase != null) - { - swBroadPhase.Stop(); - swClip = System.Diagnostics.Stopwatch.StartNew(); - } - - BuildClipPlanes(centerPositionOfDecalBox); - SplitConvexPolygonsByPlanes(); - AddTrianglePolygonsToDecalMeshFromConvexPolygons(centerPositionOfDecalBox); - - if (swClip != null) - { - swClip.Stop(); - Debug.Log("[AirSticker][Perf] WorkerThread: " - + $"skinning={swSkinning.Elapsed.TotalMilliseconds:F2}ms, " - + $"broadPhase={swBroadPhase.Elapsed.TotalMilliseconds:F2}ms, " - + $"clip+build={swClip.Elapsed.TotalMilliseconds:F2}ms " - + $"(input={_convexPolygonInfos.Count}, survived={_broadPhaseConvexPolygonInfos.Count})"); - } - } - catch (Exception e) - { - // Exceptions on the thread pool are swallowed silently, so log them here. - Debug.LogException(e); - _workerThreadFailed = true; - } - finally - { - // The flag must be reset even if an exception is thrown. - // Otherwise the coroutine and the launcher queue will be stalled forever. - _executeLaunchingOnWorkerThread = false; - } - })); - - #endregion // Run worker thread. - - // Waiting to worker thread. - while (_executeLaunchingOnWorkerThread) yield return null; - - if (_workerThreadFailed) + if (!receiverObject) { - // The worker thread failed, so discard the possibly inconsistent geometry - // that was appended to the decal meshes and cancel the launching. - RollbackAppendedGeometryIfPending(); + _jobsScheduled = false; + source.InUse = false; + _currentSource = null; OnFinished(State.LaunchingCanceled); yield break; } - foreach (var decalMesh in DecalMeshes) decalMesh.ExecutePostProcessingAfterWorkerThread(); - _hasPendingAppendedGeometry = false; + // Main-thread step between the segments: size the output from the clip result. + pipeline.CountBuild(source, receiverDecalMeshes); + + // Segment 2: build the appended geometry (serial job, off the main thread). + _currentJobHandle = pipeline.ScheduleBuildStage( + source, centerPositionOfDecalBox, _decalSpace.Ex, _decalSpace.Ey, + width, height, zOffsetInDecalSpace); + JobHandle.ScheduleBatchedJobs(); + yield return CompleteJobHandle("build stage (fan + uv + tangent)"); + _jobsScheduled = false; + source.InUse = false; + _currentSource = null; + + // Merge the built geometry into the decal meshes and upload them (main thread). + pipeline.ApplyToDecalMeshes(receiverDecalMeshes); + foreach (var decalMesh in receiverDecalMeshes) decalMesh.ExecutePostProcessingAfterWorkerThread(); } OnFinished(State.LaunchingCompleted); - _convexPolygonInfos = null; - yield return null; } /// - /// This function is called by worker thread. + /// Wait for the current job stage. When performance logging is enabled the handle is completed + /// synchronously so the actual job compute time is measured; otherwise the main thread polls the + /// handle across frames without blocking. /// - /// - private static void RunActionByWorkerThread(object action) + private IEnumerator CompleteJobHandle(string label) { - ((Action)action)(); + if (AirStickerPerformanceLog.Enabled) + { + var sw = System.Diagnostics.Stopwatch.StartNew(); + _currentJobHandle.Complete(); + sw.Stop(); + Debug.Log($"[AirSticker][Perf] {label}: {sw.Elapsed.TotalMilliseconds:F2} ms"); + } + else + { + while (!_currentJobHandle.IsCompleted) yield return null; + _currentJobHandle.Complete(); + } } /// - /// Create and add AirStickerProjector to the GameObject. + /// Create and add AirStickerProjector to the GameObject. /// /// Game object to which the component will be added. /// Receiver object to which decal is applied. @@ -454,7 +354,7 @@ public static void RemoveDecalMeshes( /// Start projection decal. /// /// - /// This processing is async, so the projection decal takes several frames to finish. + /// This processing is async, so the projection decal takes several frames to finish. /// If you want to monitor the decal projection process, should be using a callback function. /// public void Launch(UnityAction onFinishedLaunch) @@ -482,109 +382,5 @@ private void InitializeOriginAxisInDecalSpace() var trans = transform; _decalSpace = new DecalSpace(trans.right, trans.up, trans.forward * -1.0f); } - - private void AddTrianglePolygonsToDecalMeshFromConvexPolygons(Vector3 originPosInDecalSpace) - { - var convexPolygons = new List(); - foreach (var convexPolyInfo in _broadPhaseConvexPolygonInfos) - { - if (convexPolyInfo.IsOutsideClipSpace) continue; - - convexPolygons.Add(convexPolyInfo.ConvexPolygon); - } - - foreach (var decalMesh in DecalMeshes) - decalMesh.AddTrianglePolygonsToDecalMesh( - convexPolygons, - originPosInDecalSpace, - _decalSpace.Ex, - _decalSpace.Ey, - width, - height, - zOffsetInDecalSpace); - } - - private void SplitConvexPolygonsByPlanes() - { - // Convex polygons will be split by clip planes. - foreach (var clipPlane in _clipPlanes) - foreach (var convexPolyInfo in _broadPhaseConvexPolygonInfos) - { - // It is outside the clip planes, so skip it. - if (convexPolyInfo.IsOutsideClipSpace) continue; - - convexPolyInfo.ConvexPolygon.SplitAndRemoveByPlane( - clipPlane, out var isOutsideClipSpace); - convexPolyInfo.IsOutsideClipSpace = isOutsideClipSpace; - } - } - - private void BuildClipPlanes(Vector3 basePoint) - { - var basePointToNearClipDistance = depth * 0.5f; - var basePointToFarClipDistance = depth * 0.5f; - var decalSpaceTangentWs = _decalSpace.Ex; - var decalSpaceBiNormalWs = _decalSpace.Ey; - var decalSpaceNormalWs = _decalSpace.Ez; - // Build left plane. - _clipPlanes[(int)ClipPlane.Left] = new Vector4 - { - x = decalSpaceTangentWs.x, - y = decalSpaceTangentWs.y, - z = decalSpaceTangentWs.z, - w = width / 2.0f - Vector3.Dot(decalSpaceTangentWs, basePoint) - }; - // Build right plane. - _clipPlanes[(int)ClipPlane.Right] = new Vector4 - { - x = -decalSpaceTangentWs.x, - y = -decalSpaceTangentWs.y, - z = -decalSpaceTangentWs.z, - w = width / 2.0f + Vector3.Dot(decalSpaceTangentWs, basePoint) - }; - // Build bottom plane. - _clipPlanes[(int)ClipPlane.Bottom] = new Vector4 - { - x = decalSpaceBiNormalWs.x, - y = decalSpaceBiNormalWs.y, - z = decalSpaceBiNormalWs.z, - w = height / 2.0f - Vector3.Dot(decalSpaceBiNormalWs, basePoint) - }; - // Build top plane. - _clipPlanes[(int)ClipPlane.Top] = new Vector4 - { - x = -decalSpaceBiNormalWs.x, - y = -decalSpaceBiNormalWs.y, - z = -decalSpaceBiNormalWs.z, - w = height / 2.0f + Vector3.Dot(decalSpaceBiNormalWs, basePoint) - }; - // Build front plane. - _clipPlanes[(int)ClipPlane.Front] = new Vector4 - { - x = -decalSpaceNormalWs.x, - y = -decalSpaceNormalWs.y, - z = -decalSpaceNormalWs.z, - w = basePointToNearClipDistance + Vector3.Dot(decalSpaceNormalWs, basePoint) - }; - // Build back plane. - _clipPlanes[(int)ClipPlane.Back] = new Vector4 - { - x = decalSpaceNormalWs.x, - y = decalSpaceNormalWs.y, - z = decalSpaceNormalWs.z, - w = basePointToFarClipDistance - Vector3.Dot(decalSpaceNormalWs, basePoint) - }; - } - - private enum ClipPlane - { - Left, - Right, - Bottom, - Top, - Front, - Back, - Num - } } } diff --git a/Assets/AirSticker/Runtime/Scripts/AirStickerSystem.cs b/Assets/AirSticker/Runtime/Scripts/AirStickerSystem.cs index d7c6c46..6a4f2a9 100644 --- a/Assets/AirSticker/Runtime/Scripts/AirStickerSystem.cs +++ b/Assets/AirSticker/Runtime/Scripts/AirStickerSystem.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using AirSticker.Runtime.Scripts.Core; +using AirSticker.Runtime.Scripts.Core.Jobs; using UnityEngine; namespace AirSticker.Runtime.Scripts @@ -22,6 +23,12 @@ public sealed class AirStickerSystem : MonoBehaviour new ReceiverObjectTrianglePolygonsPool(); private TrianglePolygonsFactory _trianglePolygonsFactory; + private DecalMeshJobPipeline _jobPipeline; + + /// + /// The job pipeline that runs skinning / broad phase / clip / build for a launch. + /// + internal static DecalMeshJobPipeline JobPipeline => Instance ? Instance._jobPipeline : null; public static DecalProjectorLauncher DecalProjectorLauncher { @@ -58,6 +65,7 @@ private void Awake() "AirStickerSystem can't be instantiated multiply, but but it has already been instantiated."); Instance = this; _trianglePolygonsFactory = new TrianglePolygonsFactory(); + _jobPipeline = new DecalMeshJobPipeline(); } private void Update() @@ -71,31 +79,29 @@ private void OnDestroy() { _decalMeshPool.Dispose(); _trianglePolygonsFactory.Dispose(); + _jobPipeline.Dispose(); + _receiverObjectTrianglePolygonsPool.DisposeAll(); Instance = null; } internal static IEnumerator BuildTrianglePolygonsFromReceiverObject( - MeshFilter[] meshFilters, MeshRenderer[] meshRenderers, SkinnedMeshRenderer[] skinnedMeshRenderers, Terrain[] terrains, - List convexPolygonInfos) + ReceiverConvexPolygonsMesh[] resultHolder) { yield return Instance._trianglePolygonsFactory.BuildFromReceiverObject( - meshFilters, meshRenderers, skinnedMeshRenderers, terrains, - convexPolygonInfos); + resultHolder); } - internal static List GetTrianglePolygonsFromPool(GameObject receiverObject) + internal static ReceiverConvexPolygonsMesh GetTrianglePolygonsFromPool(GameObject receiverObject) { if (Instance == null) return null; - var convexPolygonInfos = Instance._receiverObjectTrianglePolygonsPool.ConvexPolygonsPool[receiverObject]; - foreach (var info in convexPolygonInfos) info.IsOutsideClipSpace = false; - return convexPolygonInfos; + return ReceiverObjectTrianglePolygonsPool.Get(receiverObject); } internal static void CollectEditDecalMeshes( diff --git a/Assets/AirSticker/Runtime/Scripts/Core/BroadPhaseConvexPolygonsDetection.cs b/Assets/AirSticker/Runtime/Scripts/Core/BroadPhaseConvexPolygonsDetection.cs deleted file mode 100644 index 7a247b4..0000000 --- a/Assets/AirSticker/Runtime/Scripts/Core/BroadPhaseConvexPolygonsDetection.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace AirSticker.Runtime.Scripts.Core -{ - /// - /// This class run broad phase convex polygons detection. - /// - internal static class BroadPhaseConvexPolygonsDetection - { - // Buffers below are reused across Execute() calls instead of being reallocated every time. - // This is safe because DecalProjectorLauncher only ever runs one launch (and therefore one - // Execute() call) at a time — it waits for AirStickerProjector.IsWorkerThreadRunning to become - // false even if the projector is destroyed in the middle of launching — and the buffers are - // fully consumed (copied out into DecalMesh) before the next Execute() call can start. - // Buffers only grow, they never shrink. - private static Vector3[] _pooledPositionBuffer; - private static Vector3[] _pooledNormalBuffer; - private static Vector3[] _pooledLocalPositionBuffer; - private static Vector3[] _pooledLocalNormalBuffer; - private static BoneWeight[] _pooledBoneWeightBuffer; - private static Line[] _pooledLineBuffer; - - private static void EnsureCapacity(ref T[] buffer, int requiredLength) - { - if (buffer != null && buffer.Length >= requiredLength) return; - var newLength = buffer == null ? requiredLength : Mathf.Max(requiredLength, buffer.Length * 2); - buffer = new T[newLength]; - } - - /// - /// Execute broad phase. - /// - /// - /// Remove polygons that don't intersect the sphere encompassing the decal box.
- /// Also, remove polygons whose mesh orientation is opposite the decal box.
- ///
- public static List Execute( - Vector3 centerPosOfDecalBox, - Vector3 decalSpaceNormalWs, - float decalBoxWidth, - float decalBoxHeight, - float decalBoxDepth, - List convexPolygonInfos, - bool projectionBackside) - { - // Calculate the radius of the sphere that encompasses the decal box. - var radius = Mathf.Sqrt(decalBoxWidth * decalBoxWidth - + decalBoxHeight * decalBoxHeight - + decalBoxDepth * decalBoxDepth) * 0.5f; - var sqrRadius = radius * radius; - - var broadPhaseConvexPolygonCount = 0; - for (var i = 0; i < convexPolygonInfos.Count; i++) - { - var convexPolygonInfo = convexPolygonInfos[i]; - var convexPolygon = convexPolygonInfo.ConvexPolygon; - if (!projectionBackside && Vector3.Dot(decalSpaceNormalWs, convexPolygon.FaceNormal) < 0) - { - // Set the flag of outside the clip space. - convexPolygonInfo.IsOutsideClipSpace = true; - continue; - } - - var v0 = convexPolygon.GetVertexPositionInWorldSpace( - convexPolygon.GetRealVertexNo(0)); - - // If the plane of the polygon doesn't intersect the sphere, - // the polygon doesn't intersect the sphere either, so it can be rejected cheaply. - var distToPlane = Vector3.Dot(convexPolygon.FaceNormal, centerPosOfDecalBox - v0); - if (distToPlane > radius || distToPlane < -radius) - { - // Set the flag of outside the clip space. - convexPolygonInfo.IsOutsideClipSpace = true; - continue; - } - - var v1 = convexPolygon.GetVertexPositionInWorldSpace( - convexPolygon.GetRealVertexNo(1)); - var v2 = convexPolygon.GetVertexPositionInWorldSpace( - convexPolygon.GetRealVertexNo(2)); - if (CalculateSqrDistancePointToTriangle(centerPosOfDecalBox, v0, v1, v2) > sqrRadius) - { - // Set the flag of outside the clip space. - convexPolygonInfo.IsOutsideClipSpace = true; - continue; - } - - broadPhaseConvexPolygonCount++; - } - - System.Diagnostics.Stopwatch swAlloc = null; - if (AirStickerPerformanceLog.Enabled) swAlloc = System.Diagnostics.Stopwatch.StartNew(); - - var broadPhaseConvexPolygonInfos = new List(broadPhaseConvexPolygonCount); - var requiredBufferLength = ConvexPolygon.DefaultMaxVertex * broadPhaseConvexPolygonCount; - EnsureCapacity(ref _pooledPositionBuffer, requiredBufferLength); - EnsureCapacity(ref _pooledNormalBuffer, requiredBufferLength); - EnsureCapacity(ref _pooledLocalPositionBuffer, requiredBufferLength); - EnsureCapacity(ref _pooledLocalNormalBuffer, requiredBufferLength); - EnsureCapacity(ref _pooledBoneWeightBuffer, requiredBufferLength); - EnsureCapacity(ref _pooledLineBuffer, requiredBufferLength); - var positionBuffer = _pooledPositionBuffer; - var normalBuffer = _pooledNormalBuffer; - var localPositionBuffer = _pooledLocalPositionBuffer; - var localNormalBuffer = _pooledLocalNormalBuffer; - var boneWeightBuffer = _pooledBoneWeightBuffer; - var lineBuffer = _pooledLineBuffer; - var startOffsetInBuffer = 0; - - if (swAlloc != null) - { - swAlloc.Stop(); - Debug.Log($"[AirSticker][Perf] BroadPhase buffer alloc: {swAlloc.Elapsed.TotalMilliseconds:F2} ms (survivors={broadPhaseConvexPolygonCount})"); - } - - for (var i = 0; i < convexPolygonInfos.Count; i++) - { - var convexPolygonInfo = convexPolygonInfos[i]; - if (!convexPolygonInfo.IsOutsideClipSpace) - { - broadPhaseConvexPolygonInfos.Add(new ConvexPolygonInfo - { - ConvexPolygon = new ConvexPolygon(convexPolygonInfo.ConvexPolygon, positionBuffer, - normalBuffer, boneWeightBuffer, lineBuffer, localPositionBuffer, localNormalBuffer, - startOffsetInBuffer), - IsOutsideClipSpace = convexPolygonInfo.IsOutsideClipSpace - }); - - startOffsetInBuffer += ConvexPolygon.DefaultMaxVertex; - } - - convexPolygonInfo.IsOutsideClipSpace = false; - } - - return broadPhaseConvexPolygonInfos; - } - - /// - /// Calculate the squared distance from the point to the triangle. - /// - /// - /// The closest point on the triangle is searched by determining which Voronoi region - /// of the triangle (the vertices, the edges or the face) the point is in.
- /// See "5.1.5 Closest Point on Triangle to Point" in "Real-Time Collision Detection" for details. - ///
- public static float CalculateSqrDistancePointToTriangle(Vector3 p, Vector3 a, Vector3 b, Vector3 c) - { - var ab = b - a; - var ac = c - a; - var ap = p - a; - - // Check if the point is in the vertex region of a. - var d1 = Vector3.Dot(ab, ap); - var d2 = Vector3.Dot(ac, ap); - if (d1 <= 0.0f && d2 <= 0.0f) return ap.sqrMagnitude; - - // Check if the point is in the vertex region of b. - var bp = p - b; - var d3 = Vector3.Dot(ab, bp); - var d4 = Vector3.Dot(ac, bp); - if (d3 >= 0.0f && d4 <= d3) return bp.sqrMagnitude; - - // Check if the point is in the edge region of ab. - var vc = d1 * d4 - d3 * d2; - if (vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f) - { - var v = d1 / (d1 - d3); - return (ap - ab * v).sqrMagnitude; - } - - // Check if the point is in the vertex region of c. - var cp = p - c; - var d5 = Vector3.Dot(ab, cp); - var d6 = Vector3.Dot(ac, cp); - if (d6 >= 0.0f && d5 <= d6) return cp.sqrMagnitude; - - // Check if the point is in the edge region of ac. - var vb = d5 * d2 - d1 * d6; - if (vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f) - { - var w = d2 / (d2 - d6); - return (ap - ac * w).sqrMagnitude; - } - - // Check if the point is in the edge region of bc. - var va = d3 * d6 - d5 * d4; - if (va <= 0.0f && d4 - d3 >= 0.0f && d5 - d6 >= 0.0f) - { - var w = (d4 - d3) / (d4 - d3 + (d5 - d6)); - return (bp - (c - b) * w).sqrMagnitude; - } - - // The point is in the face region. - var denom = 1.0f / (va + vb + vc); - return (ap - ab * (vb * denom) - ac * (vc * denom)).sqrMagnitude; - } - } -} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/BroadPhaseConvexPolygonsDetection.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/BroadPhaseConvexPolygonsDetection.cs.meta deleted file mode 100644 index 6b0c1fb..0000000 --- a/Assets/AirSticker/Runtime/Scripts/Core/BroadPhaseConvexPolygonsDetection.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 3e31e55f71ad06245b7109e726e469e9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/AirSticker/Runtime/Scripts/Core/ConvexPolygon.cs b/Assets/AirSticker/Runtime/Scripts/Core/ConvexPolygon.cs deleted file mode 100644 index c15e1eb..0000000 --- a/Assets/AirSticker/Runtime/Scripts/Core/ConvexPolygon.cs +++ /dev/null @@ -1,938 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using UnityEngine; -using UnityEngine.Assertions; - -namespace AirSticker.Runtime.Scripts.Core -{ - /// - /// Convex polygons with more vertices possible. - /// - public sealed class ConvexPolygon - { - public const int DefaultMaxVertex = 64; - private readonly BoneWeight[] _boneWeightBuffer; - private readonly bool _isSkinnedMeshRenderer; - private readonly Line[] _lineBuffer; - private readonly int _maxVertex; - private readonly Vector3[] _normalInModelSpaceBuffer; - private readonly Vector3[] _normalInWorldSpaceBuffer; - private readonly Vector3[] _positionInModelSpaceBuffer; - private readonly Vector3[] _positionInWorldSpaceBuffer; - private readonly int _rendererNo; - private readonly int _startOffsetInBuffer; - private bool _existsRootBone; - private Vector3 _faceNormal; - private Matrix4x4 _localToWorldMatrix; - - /// - /// Constructor.
- /// Buffers for the various arguments must be allocated for the required size.
- ///
- /// Buffer of vertex position in world space. - /// Buffer of vertex normal in world space. - /// Buffer of vertex bone weights. - /// Buffer of lines of convex polygons. - /// Buffer of vertex position in model space. - /// Buffer of vertex normal in model space. - /// Receiver mesh renderer which the decal mesh will be attached. - /// - /// offset of start index in buffer.
- /// The value of this variable is the starting position where the vertex information of this polygon is stored. - /// - /// Initial count of vertices of convex polygon - /// Number of receiver mesh renderer. - /// - /// Maximum number of vertices in a convex polygon.
- /// A convex polygon can be divided up to the value of this argument.
- /// If not specified, the value of DefaultMaxVertex is the maximum number of vertices.
- /// - public ConvexPolygon( - Vector3[] positionInWorldSpaceBuffer, - Vector3[] normalInWorldSpaceBuffer, - BoneWeight[] boneWeightBuffer, - Line[] lineBuffer, - Vector3[] positionInModelSpaceBuffer, - Vector3[] normalInModelSpaceBuffer, - Component receiverComponent, - int startOffsetInBuffer, - int initVertexCount, - int rendererNo, - int maxVertex = DefaultMaxVertex) - { - if (receiverComponent != null) - { - _isSkinnedMeshRenderer = receiverComponent is SkinnedMeshRenderer; - } - - _rendererNo = rendererNo; - _positionInModelSpaceBuffer = positionInModelSpaceBuffer; - _normalInModelSpaceBuffer = normalInModelSpaceBuffer; - _maxVertex = maxVertex; - _boneWeightBuffer = boneWeightBuffer; - _positionInWorldSpaceBuffer = positionInWorldSpaceBuffer; - _normalInWorldSpaceBuffer = normalInWorldSpaceBuffer; - _lineBuffer = lineBuffer; - _startOffsetInBuffer = startOffsetInBuffer; - ReceiverComponent = receiverComponent; - VertexCount = initVertexCount; - } - - /// - /// Copy Constructor.
- /// The contents of the various buffers held by the source convex polygon are copied only as needed, not all.
- /// Also, Buffers for the various arguments must be allocated for the required size.
- ///
- /// Convex polygon of copy source. - /// offset of start index in buffer. - /// - /// max vertex of convex polygon.
- /// Specify the maximum number of vertices when you want to change the number of vertices that can be divided.
- /// If the maximum number of vertices is less than the value of maxVertex in srcConvexPolygon, it is ignored.
- /// - /// Buffer of vertex position in world space. - /// Buffer of vertex normal in world space. - /// Buffer of vertex bone weights. - /// Buffer of lines of convex polygons. - /// Buffer of vertex position in model space. - /// Buffer of vertex normal in model space. - public ConvexPolygon(ConvexPolygon srcConvexPolygon, Vector3[] positionInWorldSpaceBuffer, - Vector3[] normalInWorldSpaceBuffer, BoneWeight[] boneWeightBuffer, Line[] lineBuffer, - Vector3[] positionInModelSpaceBuffer, Vector3[] normalInModelSpaceBuffer, int startOffsetInBuffer, - int maxVertex = DefaultMaxVertex) - { - ReceiverComponent = srcConvexPolygon.ReceiverComponent; - VertexCount = srcConvexPolygon.VertexCount; - - _isSkinnedMeshRenderer = srcConvexPolygon._isSkinnedMeshRenderer; - _maxVertex = Mathf.Max(maxVertex, srcConvexPolygon._maxVertex); - _rendererNo = srcConvexPolygon._rendererNo; - _positionInModelSpaceBuffer = positionInModelSpaceBuffer; - _normalInModelSpaceBuffer = normalInModelSpaceBuffer; - _positionInWorldSpaceBuffer = positionInWorldSpaceBuffer; - _normalInWorldSpaceBuffer = normalInWorldSpaceBuffer; - _boneWeightBuffer = boneWeightBuffer; - _lineBuffer = lineBuffer; - _startOffsetInBuffer = startOffsetInBuffer; - - Array.Copy(srcConvexPolygon._positionInWorldSpaceBuffer, srcConvexPolygon._startOffsetInBuffer, - _positionInWorldSpaceBuffer, _startOffsetInBuffer, srcConvexPolygon.VertexCount); - Array.Copy(srcConvexPolygon._normalInWorldSpaceBuffer, srcConvexPolygon._startOffsetInBuffer, - _normalInWorldSpaceBuffer, _startOffsetInBuffer, srcConvexPolygon.VertexCount); - - Array.Copy(srcConvexPolygon._positionInModelSpaceBuffer, srcConvexPolygon._startOffsetInBuffer, - _positionInModelSpaceBuffer, _startOffsetInBuffer, srcConvexPolygon.VertexCount); - Array.Copy(srcConvexPolygon._normalInModelSpaceBuffer, srcConvexPolygon._startOffsetInBuffer, - _normalInModelSpaceBuffer, _startOffsetInBuffer, srcConvexPolygon.VertexCount); - - Array.Copy(srcConvexPolygon._boneWeightBuffer, srcConvexPolygon._startOffsetInBuffer, - _boneWeightBuffer, _startOffsetInBuffer, srcConvexPolygon.VertexCount); - Array.Copy(srcConvexPolygon._lineBuffer, srcConvexPolygon._startOffsetInBuffer, - _lineBuffer, _startOffsetInBuffer, srcConvexPolygon.VertexCount); - - _faceNormal = srcConvexPolygon._faceNormal; - } - - public Vector3 FaceNormal => _faceNormal; - public int VertexCount { get; private set; } - - /// - /// The receiver component which the decal mesh will be attached. - /// - public Component ReceiverComponent { get; } - - private void CalculateNewVertexDataBySplitPlane( - out Vector3 newVert0, - out Vector3 newVert1, - out Vector3 newNormal0, - out Vector3 newNormal1, - out BoneWeight newBoneWeight0, - out BoneWeight newBoneWeight1, - out Vector3 newLocalVert0, - out Vector3 newLocalVert1, - out Vector3 newLocalNormal0, - out Vector3 newLocalNormal1, - Line l0, - Line l1, - Vector4 clipPlane) - { - var t = Vector4.Dot(clipPlane, Vector3ToVector4(l0.EndPosition)) - / Vector4.Dot(clipPlane, l0.StartToEndVec); - newVert0 = Vector3.Lerp(l0.EndPosition, l0.StartPosition, t); - newNormal0 = Vector3.Lerp(l0.EndNormal, l0.StartNormal, t); - newNormal0.Normalize(); - - newLocalVert0 = Vector3.Lerp(l0.EndLocalPosition, l0.StartLocalPosition, t); - newLocalNormal0 = Vector3.Lerp(l0.EndLocalNormal, l0.StartLocalNormal, t); - newLocalNormal0.Normalize(); - - t = Vector4.Dot(clipPlane, Vector3ToVector4(l1.StartPosition)) - / Vector4.Dot(clipPlane, l1.StartPosition - l1.EndPosition); - - newVert1 = Vector3.Lerp(l1.StartPosition, l1.EndPosition, t); - newNormal1 = Vector3.Lerp(l1.StartNormal, l1.EndNormal, t); - newNormal1.Normalize(); - - newLocalVert1 = Vector3.Lerp(l1.StartLocalPosition, l1.EndLocalPosition, t); - newLocalNormal1 = Vector3.Lerp(l1.StartLocalNormal, l1.EndLocalNormal, t); - newLocalNormal1.Normalize(); - - newBoneWeight0 = new BoneWeight(); - newBoneWeight1 = new BoneWeight(); - - newBoneWeight0 = l0.StartWeight; - newBoneWeight1 = l1.EndWeight; - - newBoneWeight0.weight0 = l0.StartWeight.boneIndex0 == l0.EndWeight.boneIndex0 - ? Mathf.Lerp(l0.EndWeight.weight0, l0.StartWeight.weight0, t) - : l0.StartWeight.weight0; - - newBoneWeight0.weight1 = l0.StartWeight.boneIndex1 == l0.EndWeight.boneIndex1 - ? Mathf.Lerp(l0.EndWeight.weight1, l0.StartWeight.weight1, t) - : l0.StartWeight.weight1; - - newBoneWeight0.weight2 = l0.StartWeight.boneIndex2 == l0.EndWeight.boneIndex2 - ? Mathf.Lerp(l0.EndWeight.weight2, l0.StartWeight.weight2, t) - : l0.StartWeight.weight2; - - newBoneWeight0.weight3 = l0.StartWeight.boneIndex3 == l0.EndWeight.boneIndex3 - ? Mathf.Lerp(l0.EndWeight.weight3, l0.StartWeight.weight3, t) - : l0.StartWeight.weight3; - - newBoneWeight0.boneIndex0 = l0.StartWeight.boneIndex0; - newBoneWeight0.boneIndex1 = l0.StartWeight.boneIndex1; - newBoneWeight0.boneIndex2 = l0.StartWeight.boneIndex2; - newBoneWeight0.boneIndex3 = l0.StartWeight.boneIndex3; - - newBoneWeight1.weight0 = l1.StartWeight.boneIndex0 == l1.EndWeight.boneIndex0 - ? Mathf.Lerp(l1.StartWeight.weight0, l1.EndWeight.weight0, t) - : l1.EndWeight.weight0; - - newBoneWeight1.weight1 = l1.StartWeight.boneIndex1 == l1.EndWeight.boneIndex1 - ? Mathf.Lerp(l1.StartWeight.weight1, l1.EndWeight.weight1, t) - : l1.EndWeight.weight1; - - newBoneWeight1.weight2 = l1.StartWeight.boneIndex2 == l1.EndWeight.boneIndex2 - ? Mathf.Lerp(l1.StartWeight.weight2, l1.EndWeight.weight2, t) - : l1.EndWeight.weight2; - - newBoneWeight1.weight3 = l1.StartWeight.boneIndex3 == l1.EndWeight.boneIndex3 - ? Mathf.Lerp(l1.StartWeight.weight3, l1.EndWeight.weight3, t) - : l1.EndWeight.weight3; - - newBoneWeight1.boneIndex0 = l1.EndWeight.boneIndex0; - newBoneWeight1.boneIndex1 = l1.EndWeight.boneIndex1; - newBoneWeight1.boneIndex2 = l1.EndWeight.boneIndex2; - newBoneWeight1.boneIndex3 = l1.EndWeight.boneIndex3; - - // Normalize bone weights. - var total = newBoneWeight0.weight0 + newBoneWeight0.weight1 + newBoneWeight0.weight2 + - newBoneWeight0.weight3; - if (total > 0.0f) - { - newBoneWeight0.weight0 /= total; - newBoneWeight0.weight1 /= total; - newBoneWeight0.weight2 /= total; - newBoneWeight0.weight3 /= total; - } - - total = newBoneWeight1.weight0 + newBoneWeight1.weight1 + newBoneWeight1.weight2 + - newBoneWeight1.weight3; - if (total > 0.0f) - { - newBoneWeight1.weight0 /= total; - newBoneWeight1.weight1 /= total; - newBoneWeight1.weight2 /= total; - newBoneWeight1.weight3 /= total; - } - } - - /// - /// Get the real vertex no from the virtual vertex no. - /// - /// - /// If you get the 0th vertex number, call GetRealVertexNo( 0 ), - /// and the function will return a real vertex number. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetRealVertexNo(int virtualVertNo) - { - Assert.IsTrue(virtualVertNo < _maxVertex, "The vertex number is over. MaxVertex should be checked."); - return _startOffsetInBuffer + virtualVertNo; - } - - /// - /// Get the vertex position in world space from the buffer. - /// - /// - /// real vertex no. - /// This value should be obtained using the GetRealVertexNo function. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector3 GetVertexPositionInWorldSpace(int vertNo) - { - return _positionInWorldSpaceBuffer[vertNo]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetVertexPositionInWorldSpace(int vertNo, Vector3 position) - { - _positionInWorldSpaceBuffer[vertNo] = position; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CopyVertexPositionInWorldSpace(int destVertNo, int srcVertNo) - { - _positionInWorldSpaceBuffer[destVertNo] = _positionInWorldSpaceBuffer[srcVertNo]; - } - - /// - /// Get the vertex position in the model space. - /// - /// - /// Real vertex no. - /// This value should be obtained using the GetRealVertexNo function. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector3 GetVertexPositionInModelSpace(int vertNo) - { - return _positionInModelSpaceBuffer[vertNo]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetVertexPositionInModelSpace(int vertNo, Vector3 position) - { - _positionInModelSpaceBuffer[vertNo] = position; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CopyVertexPositionInModelSpace(int destVertNo, int srcVertNo) - { - _positionInModelSpaceBuffer[destVertNo] = _positionInModelSpaceBuffer[srcVertNo]; - } - - /// - /// Get the vertex normal in the world space. - /// - /// - /// Real vertex no. - /// This value should be obtained using the GetRealVertexNo function. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector3 GetVertexNormalInWorldSpace(int vertNo) - { - return _normalInWorldSpaceBuffer[vertNo]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetVertexNormalInWorldSpace(int vertNo, Vector3 normal) - { - _normalInWorldSpaceBuffer[vertNo] = normal; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CopyVertexNormalInWorldSpace(int destVertNo, int srcVertNo) - { - _normalInWorldSpaceBuffer[destVertNo] = _normalInWorldSpaceBuffer[srcVertNo]; - } - - /// - /// Get the vertex normal in the model space. - /// - /// - /// Real vertex no. - /// This value should be obtained using the GetRealVertexNo function. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector3 GetVertexNormalInModelSpace(int vertNo) - { - return _normalInModelSpaceBuffer[vertNo]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetVertexNormalInModelSpace(int vertNo, Vector3 normal) - { - _normalInModelSpaceBuffer[vertNo] = normal; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CopyVertexNormalInModelSpace(int destVertNo, int srcVertNo) - { - _normalInModelSpaceBuffer[destVertNo] = _normalInModelSpaceBuffer[srcVertNo]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Line GetLine(int startVertNo) - { - return _lineBuffer[startVertNo]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ref Line GetLineRef(int startVertNo) - { - return ref _lineBuffer[startVertNo]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CopyLine(int destStartVertNo, int srcStartVertNo) - { - _lineBuffer[destStartVertNo] = _lineBuffer[srcStartVertNo]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector4 Vector3ToVector4(Vector3 v) - { - return new Vector4(v.x, v.y, v.z, 1.0f); - } - - /// - /// Split and remove to convex polygon by plane. - /// - /// - /// If a convex polygon can be divided by a plane, the division process is performed to create a new convex polygon. - /// Also, vertices outside (on the negative side) of the plane are discarded in this case. - /// For example, if a triangle is divided by a plane, it will be divided into a quadrangle and a triangle, but In this - /// case, - /// information about either of the polygons (polygons outside the plane) will be lost after the division. - /// Also, set allVertexIsOutside to true if all vertices that make up the convex polygon are outside the plane. - /// - /// - /// If all vertices of the convex polygon are outside the plane, True is set. - public void SplitAndRemoveByPlane(Vector4 clipPlane, out bool allVertexIsOutside) - { - allVertexIsOutside = false; - - var numOutsideVertex = 0; - var removeVertStartNo = -1; - var removeVertEndNo = 0; - var remainVertStartNo = -1; - var remainVertEndNo = 0; - for (var no = 0; no < VertexCount; no++) - { - var t = Vector4.Dot(clipPlane, Vector3ToVector4(GetVertexPositionInWorldSpace(GetRealVertexNo(no)))); - if (t < 0) - { - // outside. - if (removeVertStartNo == -1) removeVertStartNo = no; - - removeVertEndNo = no; - numOutsideVertex++; - } - else - { - // inside - if (remainVertStartNo == -1) remainVertStartNo = no; - - remainVertEndNo = no; - } - } - - if (numOutsideVertex == VertexCount) - { - // Since all vertices are outside the clip plane, no division can be performed. - allVertexIsOutside = true; - return; - } - - if (numOutsideVertex == 0) - // Since all vertices are inside the clip plane, no division can be performed. - return; - - // Split processing from here. - // Two new vertices are added at the intersection of the polygon's edges and planes. - // Also, since vertices outside the plane are excluded, the increase/decrease value of the polygon's vertices is 2 - numOutsideVertex. - var deltaVerticesSize = 2 - numOutsideVertex; - - if (removeVertStartNo == 0) - { - // Remove the 0th vertex. - // Two line - // Back up information on two lines that intersect with a plane. - var l0 = GetLine(GetRealVertexNo(remainVertStartNo - 1)); - var l1 = GetLine(GetRealVertexNo(remainVertEndNo)); - // Pack the remaining vertex forward. - var vertNo = 0; - for (var i = remainVertStartNo; i < remainVertEndNo + 1; i++) - { - var destVertNo = GetRealVertexNo(vertNo); - var srcVertNo = GetRealVertexNo(i); - CopyVertexPositionInWorldSpace(destVertNo, srcVertNo); - CopyVertexNormalInWorldSpace(destVertNo, srcVertNo); - CopyVertexPositionInModelSpace(destVertNo, srcVertNo); - CopyVertexNormalInModelSpace(destVertNo, srcVertNo); - CopyVertexBoneWeight(destVertNo, srcVertNo); - CopyLine(destVertNo, srcVertNo); - - vertNo++; - } - - CalculateNewVertexDataBySplitPlane( - out var newVert0, - out var newVert1, - out var newNormal0, - out var newNormal1, - out var newBoneWeight0, - out var newBoneWeight1, - out var newLocalVert0, - out var newLocalVert1, - out var newLocalNormal0, - out var newLocalNormal1, - l1, - l0, - clipPlane - ); - - // Added two vertex. - var newVertNo0_local = vertNo; - var newVertNo1_local = vertNo + 1; - - var newVertNo0 = GetRealVertexNo(newVertNo0_local); - var newVertNo1 = GetRealVertexNo(newVertNo1_local); - SetVertexPositionInWorldSpace(newVertNo0, newVert0); - SetVertexPositionInWorldSpace(newVertNo1, newVert1); - - SetVertexNormalInWorldSpace(newVertNo0, newNormal0); - SetVertexNormalInWorldSpace(newVertNo1, newNormal1); - - SetVertexPositionInModelSpace(newVertNo0, newLocalVert0); - SetVertexPositionInModelSpace(newVertNo1, newLocalVert1); - - SetVertexNormalInModelSpace(newVertNo0, newLocalNormal0); - SetVertexNormalInModelSpace(newVertNo1, newLocalNormal1); - - SetVertexBoneWeight(newVertNo0, newBoneWeight0); - SetVertexBoneWeight(newVertNo1, newBoneWeight1); - - // Build the lines. - VertexCount += deltaVerticesSize; - ref var line_0 = ref GetLineRef(GetRealVertexNo(newVertNo0_local - 1)); - ref var line_1 = ref GetLineRef(newVertNo0); - ref var line_2 = ref GetLineRef(newVertNo1); - line_0.SetEndAndCalcStartToEnd(newVert0, newNormal0, - newLocalVert0, newLocalNormal0); - line_1.SetStartEndAndCalcStartToEnd( - newVert0, - newVert1, - newNormal0, - newNormal1, - newLocalVert0, - newLocalVert1, - newLocalNormal0, - newLocalNormal1); - var endVertNo1 = GetRealVertexNo((newVertNo1_local + 1) % VertexCount); - line_2.SetStartEndAndCalcStartToEnd( - newVert1, - GetVertexPositionInWorldSpace(endVertNo1), - newNormal1, - GetVertexNormalInWorldSpace(endVertNo1), - newLocalVert1, - GetVertexPositionInModelSpace(endVertNo1), - newLocalNormal1, - GetVertexNormalInModelSpace(endVertNo1)); - - line_0.SetEndBoneWeight(newBoneWeight0); - line_1.SetStartEndBoneWeights(newBoneWeight0, newBoneWeight1); - line_2.SetStartEndBoneWeights( - newBoneWeight1, - GetVertexBoneWeight(endVertNo1)); - } - else - { - // Remove non 0th vertex. - // Back up information on two lines that intersect with a plane. - var l0 = GetLine(GetRealVertexNo(removeVertStartNo - 1)); - var l1 = GetLine(GetRealVertexNo(removeVertEndNo)); - if (deltaVerticesSize > 0) - // The vertex increases. - for (var i = VertexCount - 1; i > removeVertEndNo; i--) - { - var destVertNo = GetRealVertexNo(i + deltaVerticesSize); - var srcVertNo = GetRealVertexNo(i); - CopyVertexPositionInWorldSpace(destVertNo, srcVertNo); - CopyVertexNormalInWorldSpace(destVertNo, srcVertNo); - CopyVertexPositionInModelSpace(destVertNo, srcVertNo); - CopyVertexNormalInModelSpace(destVertNo, srcVertNo); - CopyVertexBoneWeight(destVertNo, srcVertNo); - CopyLine(destVertNo, srcVertNo); - } - else - // The vertex decrease or stays the same. - for (var i = removeVertEndNo + 1; i < VertexCount; i++) - { - var destVertNo = GetRealVertexNo(i + deltaVerticesSize); - var srcVertNo = GetRealVertexNo(i); - - CopyVertexPositionInWorldSpace(destVertNo, srcVertNo); - CopyVertexNormalInWorldSpace(destVertNo, srcVertNo); - CopyVertexPositionInModelSpace(destVertNo, srcVertNo); - CopyVertexNormalInModelSpace(destVertNo, srcVertNo); - CopyVertexBoneWeight(destVertNo, srcVertNo); - CopyLine(destVertNo, srcVertNo); - } - - // Add two vertex. - CalculateNewVertexDataBySplitPlane( - out var newVert0, - out var newVert1, - out var newNormal0, - out var newNormal1, - out var newBoneWeight0, - out var newBoneWeight1, - out var newLocalVert0, - out var newLocalVert1, - out var newLocalNormal0, - out var newLocalNormal1, - l0, - l1, - clipPlane); - var newVertNo0_local = removeVertStartNo; - var newVertNo1_local = removeVertStartNo + 1; - - var newVertNo0 = GetRealVertexNo(newVertNo0_local); - var newVertNo1 = GetRealVertexNo(newVertNo1_local); - SetVertexPositionInWorldSpace(newVertNo0, newVert0); - SetVertexPositionInWorldSpace(newVertNo1, newVert1); - - SetVertexNormalInWorldSpace(newVertNo0, newNormal0); - SetVertexNormalInWorldSpace(newVertNo1, newNormal1); - - SetVertexPositionInModelSpace(newVertNo0, newLocalVert0); - SetVertexPositionInModelSpace(newVertNo1, newLocalVert1); - - SetVertexNormalInModelSpace(newVertNo0, newLocalNormal0); - SetVertexNormalInModelSpace(newVertNo1, newLocalNormal1); - - SetVertexBoneWeight(newVertNo0, newBoneWeight0); - SetVertexBoneWeight(newVertNo1, newBoneWeight1); - - // Build the lines. - VertexCount += deltaVerticesSize; - ref var line_0 = ref GetLineRef(GetRealVertexNo(newVertNo0_local - 1)); - ref var line_1 = ref GetLineRef(newVertNo0); - ref var line_2 = ref GetLineRef(newVertNo1); - line_0.SetEndAndCalcStartToEnd(newVert0, newNormal0, - newLocalVert0, newLocalNormal0); - line_1.SetStartEndAndCalcStartToEnd( - newVert0, - newVert1, - newNormal0, - newNormal1, - newLocalVert0, - newLocalVert1, - newLocalNormal0, - newLocalNormal1); - - var endVertNo1_Direct = GetRealVertexNo((newVertNo1_local + 1) % VertexCount); - line_2.SetStartEndAndCalcStartToEnd( - newVert1, - GetVertexPositionInWorldSpace(endVertNo1_Direct), - newNormal1, - GetVertexNormalInWorldSpace(endVertNo1_Direct), - newLocalVert1, - GetVertexPositionInModelSpace(endVertNo1_Direct), - newLocalNormal1, - GetVertexNormalInModelSpace(endVertNo1_Direct)); - - line_0.SetEndBoneWeight(newBoneWeight0); - line_1.SetStartEndBoneWeights(newBoneWeight0, newBoneWeight1); - line_2.SetStartEndBoneWeights( - newBoneWeight1, - GetVertexBoneWeight(endVertNo1_Direct)); - } - } - - public bool IsIntersectRayToTriangle(out Vector3 hitPoint, Vector3 rayStartPos, Vector3 rayEndPos) - { - hitPoint = Vector3.zero; - if (VertexCount != 3) return false; - var vertNo0 = GetRealVertexNo(0); - var vertNo1 = GetRealVertexNo(1); - var vertNo2 = GetRealVertexNo(2); - var v0Pos = GetVertexPositionInWorldSpace(vertNo0); - var v1Pos = GetVertexPositionInWorldSpace(vertNo1); - var v2Pos = GetVertexPositionInWorldSpace(vertNo2); - - // Check to intersect plane to ray. - var v0ToRayStart = rayStartPos - v0Pos; - var v0ToRayEnd = rayEndPos - v0Pos; - var v0ToRayStartNorm = v0ToRayStart.normalized; - var v0ToRayEndNorm = v0ToRayEnd.normalized; - var t = Vector3.Dot(v0ToRayStartNorm, FaceNormal) - * Vector3.Dot(v0ToRayEndNorm, FaceNormal); - if (t < 0.0f) - { - // Hit. - // Next Calculate hit point. - var t0 = Mathf.Abs(Vector3.Dot(v0ToRayStart, FaceNormal)); - var t1 = Mathf.Abs(Vector3.Dot(v0ToRayEnd, FaceNormal)); - var intersectPoint = Vector3.Lerp(rayStartPos, rayEndPos, t0 / (t0 + t1)); - // Check to hit point is inside the triangle. - var v0ToIntersectPos = intersectPoint - v0Pos; - var v1ToIntersectPos = intersectPoint - v1Pos; - var v2ToIntersectPos = intersectPoint - v2Pos; - v0ToIntersectPos.Normalize(); - v1ToIntersectPos.Normalize(); - v2ToIntersectPos.Normalize(); - var v0ToV1 = GetLineRef(vertNo0).StartToEndVec; - var v1ToV2 = GetLineRef(vertNo1).StartToEndVec; - var v2ToV0 = GetLineRef(vertNo2).StartToEndVec; - v0ToV1.Normalize(); - v1ToV2.Normalize(); - v2ToV0.Normalize(); - - var a0 = Vector3.Cross(v0ToV1, v0ToIntersectPos); - var a1 = Vector3.Cross(v1ToV2, v1ToIntersectPos); - var a2 = Vector3.Cross(v2ToV0, v2ToIntersectPos); - a0.Normalize(); - a1.Normalize(); - a2.Normalize(); - - if (Vector3.Dot(a0, a1) > 0.0f - && Vector3.Dot(a0, a2) > 0.0f) - { - // Since the hit point is inside, intersected is determined - hitPoint = intersectPoint; - return true; - } - } - - return false; - } - - /// - /// Get the vertex bone weight from the buffer. - /// - /// - /// Real vertex no. - /// This value should be obtained using the GetRealVertexNo function. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public BoneWeight GetVertexBoneWeight(int vertNo) - { - return _boneWeightBuffer[vertNo]; - } - - /// - /// Set the vertex bone weight to the buffer. - /// - /// - /// Real vertex no. - /// This value should be obtained using the GetRealVertexNo function. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetVertexBoneWeight(int vertNo, BoneWeight boneWeight) - { - _boneWeightBuffer[vertNo] = boneWeight; - } - - /// - /// Copy the vertex bone weight to buffer. - /// - /// - /// the vertex no of destination. - /// real vertex no. - /// This value should be obtained using the GetRealVertexNo function. - /// - /// - /// the vertex no of source. - /// real vertex no. - /// This value should be obtained using the GetRealVertexNo function. - /// - private void CopyVertexBoneWeight(int destVertNo, int srcVertNo) - { - _boneWeightBuffer[destVertNo] = _boneWeightBuffer[srcVertNo]; - } - - private static void Multiply(ref Matrix4x4 mOut, Matrix4x4 m, float s) - { - mOut = m; - mOut.m00 *= s; - mOut.m01 *= s; - mOut.m02 *= s; - mOut.m03 *= s; - - mOut.m10 *= s; - mOut.m11 *= s; - mOut.m12 *= s; - mOut.m13 *= s; - - mOut.m20 *= s; - mOut.m21 *= s; - mOut.m22 *= s; - mOut.m23 *= s; - - mOut.m30 *= s; - mOut.m31 *= s; - mOut.m32 *= s; - mOut.m33 *= s; - } - - private static void MultiplyAdd(ref Matrix4x4 mOut, Matrix4x4 m, float s) - { - mOut.m00 += m.m00 * s; - mOut.m01 += m.m01 * s; - mOut.m02 += m.m02 * s; - mOut.m03 += m.m03 * s; - - mOut.m10 += m.m10 * s; - mOut.m11 += m.m11 * s; - mOut.m12 += m.m12 * s; - mOut.m13 += m.m13 * s; - - mOut.m20 += m.m20 * s; - mOut.m21 += m.m21 * s; - mOut.m22 += m.m22 * s; - mOut.m23 += m.m23 * s; - - mOut.m30 += m.m30 * s; - mOut.m31 += m.m31 * s; - mOut.m32 += m.m32 * s; - mOut.m33 += m.m33 * s; - } - - /// - /// Prepare to run on worker threads. - /// - /// - /// Some unity APIs aren't working on the worker threads. - /// Therefore, cache the necessary data in the worker thread. - /// - public void PrepareToRunOnWorkerThread() - { - if (ReceiverComponent is Renderer renderer) - { - // TODO: Do I need to use the Renderer's localWorldMatrix? - _localToWorldMatrix = renderer.localToWorldMatrix; - } - else if (ReceiverComponent is Terrain terrain) - { - _localToWorldMatrix = terrain.transform.localToWorldMatrix; - } - - if (_isSkinnedMeshRenderer) - { - var skinnedMeshRenderer = ReceiverComponent as SkinnedMeshRenderer; - _existsRootBone = skinnedMeshRenderer.rootBone != null; - } - else - { - _existsRootBone = false; - } - } - - public void CalculatePositionsAndNormalsInWorldSpace(Matrix4x4[][] boneMatricesPallet, - Matrix4x4[] localToWorldMatrices, BoneWeight[] boneWeights) - { - var vertNo0 = GetRealVertexNo(0); - var vertNo1 = GetRealVertexNo(1); - var vertNo2 = GetRealVertexNo(2); - if (_isSkinnedMeshRenderer) - { - if (_existsRootBone) - { - boneWeights[0] = GetVertexBoneWeight(vertNo0); - boneWeights[1] = GetVertexBoneWeight(vertNo1); - boneWeights[2] = GetVertexBoneWeight(vertNo2); - - var boneMatrices = boneMatricesPallet[_rendererNo]; - Multiply(ref localToWorldMatrices[0], - boneMatrices[boneWeights[0].boneIndex0], - boneWeights[0].weight0); - MultiplyAdd( - ref localToWorldMatrices[0], - boneMatrices[boneWeights[0].boneIndex1], - boneWeights[0].weight1); - MultiplyAdd( - ref localToWorldMatrices[0], - boneMatrices[boneWeights[0].boneIndex2], - boneWeights[0].weight2); - MultiplyAdd( - ref localToWorldMatrices[0], - boneMatrices[boneWeights[0].boneIndex3], - boneWeights[0].weight3); - - Multiply(ref localToWorldMatrices[1], - boneMatrices[boneWeights[1].boneIndex0], - boneWeights[1].weight0); - MultiplyAdd( - ref localToWorldMatrices[1], - boneMatrices[boneWeights[1].boneIndex1], - boneWeights[1].weight1); - MultiplyAdd( - ref localToWorldMatrices[1], - boneMatrices[boneWeights[1].boneIndex2], - boneWeights[1].weight2); - MultiplyAdd( - ref localToWorldMatrices[1], - boneMatrices[boneWeights[1].boneIndex3], - boneWeights[1].weight3); - Multiply(ref localToWorldMatrices[2], - boneMatrices[boneWeights[2].boneIndex0], - boneWeights[2].weight0); - MultiplyAdd( - ref localToWorldMatrices[2], - boneMatrices[boneWeights[2].boneIndex1], - boneWeights[2].weight1); - MultiplyAdd( - ref localToWorldMatrices[2], - boneMatrices[boneWeights[2].boneIndex2], - boneWeights[2].weight2); - MultiplyAdd( - ref localToWorldMatrices[2], - boneMatrices[boneWeights[2].boneIndex3], - boneWeights[2].weight3); - } - else - { - localToWorldMatrices[0] = _localToWorldMatrix; - localToWorldMatrices[1] = _localToWorldMatrix; - localToWorldMatrices[2] = _localToWorldMatrix; - } - } - else - { - localToWorldMatrices[0] = _localToWorldMatrix; - localToWorldMatrices[1] = _localToWorldMatrix; - localToWorldMatrices[2] = _localToWorldMatrix; - } - - SetVertexPositionInWorldSpace(vertNo0, - localToWorldMatrices[0].MultiplyPoint3x4(GetVertexPositionInModelSpace(vertNo0))); - SetVertexPositionInWorldSpace(vertNo1, - localToWorldMatrices[1].MultiplyPoint3x4(GetVertexPositionInModelSpace(vertNo1))); - SetVertexPositionInWorldSpace(vertNo2, - localToWorldMatrices[2].MultiplyPoint3x4(GetVertexPositionInModelSpace(vertNo2))); - - SetVertexNormalInWorldSpace(vertNo0, - localToWorldMatrices[0].MultiplyVector(GetVertexNormalInModelSpace(vertNo0))); - SetVertexNormalInWorldSpace(vertNo1, - localToWorldMatrices[1].MultiplyVector(GetVertexNormalInModelSpace(vertNo1))); - SetVertexNormalInWorldSpace(vertNo2, - localToWorldMatrices[2].MultiplyVector(GetVertexNormalInModelSpace(vertNo2))); - - for (var virtualVertNo = 0; virtualVertNo < VertexCount; virtualVertNo++) - { - var startVertNo = GetRealVertexNo(virtualVertNo); - var nextVertNo = GetRealVertexNo((virtualVertNo + 1) % VertexCount); - ref var line = ref GetLineRef(startVertNo); - line.Initialize( - GetVertexPositionInWorldSpace(startVertNo), - GetVertexPositionInWorldSpace(nextVertNo), - GetVertexNormalInWorldSpace(startVertNo), - GetVertexNormalInWorldSpace(nextVertNo), - GetVertexBoneWeight(startVertNo), - GetVertexBoneWeight(nextVertNo), - GetVertexPositionInModelSpace(startVertNo), - GetVertexPositionInModelSpace(nextVertNo), - GetVertexNormalInModelSpace(startVertNo), - GetVertexNormalInModelSpace(nextVertNo)); - } - - _faceNormal = Vector3.Cross( - GetVertexPositionInWorldSpace(vertNo1) - GetVertexPositionInWorldSpace(vertNo0), - GetVertexPositionInWorldSpace(vertNo2) - GetVertexPositionInWorldSpace(vertNo0)); - _faceNormal.Normalize(); - } - } -} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/ConvexPolygon.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/ConvexPolygon.cs.meta deleted file mode 100644 index 9e8b7ec..0000000 --- a/Assets/AirSticker/Runtime/Scripts/Core/ConvexPolygon.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 20a3e0dd725cfa04d8e2f9ba546c9d9b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/AirSticker/Runtime/Scripts/Core/DecalMesh.cs b/Assets/AirSticker/Runtime/Scripts/Core/DecalMesh.cs index 6ba966a..d9bd062 100644 --- a/Assets/AirSticker/Runtime/Scripts/Core/DecalMesh.cs +++ b/Assets/AirSticker/Runtime/Scripts/Core/DecalMesh.cs @@ -1,6 +1,7 @@ using System; -using System.Collections.Generic; using System.Runtime.InteropServices; +using Unity.Collections; +using Unity.Mathematics; using UnityEngine; using UnityEngine.Rendering; using Object = UnityEngine.Object; @@ -50,9 +51,7 @@ public sealed class DecalMesh : IDisposable private Mesh _mesh; private Vector3[] _normalBuffer; private int _numIndex; - private int _numIndexOnSnapshot; private int _numVertex; - private int _numVertexOnSnapshot; private Vector3[] _positionBuffer; private Vector4[] _tangentBuffer; private Vector2[] _uvBuffer; @@ -80,6 +79,7 @@ public DecalMesh( internal GameObject ReceiverObject => _receiverObject; internal Material DecalMaterial => _decalMaterial; + internal Component ReceiverComponent => _receiverComponent; public void Dispose() { @@ -219,51 +219,11 @@ public void Clear() _decalMeshRenderer?.Destroy(); _numIndex = 0; _numVertex = 0; - _numIndexOnSnapshot = 0; - _numVertexOnSnapshot = 0; Object.Destroy(_mesh); _decalMeshRenderer = null; _mesh = new Mesh(); } - /// - /// Snapshot the sizes of the CPU-side buffers. - /// Taken just before the worker thread appends geometry, - /// so that a canceled launch can be rolled back (see RollbackAppendedGeometry). - /// - internal void SnapshotBufferSizes() - { - _numVertexOnSnapshot = _numVertex; - _numIndexOnSnapshot = _numIndex; - } - - /// - /// Roll back the CPU-side buffers to the last snapshot. - /// - /// - /// A launch that is canceled after its worker thread ran (the projector was destroyed, - /// or the worker thread failed) leaves the appended geometry in the buffers without - /// uploading it. Because the decal mesh is pooled and shared, that geometry would be - /// uploaded by the next launch unless it is discarded here. - /// Must be called on the main thread after the worker thread has finished. - /// - internal void RollbackAppendedGeometry() - { - // Nothing was appended after the snapshot (or the mesh was cleared), so nothing to roll back. - if (_numVertex <= _numVertexOnSnapshot) return; - - _numVertex = _numVertexOnSnapshot; - _numIndex = _numIndexOnSnapshot; - // Keep the invariant that the buffer lengths equal the vertex/index counts, - // because the upload passes the whole arrays to the Unity Mesh API. - Array.Resize(ref _positionBuffer, _numVertex); - Array.Resize(ref _normalBuffer, _numVertex); - Array.Resize(ref _boneWeightsBuffer, _numVertex); - Array.Resize(ref _uvBuffer, _numVertex); - Array.Resize(ref _tangentBuffer, _numVertex); - Array.Resize(ref _indexBuffer, _numIndex); - } - /// /// Destroy the decal mesh renderer that was spawned under the receiver object. /// @@ -284,111 +244,51 @@ public void EnableDecalMeshRenderer() } /// - /// Add triangle polygons to decal mesh from convex polygons. + /// Append this launch's built geometry (produced by ) to the + /// CPU-side buffers. Called on the main thread after the build job has completed. /// - public void AddTrianglePolygonsToDecalMesh( - List convexPolygons, - Vector3 decalSpaceOriginPosInWorldSpace, - Vector3 decalSpaceTangentInWorldSpace, - Vector3 decalSpaceBiNormalInWorldSpace, - float decalSpaceWidth, - float decalSpaceHeight, - float zOffsetInDecalSpace - ) + /// + /// The indices in the job output are in the output array's space (they index Out* directly), so + /// they are shifted by (existing vertex count - vertexOffset) to reference this mesh's full vertex + /// buffer, which accumulates across launches. + /// + internal void AppendFromJobOutput( + NativeArray positions, + NativeArray normals, + NativeArray uvs, + NativeArray tangents, + NativeArray boneWeights, + NativeArray indices, + int vertexOffset, + int vertexCount, + int indexOffset, + int indexCount) { - if (!_receiverComponent) return; - - var uv = new Vector2(); - // Calculate the vertex count and the index count to be added. - var deltaVertex = 0; - var deltaIndex = 0; - foreach (var convexPolygon in convexPolygons) - { - if (convexPolygon.ReceiverComponent != _receiverComponent) continue; - deltaVertex += convexPolygon.VertexCount; - // Index count increases with the number of triangles*3 - deltaIndex += (convexPolygon.VertexCount - 2) * 3; - } + if (!_receiverComponent || vertexCount <= 0) return; + var indexDelta = _numVertex - vertexOffset; var addVertNo = _numVertex; var addIndexNo = _numIndex; - var indexBase = addVertNo; - var appendedVertexStart = _numVertex; - var appendedIndexStart = _numIndex; - // Expand the vertex buffer. - _numVertex += deltaVertex; + + _numVertex += vertexCount; Array.Resize(ref _positionBuffer, _numVertex); Array.Resize(ref _normalBuffer, _numVertex); Array.Resize(ref _boneWeightsBuffer, _numVertex); Array.Resize(ref _uvBuffer, _numVertex); Array.Resize(ref _tangentBuffer, _numVertex); - - // Expand the index buffer. - _numIndex += deltaIndex; - Array.Resize(ref _indexBuffer, _numIndex); - - foreach (var convexPolygon in convexPolygons) + for (var k = 0; k < vertexCount; k++) { - if (convexPolygon.ReceiverComponent != _receiverComponent) continue; - - var numVertex = convexPolygon.VertexCount; - for (var localVertNo = 0; localVertNo < numVertex; localVertNo++) - { - var vertNo = convexPolygon.GetRealVertexNo(localVertNo); - var vertPos = convexPolygon.GetVertexPositionInWorldSpace(vertNo); - - var decalSpaceToVertPos = vertPos - decalSpaceOriginPosInWorldSpace; - - uv.x = Vector3.Dot(decalSpaceTangentInWorldSpace, decalSpaceToVertPos) / decalSpaceWidth + 0.5f; - uv.y = Vector3.Dot(decalSpaceBiNormalInWorldSpace, decalSpaceToVertPos) / decalSpaceHeight + - 0.5f; - _uvBuffer[addVertNo] = uv; - // Convert position and rotation to parent space. - vertPos = convexPolygon.GetVertexPositionInModelSpace(vertNo); - var normal = convexPolygon.GetVertexNormalInModelSpace(vertNo); - - // Add a slight offset in the opposite direction of the decal projection to avoid Z-fighting. - // TODO: This number can be adjusted later. - vertPos += normal * zOffsetInDecalSpace; - _positionBuffer[addVertNo] = vertPos; - _normalBuffer[addVertNo] = normal; - _boneWeightsBuffer[addVertNo] = convexPolygon.GetVertexBoneWeight(vertNo); - addVertNo++; - } - - // The convex polygon is constructed by the number of vertices - 2 triangles. - var numTriangle = numVertex - 2; - for (var triNo = 0; triNo < numTriangle; triNo++) - { - _indexBuffer[addIndexNo++] = indexBase; - _indexBuffer[addIndexNo++] = indexBase + triNo + 1; - _indexBuffer[addIndexNo++] = indexBase + triNo + 2; - } - - indexBase += numVertex; + _positionBuffer[addVertNo + k] = positions[vertexOffset + k]; + _normalBuffer[addVertNo + k] = normals[vertexOffset + k]; + _uvBuffer[addVertNo + k] = uvs[vertexOffset + k]; + _tangentBuffer[addVertNo + k] = tangents[vertexOffset + k]; + _boneWeightsBuffer[addVertNo + k] = boneWeights[vertexOffset + k]; } - // Calculate the tangents of the appended geometry here on the worker thread, - // instead of calling Mesh.RecalculateTangents() on the main thread at upload time. - System.Diagnostics.Stopwatch swTangent = null; - if (AirStickerPerformanceLog.Enabled) swTangent = System.Diagnostics.Stopwatch.StartNew(); - - DecalMeshTangentCalculator.CalculateTangents( - _positionBuffer, - _normalBuffer, - _uvBuffer, - _indexBuffer, - _tangentBuffer, - appendedVertexStart, - deltaVertex, - appendedIndexStart, - deltaIndex); - - if (swTangent != null) - { - swTangent.Stop(); - Debug.Log($"[AirSticker][Perf] CalculateTangents (worker): {swTangent.Elapsed.TotalMilliseconds:F2} ms (vertices={deltaVertex})"); - } + _numIndex += indexCount; + Array.Resize(ref _indexBuffer, _numIndex); + for (var k = 0; k < indexCount; k++) + _indexBuffer[addIndexNo + k] = indices[indexOffset + k] + indexDelta; } [StructLayout(LayoutKind.Sequential)] diff --git a/Assets/AirSticker/Runtime/Scripts/Core/DecalProjectorLauncher.cs b/Assets/AirSticker/Runtime/Scripts/Core/DecalProjectorLauncher.cs index 3922af5..ae17535 100644 --- a/Assets/AirSticker/Runtime/Scripts/Core/DecalProjectorLauncher.cs +++ b/Assets/AirSticker/Runtime/Scripts/Core/DecalProjectorLauncher.cs @@ -42,27 +42,16 @@ private bool IsCurrentRequestFinished() if (_currentRequest == null) return true; // The request is empty. var projector = _currentRequest.Projector; - // Even if the projector is dead or the launching is canceled, the worker thread may still be - // running, because destroying the projector stops its coroutine but not the queued ThreadPool - // work item. Starting the next launch in that state corrupts the buffers shared between - // launches (e.g. the pooled buffers of BroadPhaseConvexPolygonsDetection), so the next - // request must wait until the worker thread finishes. + // Even if the projector is dead or the launching is canceled, its scheduled jobs may still be + // running, because destroying the projector stops its coroutine but not the jobs. Starting the + // next launch in that state corrupts the buffers the pipeline shares between launches, so the + // next request must wait until the jobs finish. // The C# instance is still readable after the Unity object is destroyed. if (projector.IsWorkerThreadRunning) return false; - var isFinished = !projector // Projector that threw the request is dead. - || projector.NowState == - AirStickerProjector.State.LaunchingCompleted // Launching is completed. - || projector.NowState == - AirStickerProjector.State.LaunchingCanceled; // Launching is canceled. - if (isFinished) - // If the projector died mid-launch, the geometry its worker thread appended to the - // pooled decal meshes has not been uploaded and must be discarded here, so that it - // doesn't show up in the next launch that shares the same decal meshes. - // (No-op when the launch completed normally or nothing was appended.) - projector.RollbackAppendedGeometryIfPending(); - - return isFinished; + return !projector // Projector that threw the request is dead. + || projector.NowState == AirStickerProjector.State.LaunchingCompleted + || projector.NowState == AirStickerProjector.State.LaunchingCanceled; } private void ProcessNextRequest() diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs.meta b/Assets/AirSticker/Runtime/Scripts/Core/Jobs.meta new file mode 100644 index 0000000..84ee698 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0c03e02eedda1fe45af2bee9a6681a33 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipJob.cs b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipJob.cs new file mode 100644 index 0000000..93777e9 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipJob.cs @@ -0,0 +1,85 @@ +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; + +namespace AirSticker.Runtime.Scripts.Core.Jobs +{ + /// + /// Clips every surviving triangle's convex polygon by the six decal box planes, in parallel. + /// + /// + /// Runs once per source triangle. Non-survivors (rejected by the broad phase) return immediately with + /// a vertex count of 0. A survivor is seeded into its fixed-stride clip slot from the skinned world + /// positions and the model-space source data, then split by the six planes in the same order as the + /// old pipeline (Left, Right, Bottom, Top, Front, Back). A polygon that is fully clipped away also + /// ends with a vertex count of 0. + /// + /// Each iteration only touches its own stride slot in the clip buffers, so the parallel-for + /// restriction is disabled on them; the slots are disjoint across iterations. + /// + /// + [BurstCompile] + internal struct ConvexPolygonClipJob : IJobParallelFor + { + [ReadOnly] public NativeArray SurviveFlags; + [ReadOnly] public NativeArray WorldPositions; + [ReadOnly] public NativeArray SourcePositionsMs; + [ReadOnly] public NativeArray SourceNormalsMs; + [ReadOnly] public NativeArray SourceBoneWeights; + + // The six clip planes (Left, Right, Bottom, Top, Front, Back). + [ReadOnly] public NativeArray ClipPlanes; + + [NativeDisableParallelForRestriction] public NativeArray ClipWorldPositions; + [NativeDisableParallelForRestriction] public NativeArray ClipModelPositions; + [NativeDisableParallelForRestriction] public NativeArray ClipModelNormals; + [NativeDisableParallelForRestriction] public NativeArray ClipBoneWeights; + + [WriteOnly] public NativeArray ClipVertexCounts; + + public void Execute(int triangleIndex) + { + if (SurviveFlags[triangleIndex] == 0) + { + ClipVertexCounts[triangleIndex] = 0; + return; + } + + var baseOffset = triangleIndex * DecalMeshJobBuffers.MaxVertexCountPerConvexPolygon; + var vertexBase = triangleIndex * ReceiverConvexPolygonsMesh.VerticesPerTriangle; + + // Seed the clip slot with the triangle's three vertices. + for (var k = 0; k < ReceiverConvexPolygonsMesh.VerticesPerTriangle; k++) + { + ClipWorldPositions[baseOffset + k] = WorldPositions[vertexBase + k]; + ClipModelPositions[baseOffset + k] = SourcePositionsMs[vertexBase + k]; + ClipModelNormals[baseOffset + k] = SourceNormalsMs[vertexBase + k]; + ClipBoneWeights[baseOffset + k] = SourceBoneWeights[vertexBase + k]; + } + + var buffers = new ClipVertexBuffers + { + WorldPositions = ClipWorldPositions, + ModelPositions = ClipModelPositions, + ModelNormals = ClipModelNormals, + BoneWeights = ClipBoneWeights + }; + + var vertexCount = ReceiverConvexPolygonsMesh.VerticesPerTriangle; + for (var planeNo = 0; planeNo < ClipPlanes.Length; planeNo++) + { + ConvexPolygonClipping.ClipByPlane( + buffers, baseOffset, ref vertexCount, ClipPlanes[planeNo], out var allVertexIsOutside); + if (allVertexIsOutside) + { + vertexCount = 0; + break; + } + } + + ClipVertexCounts[triangleIndex] = vertexCount; + } + } +} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipJob.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipJob.cs.meta new file mode 100644 index 0000000..6ce0b67 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipJob.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 97c89540da9e3d948a086817d75998d1 \ No newline at end of file diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipping.cs b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipping.cs new file mode 100644 index 0000000..71d19c4 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipping.cs @@ -0,0 +1,274 @@ +using Unity.Collections; +using Unity.Mathematics; +using UnityEngine; + +namespace AirSticker.Runtime.Scripts.Core.Jobs +{ + /// + /// Struct-of-arrays view of the clip working buffers for one convex polygon. + /// + /// + /// Holds only NativeArray handles, so it is cheap to pass by value and is Burst-compatible. + /// The per-vertex world normal is intentionally absent: it is dead data in the pipeline. + /// + internal struct ClipVertexBuffers + { + public NativeArray WorldPositions; + public NativeArray ModelPositions; + public NativeArray ModelNormals; + public NativeArray BoneWeights; + } + + /// + /// Splits a convex polygon by a plane in place, discarding the vertices on the negative side. + /// + /// + /// Faithful port of ConvexPolygon.SplitAndRemoveByPlane onto fixed-stride NativeArray slices. + /// Two differences from the original, both behavior-preserving: + /// + /// The Line buffer is gone. The two edges that straddle the plane are captured from + /// the vertex ring before the ring is mutated; every later plane recomputes its edges from the + /// ring on the fly. Because the ring is maintained exactly as before, this is identical to the + /// old cached lines. + /// The world-space per-vertex normal is not carried, because nothing downstream reads it. + /// + /// + internal static class ConvexPolygonClipping + { + private struct EdgeEndpoints + { + public float3 StartWorldPos; + public float3 EndWorldPos; + public float3 StartModelPos; + public float3 EndModelPos; + public float3 StartModelNormal; + public float3 EndModelNormal; + public BoneWeight StartWeight; + public BoneWeight EndWeight; + } + + private struct SplitVertex + { + public float3 WorldPos; + public float3 ModelPos; + public float3 ModelNormal; + public BoneWeight Weight; + } + + public static void ClipByPlane( + ClipVertexBuffers buffers, + int baseOffset, + ref int vertexCount, + float4 clipPlane, + out bool allVertexIsOutside) + { + allVertexIsOutside = false; + + var numOutsideVertex = 0; + var removeVertStartNo = -1; + var removeVertEndNo = 0; + var remainVertStartNo = -1; + var remainVertEndNo = 0; + for (var no = 0; no < vertexCount; no++) + { + var t = SignedDistance(clipPlane, buffers.WorldPositions[baseOffset + no]); + if (t < 0) + { + if (removeVertStartNo == -1) removeVertStartNo = no; + removeVertEndNo = no; + numOutsideVertex++; + } + else + { + if (remainVertStartNo == -1) remainVertStartNo = no; + remainVertEndNo = no; + } + } + + if (numOutsideVertex == vertexCount) + { + allVertexIsOutside = true; + return; + } + + if (numOutsideVertex == 0) return; + + // The polygon's vertex count changes by 2 - numOutsideVertex. + var deltaVerticesSize = 2 - numOutsideVertex; + + if (removeVertStartNo == 0) + { + // The 0th vertex is outside. The remaining (inside) vertices are packed to the front and the + // two intersection vertices are appended. + var enterEdge = CaptureEdge(buffers, baseOffset, vertexCount, remainVertStartNo - 1); + var leaveEdge = CaptureEdge(buffers, baseOffset, vertexCount, remainVertEndNo); + + var vertNo = 0; + for (var i = remainVertStartNo; i < remainVertEndNo + 1; i++) + { + CopyVertex(buffers, baseOffset, vertNo, i); + vertNo++; + } + + // Matches the original's CalculateNewVertexDataBySplitPlane(l1, l0): new0 from the leave edge, + // new1 from the enter edge. + CalculateNewVertices(leaveEdge, enterEdge, clipPlane, out var new0, out var new1); + + var newVertNo0Local = vertNo; + var newVertNo1Local = vertNo + 1; + WriteVertex(buffers, baseOffset, newVertNo0Local, new0); + WriteVertex(buffers, baseOffset, newVertNo1Local, new1); + + vertexCount += deltaVerticesSize; + } + else + { + var enterEdge = CaptureEdge(buffers, baseOffset, vertexCount, removeVertStartNo - 1); + var leaveEdge = CaptureEdge(buffers, baseOffset, vertexCount, removeVertEndNo); + + if (deltaVerticesSize > 0) + // The vertex count increases, so shift the tail up to make room. + for (var i = vertexCount - 1; i > removeVertEndNo; i--) + CopyVertex(buffers, baseOffset, i + deltaVerticesSize, i); + else + // The vertex count decreases or stays the same, so shift the tail down. + for (var i = removeVertEndNo + 1; i < vertexCount; i++) + CopyVertex(buffers, baseOffset, i + deltaVerticesSize, i); + + CalculateNewVertices(enterEdge, leaveEdge, clipPlane, out var new0, out var new1); + + var newVertNo0Local = removeVertStartNo; + var newVertNo1Local = removeVertStartNo + 1; + WriteVertex(buffers, baseOffset, newVertNo0Local, new0); + WriteVertex(buffers, baseOffset, newVertNo1Local, new1); + + vertexCount += deltaVerticesSize; + } + } + + private static EdgeEndpoints CaptureEdge(ClipVertexBuffers buffers, int baseOffset, int vertexCount, + int startVertNo) + { + var s = baseOffset + startVertNo; + var e = baseOffset + (startVertNo + 1) % vertexCount; + return new EdgeEndpoints + { + StartWorldPos = buffers.WorldPositions[s], + EndWorldPos = buffers.WorldPositions[e], + StartModelPos = buffers.ModelPositions[s], + EndModelPos = buffers.ModelPositions[e], + StartModelNormal = buffers.ModelNormals[s], + EndModelNormal = buffers.ModelNormals[e], + StartWeight = buffers.BoneWeights[s], + EndWeight = buffers.BoneWeights[e] + }; + } + + private static void CalculateNewVertices(EdgeEndpoints e0, EdgeEndpoints e1, float4 clipPlane, + out SplitVertex new0, out SplitVertex new1) + { + // new0 is the intersection on edge e0, interpolated from its End toward its Start. + var t = SignedDistance(clipPlane, e0.EndWorldPos) + / math.dot(clipPlane.xyz, e0.EndWorldPos - e0.StartWorldPos); + new0 = new SplitVertex + { + WorldPos = ClampedLerp(e0.EndWorldPos, e0.StartWorldPos, t), + ModelPos = ClampedLerp(e0.EndModelPos, e0.StartModelPos, t), + ModelNormal = DecalGeometryMath.NormalizeSafe(ClampedLerp(e0.EndModelNormal, e0.StartModelNormal, t)) + }; + + // new1 is the intersection on edge e1, interpolated from its Start toward its End. + t = SignedDistance(clipPlane, e1.StartWorldPos) + / math.dot(clipPlane.xyz, e1.StartWorldPos - e1.EndWorldPos); + new1 = new SplitVertex + { + WorldPos = ClampedLerp(e1.StartWorldPos, e1.EndWorldPos, t), + ModelPos = ClampedLerp(e1.StartModelPos, e1.EndModelPos, t), + ModelNormal = DecalGeometryMath.NormalizeSafe(ClampedLerp(e1.StartModelNormal, e1.EndModelNormal, t)) + }; + + // NOTE: the original reuses `t` (= e1's t) for BOTH bone-weight interpolations, not e0's t for + // new0. That is a quirk of the original code; it is replicated here to keep the output identical. + new0.Weight = InterpolateWeightFromStart(e0.StartWeight, e0.EndWeight, t); + new1.Weight = InterpolateWeightFromEnd(e1.StartWeight, e1.EndWeight, t); + } + + private static BoneWeight InterpolateWeightFromStart(BoneWeight start, BoneWeight end, float t) + { + var result = start; + result.weight0 = start.boneIndex0 == end.boneIndex0 ? ClampedLerp(end.weight0, start.weight0, t) : start.weight0; + result.weight1 = start.boneIndex1 == end.boneIndex1 ? ClampedLerp(end.weight1, start.weight1, t) : start.weight1; + result.weight2 = start.boneIndex2 == end.boneIndex2 ? ClampedLerp(end.weight2, start.weight2, t) : start.weight2; + result.weight3 = start.boneIndex3 == end.boneIndex3 ? ClampedLerp(end.weight3, start.weight3, t) : start.weight3; + result.boneIndex0 = start.boneIndex0; + result.boneIndex1 = start.boneIndex1; + result.boneIndex2 = start.boneIndex2; + result.boneIndex3 = start.boneIndex3; + return NormalizeWeight(result); + } + + private static BoneWeight InterpolateWeightFromEnd(BoneWeight start, BoneWeight end, float t) + { + var result = end; + result.weight0 = start.boneIndex0 == end.boneIndex0 ? ClampedLerp(start.weight0, end.weight0, t) : end.weight0; + result.weight1 = start.boneIndex1 == end.boneIndex1 ? ClampedLerp(start.weight1, end.weight1, t) : end.weight1; + result.weight2 = start.boneIndex2 == end.boneIndex2 ? ClampedLerp(start.weight2, end.weight2, t) : end.weight2; + result.weight3 = start.boneIndex3 == end.boneIndex3 ? ClampedLerp(start.weight3, end.weight3, t) : end.weight3; + result.boneIndex0 = end.boneIndex0; + result.boneIndex1 = end.boneIndex1; + result.boneIndex2 = end.boneIndex2; + result.boneIndex3 = end.boneIndex3; + return NormalizeWeight(result); + } + + private static BoneWeight NormalizeWeight(BoneWeight w) + { + var total = w.weight0 + w.weight1 + w.weight2 + w.weight3; + if (total > 0.0f) + { + w.weight0 /= total; + w.weight1 /= total; + w.weight2 /= total; + w.weight3 /= total; + } + + return w; + } + + private static void CopyVertex(ClipVertexBuffers buffers, int baseOffset, int dstLocal, int srcLocal) + { + var d = baseOffset + dstLocal; + var s = baseOffset + srcLocal; + buffers.WorldPositions[d] = buffers.WorldPositions[s]; + buffers.ModelPositions[d] = buffers.ModelPositions[s]; + buffers.ModelNormals[d] = buffers.ModelNormals[s]; + buffers.BoneWeights[d] = buffers.BoneWeights[s]; + } + + private static void WriteVertex(ClipVertexBuffers buffers, int baseOffset, int localVertNo, SplitVertex v) + { + var i = baseOffset + localVertNo; + buffers.WorldPositions[i] = v.WorldPos; + buffers.ModelPositions[i] = v.ModelPos; + buffers.ModelNormals[i] = v.ModelNormal; + buffers.BoneWeights[i] = v.Weight; + } + + // Vector4.Dot(plane, (pos, 1)) — the signed distance of pos from the plane (scaled by |plane.xyz|). + private static float SignedDistance(float4 plane, float3 pos) + { + return math.dot(plane.xyz, pos) + plane.w; + } + + // Matches UnityEngine.Vector3.Lerp / Mathf.Lerp, which clamp t to [0, 1]. + private static float3 ClampedLerp(float3 a, float3 b, float t) + { + return math.lerp(a, b, math.saturate(t)); + } + + private static float ClampedLerp(float a, float b, float t) + { + return math.lerp(a, b, math.saturate(t)); + } + } +} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipping.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipping.cs.meta new file mode 100644 index 0000000..b031fac --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ConvexPolygonClipping.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a5dc9c937b8782b4eb2f8815bb04d846 \ No newline at end of file diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalGeometryMath.cs b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalGeometryMath.cs new file mode 100644 index 0000000..5cf9eac --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalGeometryMath.cs @@ -0,0 +1,77 @@ +using Unity.Mathematics; + +namespace AirSticker.Runtime.Scripts.Core.Jobs +{ + /// + /// Pure geometry helpers shared by the decal mesh jobs. No Unity API objects, so the whole class is + /// Burst-compatible and can be unit-tested directly. + /// + internal static class DecalGeometryMath + { + /// + /// Normalize with the same semantics as UnityEngine.Vector3.normalized: return the zero + /// vector when the length is below Unity's epsilon, instead of producing NaN like math.normalize. + /// + /// + /// Matching this behavior keeps degenerate (zero-area) triangles from diverging between the old + /// CPU path and the job path. + /// + public static float3 NormalizeSafe(float3 v) + { + var length = math.length(v); + return length > 1e-5f ? v / length : float3.zero; + } + + /// + /// Squared distance from the point to the triangle (a, b, c). + /// + /// + /// Direct port of BroadPhaseConvexPolygonsDetection.CalculateSqrDistancePointToTriangle. + /// See "5.1.5 Closest Point on Triangle to Point" in "Real-Time Collision Detection". + /// + public static float CalculateSqrDistancePointToTriangle(float3 p, float3 a, float3 b, float3 c) + { + var ab = b - a; + var ac = c - a; + var ap = p - a; + + var d1 = math.dot(ab, ap); + var d2 = math.dot(ac, ap); + if (d1 <= 0.0f && d2 <= 0.0f) return math.lengthsq(ap); + + var bp = p - b; + var d3 = math.dot(ab, bp); + var d4 = math.dot(ac, bp); + if (d3 >= 0.0f && d4 <= d3) return math.lengthsq(bp); + + var vc = d1 * d4 - d3 * d2; + if (vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f) + { + var v = d1 / (d1 - d3); + return math.lengthsq(ap - ab * v); + } + + var cp = p - c; + var d5 = math.dot(ab, cp); + var d6 = math.dot(ac, cp); + if (d6 >= 0.0f && d5 <= d6) return math.lengthsq(cp); + + var vb = d5 * d2 - d1 * d6; + if (vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f) + { + var w = d2 / (d2 - d6); + return math.lengthsq(ap - ac * w); + } + + var va = d3 * d6 - d5 * d4; + if (va <= 0.0f && d4 - d3 >= 0.0f && d5 - d6 >= 0.0f) + { + var w = (d4 - d3) / (d4 - d3 + (d5 - d6)); + return math.lengthsq(bp - (c - b) * w); + } + + var denom = 1.0f / (va + vb + vc); + return math.lengthsq(ap - ab * (vb * denom) - ac * (vc * denom)); + } + } +} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalGeometryMath.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalGeometryMath.cs.meta new file mode 100644 index 0000000..66fd5ab --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalGeometryMath.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7c75b9466d422d847a3b4d7eb86e4221 \ No newline at end of file diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshBuildJob.cs b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshBuildJob.cs new file mode 100644 index 0000000..251de29 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshBuildJob.cs @@ -0,0 +1,121 @@ +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; + +namespace AirSticker.Runtime.Scripts.Core.Jobs +{ + /// + /// Builds the appended decal geometry for every decal mesh from the clipped convex polygons. + /// + /// + /// A single serial job (not parallel) so the triangle-fan output offsets can be accumulated naturally, + /// the same way the old DecalMesh.AddTrianglePolygonsToDecalMesh did on the worker thread. It + /// runs off the main thread to keep the fan expansion, UV projection and tangent calculation off the + /// main thread (this is the property Step 2 established and must be preserved). + /// + /// Output layout: the arrays hold only this launch's appended geometry for all decal meshes, + /// concatenated, with decal mesh dm occupying [VertexOffsets[dm], VertexOffsets[dm]+count). + /// Indices are written in this output space (i.e. they index Out* directly), so the tangent + /// calculation can run on the output arrays. When the main thread merges the output into a decal + /// mesh's persistent buffer it shifts the indices by (existing vertex count - VertexOffsets[dm]). + /// + /// + [BurstCompile] + internal struct DecalMeshBuildJob : IJob + { + public int TriangleCount; + public int DecalMeshCount; + + // Source / clip data. + [ReadOnly] public NativeArray TriangleComponentIndices; + [ReadOnly] public NativeArray ClipWorldPositions; + [ReadOnly] public NativeArray ClipModelPositions; + [ReadOnly] public NativeArray ClipModelNormals; + [ReadOnly] public NativeArray ClipBoneWeights; + [ReadOnly] public NativeArray ClipVertexCounts; + + // Per decal mesh (length == DecalMeshCount). + [ReadOnly] public NativeArray DecalMeshComponentIndices; + [ReadOnly] public NativeArray DecalMeshVertexOffsets; + [ReadOnly] public NativeArray DecalMeshIndexOffsets; + + // Decal space projection parameters. + public float3 DecalSpaceOriginWs; + public float3 DecalSpaceTangentWs; + public float3 DecalSpaceBiNormalWs; + public float DecalWidth; + public float DecalHeight; + public float ZOffsetInDecalSpace; + + // Appended geometry outputs. Not WriteOnly because the tangent stage reads positions/normals/uvs/indices. + public NativeArray OutPositions; + public NativeArray OutNormals; + public NativeArray OutUvs; + public NativeArray OutTangents; + public NativeArray OutBoneWeights; + public NativeArray OutIndices; + + // Tangent accumulation scratch, length >= the largest decal mesh's appended vertex count. + public NativeArray TangentAccumulation; + public NativeArray BitangentAccumulation; + + public void Execute() + { + for (var dm = 0; dm < DecalMeshCount; dm++) + { + var componentIndex = DecalMeshComponentIndices[dm]; + var appendedVertexStart = DecalMeshVertexOffsets[dm]; + var appendedIndexStart = DecalMeshIndexOffsets[dm]; + + var vertWrite = appendedVertexStart; + var indexWrite = appendedIndexStart; + // Index base in the output space: indices reference Out* directly (see the class remarks). + var indexBase = appendedVertexStart; + + for (var tri = 0; tri < TriangleCount; tri++) + { + var vertexCount = ClipVertexCounts[tri]; + if (vertexCount < 3) continue; + if (TriangleComponentIndices[tri] != componentIndex) continue; + + var slotBase = tri * DecalMeshJobBuffers.MaxVertexCountPerConvexPolygon; + for (var k = 0; k < vertexCount; k++) + { + var src = slotBase + k; + + var toVertWs = ClipWorldPositions[src] - DecalSpaceOriginWs; + OutUvs[vertWrite] = new float2( + math.dot(DecalSpaceTangentWs, toVertWs) / DecalWidth + 0.5f, + math.dot(DecalSpaceBiNormalWs, toVertWs) / DecalHeight + 0.5f); + + var normal = ClipModelNormals[src]; + // Push the vertex slightly against the projection direction to avoid Z-fighting. + OutPositions[vertWrite] = ClipModelPositions[src] + normal * ZOffsetInDecalSpace; + OutNormals[vertWrite] = normal; + OutBoneWeights[vertWrite] = ClipBoneWeights[src]; + vertWrite++; + } + + var numTriangle = vertexCount - 2; + for (var t = 0; t < numTriangle; t++) + { + OutIndices[indexWrite++] = indexBase; + OutIndices[indexWrite++] = indexBase + t + 1; + OutIndices[indexWrite++] = indexBase + t + 2; + } + + indexBase += vertexCount; + } + + var appendedVertexCount = vertWrite - appendedVertexStart; + var appendedIndexCount = indexWrite - appendedIndexStart; + DecalMeshTangents.ComputeTangents( + OutPositions, OutNormals, OutUvs, OutIndices, OutTangents, + TangentAccumulation, BitangentAccumulation, + appendedVertexStart, appendedVertexCount, appendedIndexStart, appendedIndexCount); + } + } + } +} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshBuildJob.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshBuildJob.cs.meta new file mode 100644 index 0000000..9e30898 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshBuildJob.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 42f8be710ceafe247b8d9acbde3640c9 \ No newline at end of file diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobBuffers.cs b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobBuffers.cs new file mode 100644 index 0000000..68ccb32 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobBuffers.cs @@ -0,0 +1,110 @@ +using System; +using Unity.Collections; +using Unity.Mathematics; +using UnityEngine; + +namespace AirSticker.Runtime.Scripts.Core.Jobs +{ + /// + /// Per-launch working buffers of the decal mesh job pipeline, pooled to avoid per-launch allocations. + /// + /// + /// Reusing the buffers across launches is safe because DecalProjectorLauncher runs only one + /// launch at a time and each launch's jobs complete (and are copied out into the decal meshes) before + /// the next launch starts. The buffers only grow; they are released when the AirStickerSystem is + /// destroyed. This replaces the static pool that BroadPhaseConvexPolygonsDetection used, and its + /// lifetime is tied to the system so the memory is not held forever (old residual task 1). + /// + /// The clip buffers are sized by the source triangle count (not the survivor count), so no compaction + /// pass is needed between the skinning and clip jobs. Dropping the per-vertex world normal and the + /// Line data shrank each vertex from ~252 B to 68 B, so even sized by triangle count these buffers are + /// smaller than the old survivor-sized pool. + /// + internal sealed class DecalMeshJobBuffers : IDisposable + { + /// + /// Maximum number of vertices a convex polygon can grow to while being clipped by the six planes. + /// Matches the fixed stride of the clip working buffers. + /// + public const int MaxVertexCountPerConvexPolygon = 64; + + private bool _disposed; + + // --- Per source triangle vertex / triangle (skinning job outputs) --- + // World-space positions of every triangle vertex (triangleCount * 3). + public NativeArray WorldPositions; + // 1 if the triangle survives the broad phase, 0 otherwise. + public NativeArray SurviveFlags; + + // --- Per receiver component (sized by componentCount / total bone count) --- + public NativeArray ComponentLocalToWorld; + public NativeArray ComponentExistsRootBone; + // Start index into BoneMatrices for each skinned component, or -1 if the component has no bone palette. + public NativeArray ComponentBoneMatrixOffset; + public NativeArray BoneMatrices; + + // --- Clip working set (sized by triangleCount; each triangle owns a fixed stride slot) --- + public NativeArray ClipWorldPositions; + public NativeArray ClipModelPositions; + public NativeArray ClipModelNormals; + public NativeArray ClipBoneWeights; + // Final vertex count of each triangle's convex polygon after clipping. 0 means the polygon was + // rejected by the broad phase or fully clipped away, i.e. it contributes no geometry. + public NativeArray ClipVertexCounts; + + public void EnsurePerTriangleCapacity(int triangleCount) + { + var vertexCount = triangleCount * ReceiverConvexPolygonsMesh.VerticesPerTriangle; + var clipVertexCount = triangleCount * MaxVertexCountPerConvexPolygon; + EnsureCapacity(ref WorldPositions, vertexCount); + EnsureCapacity(ref SurviveFlags, triangleCount); + EnsureCapacity(ref ClipWorldPositions, clipVertexCount); + EnsureCapacity(ref ClipModelPositions, clipVertexCount); + EnsureCapacity(ref ClipModelNormals, clipVertexCount); + EnsureCapacity(ref ClipBoneWeights, clipVertexCount); + EnsureCapacity(ref ClipVertexCounts, triangleCount); + } + + public void EnsureComponentCapacity(int componentCount, int boneMatrixCount) + { + EnsureCapacity(ref ComponentLocalToWorld, math.max(1, componentCount)); + EnsureCapacity(ref ComponentExistsRootBone, math.max(1, componentCount)); + EnsureCapacity(ref ComponentBoneMatrixOffset, math.max(1, componentCount)); + EnsureCapacity(ref BoneMatrices, math.max(1, boneMatrixCount)); + } + + private static void EnsureCapacity(ref NativeArray buffer, int requiredLength) where T : struct + { + if (buffer.IsCreated && buffer.Length >= requiredLength) return; + + var newLength = buffer.IsCreated + ? math.max(requiredLength, buffer.Length * 2) + : math.max(requiredLength, 1); + if (buffer.IsCreated) buffer.Dispose(); + buffer = new NativeArray(newLength, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + DisposeIfCreated(ref WorldPositions); + DisposeIfCreated(ref SurviveFlags); + DisposeIfCreated(ref ComponentLocalToWorld); + DisposeIfCreated(ref ComponentExistsRootBone); + DisposeIfCreated(ref ComponentBoneMatrixOffset); + DisposeIfCreated(ref BoneMatrices); + DisposeIfCreated(ref ClipWorldPositions); + DisposeIfCreated(ref ClipModelPositions); + DisposeIfCreated(ref ClipModelNormals); + DisposeIfCreated(ref ClipBoneWeights); + DisposeIfCreated(ref ClipVertexCounts); + } + + private static void DisposeIfCreated(ref NativeArray buffer) where T : struct + { + if (buffer.IsCreated) buffer.Dispose(); + } + } +} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobBuffers.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobBuffers.cs.meta new file mode 100644 index 0000000..39e8ca5 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobBuffers.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4cfadb634193f0040987696981490102 \ No newline at end of file diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobPipeline.cs b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobPipeline.cs new file mode 100644 index 0000000..b2c6cbe --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobPipeline.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; + +namespace AirSticker.Runtime.Scripts.Core.Jobs +{ + /// + /// Drives the decal mesh job pipeline for one receiver object: skinning + broad phase, clip, and mesh + /// build. Owned by AirStickerSystem and reused across launches (one launch runs at a time). + /// + /// + /// Usage from the projector's coroutine (two async segments with a cheap main-thread step between): + /// + /// var h1 = pipeline.ScheduleClipStage(source, ...); // segment 1 + /// while (!h1.IsCompleted) yield return null; h1.Complete(); + /// pipeline.CountBuild(source, decalMeshes); // main thread + /// var h2 = pipeline.ScheduleBuildStage(source, decalMeshes, ...); // segment 2 + /// while (!h2.IsCompleted) yield return null; h2.Complete(); + /// pipeline.ApplyToDecalMeshes(decalMeshes); // main thread merge + (caller) upload + /// + /// + internal sealed class DecalMeshJobPipeline : IDisposable + { + private const int SkinningBatch = 64; + private const int ClipBatch = 16; + + private readonly DecalMeshJobBuffers _buffers = new DecalMeshJobBuffers(); + private NativeArray _clipPlanes = new NativeArray(6, Allocator.Persistent); + + // Per decal mesh (grown as needed). + private NativeArray _decalMeshComponentIndices; + private NativeArray _decalMeshVertexOffsets; + private NativeArray _decalMeshIndexOffsets; + private NativeArray _decalMeshVertexCounts; + private NativeArray _decalMeshIndexCounts; + + // Appended geometry outputs (grown as needed). + private NativeArray _outPositions; + private NativeArray _outNormals; + private NativeArray _outUvs; + private NativeArray _outTangents; + private NativeArray _outBoneWeights; + private NativeArray _outIndices; + private NativeArray _tangentAccumulation; + private NativeArray _bitangentAccumulation; + + private int _decalMeshCount; + // The handle of the most recently scheduled stage. Completed before disposing so a job that is still + // running (e.g. when the scene is unloaded mid-launch) never reads freed NativeArrays. + private JobHandle _lastScheduledHandle; + private bool _disposed; + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + // Ensure no scheduled job is still reading/writing the buffers before they are freed. Unity does + // not guarantee that AirStickerProjector.OnDestroy (which completes its own handle) runs before + // AirStickerSystem.OnDestroy (which calls this). + _lastScheduledHandle.Complete(); + _buffers.Dispose(); + DisposeIfCreated(ref _clipPlanes); + DisposeIfCreated(ref _decalMeshComponentIndices); + DisposeIfCreated(ref _decalMeshVertexOffsets); + DisposeIfCreated(ref _decalMeshIndexOffsets); + DisposeIfCreated(ref _decalMeshVertexCounts); + DisposeIfCreated(ref _decalMeshIndexCounts); + DisposeIfCreated(ref _outPositions); + DisposeIfCreated(ref _outNormals); + DisposeIfCreated(ref _outUvs); + DisposeIfCreated(ref _outTangents); + DisposeIfCreated(ref _outBoneWeights); + DisposeIfCreated(ref _outIndices); + DisposeIfCreated(ref _tangentAccumulation); + DisposeIfCreated(ref _bitangentAccumulation); + } + + /// + /// Segment 1: build the per-component palette (main thread) and schedule the skinning/broad-phase + /// and clip jobs. Returns the handle of the clip job. + /// + public JobHandle ScheduleClipStage( + ReceiverConvexPolygonsMesh source, + float3 centerPositionOfDecalBox, + float3 decalSpaceEx, + float3 decalSpaceEy, + float3 decalSpaceEz, + float width, + float height, + float depth, + bool projectionBackside) + { + var componentCount = source.ComponentCount; + var triangleCount = source.TriangleCount; + + var totalBones = 0; + for (var i = 0; i < componentCount; i++) + if (source.ComponentByIndex[i] is SkinnedMeshRenderer smr && smr.rootBone != null && + smr.sharedMesh != null) + totalBones += smr.bones.Length; + + _buffers.EnsureComponentCapacity(componentCount, totalBones); + _buffers.EnsurePerTriangleCapacity(triangleCount); + + var boneCursor = 0; + for (var i = 0; i < componentCount; i++) + { + var component = source.ComponentByIndex[i]; + var localToWorld = float4x4.identity; + var existsRootBone = false; + var boneOffset = -1; + + if (component is SkinnedMeshRenderer smr) + { + if (smr.rootBone != null && smr.sharedMesh != null) + { + existsRootBone = true; + boneOffset = boneCursor; + var bindPoses = smr.sharedMesh.bindposes; + var bones = smr.bones; + for (var b = 0; b < bones.Length; b++) + _buffers.BoneMatrices[boneCursor++] = ToFloat4x4(bones[b].localToWorldMatrix * bindPoses[b]); + } + + localToWorld = ToFloat4x4(smr.localToWorldMatrix); + } + else if (component is Renderer renderer) + { + localToWorld = ToFloat4x4(renderer.localToWorldMatrix); + } + else if (component is Terrain terrain) + { + localToWorld = ToFloat4x4(terrain.transform.localToWorldMatrix); + } + + _buffers.ComponentLocalToWorld[i] = localToWorld; + _buffers.ComponentExistsRootBone[i] = existsRootBone; + _buffers.ComponentBoneMatrixOffset[i] = boneOffset; + } + + BuildClipPlanes(centerPositionOfDecalBox, decalSpaceEx, decalSpaceEy, decalSpaceEz, width, height, depth); + + var radius = math.sqrt(width * width + height * height + depth * depth) * 0.5f; + + var skinningJob = new SkinningBroadPhaseJob + { + SourcePositionsMs = source.SourcePositionsMs, + SourceBoneWeights = source.SourceBoneWeights, + TriangleComponentIndices = source.TriangleComponentIndices, + ComponentIsSkinned = source.ComponentIsSkinned, + ComponentExistsRootBone = _buffers.ComponentExistsRootBone, + ComponentLocalToWorld = _buffers.ComponentLocalToWorld, + ComponentBoneMatrixOffset = _buffers.ComponentBoneMatrixOffset, + BoneMatrices = _buffers.BoneMatrices, + CenterPositionOfDecalBox = centerPositionOfDecalBox, + DecalSpaceNormalWs = decalSpaceEz, + Radius = radius, + SqrRadius = radius * radius, + ProjectionBackside = projectionBackside, + WorldPositions = _buffers.WorldPositions, + SurviveFlags = _buffers.SurviveFlags + }; + var skinningHandle = skinningJob.Schedule(triangleCount, SkinningBatch); + + var clipJob = new ConvexPolygonClipJob + { + SurviveFlags = _buffers.SurviveFlags, + WorldPositions = _buffers.WorldPositions, + SourcePositionsMs = source.SourcePositionsMs, + SourceNormalsMs = source.SourceNormalsMs, + SourceBoneWeights = source.SourceBoneWeights, + ClipPlanes = _clipPlanes, + ClipWorldPositions = _buffers.ClipWorldPositions, + ClipModelPositions = _buffers.ClipModelPositions, + ClipModelNormals = _buffers.ClipModelNormals, + ClipBoneWeights = _buffers.ClipBoneWeights, + ClipVertexCounts = _buffers.ClipVertexCounts + }; + _lastScheduledHandle = clipJob.Schedule(triangleCount, ClipBatch, skinningHandle); + return _lastScheduledHandle; + } + + /// + /// Main-thread step between the two segments: count the appended geometry of each decal mesh and + /// size the output buffers. Must be called after the clip job has completed. + /// + public void CountBuild(ReceiverConvexPolygonsMesh source, IList decalMeshes) + { + _decalMeshCount = decalMeshes.Count; + EnsurePerDecalMeshCapacity(_decalMeshCount); + + var triangleCount = source.TriangleCount; + var vertexCursor = 0; + var indexCursor = 0; + var maxDecalMeshVertexCount = 0; + + for (var dm = 0; dm < _decalMeshCount; dm++) + { + var componentIndex = source.IndexOfComponent(decalMeshes[dm].ReceiverComponent); + _decalMeshComponentIndices[dm] = componentIndex; + _decalMeshVertexOffsets[dm] = vertexCursor; + _decalMeshIndexOffsets[dm] = indexCursor; + + var vertexCount = 0; + var indexCount = 0; + if (componentIndex >= 0) + for (var tri = 0; tri < triangleCount; tri++) + { + var vc = _buffers.ClipVertexCounts[tri]; + if (vc < 3) continue; + if (source.TriangleComponentIndices[tri] != componentIndex) continue; + vertexCount += vc; + indexCount += (vc - 2) * 3; + } + + _decalMeshVertexCounts[dm] = vertexCount; + _decalMeshIndexCounts[dm] = indexCount; + vertexCursor += vertexCount; + indexCursor += indexCount; + maxDecalMeshVertexCount = math.max(maxDecalMeshVertexCount, vertexCount); + } + + EnsureOutputCapacity(vertexCursor, indexCursor, maxDecalMeshVertexCount); + } + + /// + /// Segment 2: schedule the (serial) mesh build job. + /// + public JobHandle ScheduleBuildStage( + ReceiverConvexPolygonsMesh source, + float3 decalSpaceOriginWs, + float3 decalSpaceEx, + float3 decalSpaceEy, + float width, + float height, + float zOffsetInDecalSpace) + { + var job = new DecalMeshBuildJob + { + TriangleCount = source.TriangleCount, + DecalMeshCount = _decalMeshCount, + TriangleComponentIndices = source.TriangleComponentIndices, + ClipWorldPositions = _buffers.ClipWorldPositions, + ClipModelPositions = _buffers.ClipModelPositions, + ClipModelNormals = _buffers.ClipModelNormals, + ClipBoneWeights = _buffers.ClipBoneWeights, + ClipVertexCounts = _buffers.ClipVertexCounts, + DecalMeshComponentIndices = _decalMeshComponentIndices, + DecalMeshVertexOffsets = _decalMeshVertexOffsets, + DecalMeshIndexOffsets = _decalMeshIndexOffsets, + DecalSpaceOriginWs = decalSpaceOriginWs, + DecalSpaceTangentWs = decalSpaceEx, + DecalSpaceBiNormalWs = decalSpaceEy, + DecalWidth = width, + DecalHeight = height, + ZOffsetInDecalSpace = zOffsetInDecalSpace, + OutPositions = _outPositions, + OutNormals = _outNormals, + OutUvs = _outUvs, + OutTangents = _outTangents, + OutBoneWeights = _outBoneWeights, + OutIndices = _outIndices, + TangentAccumulation = _tangentAccumulation, + BitangentAccumulation = _bitangentAccumulation + }; + _lastScheduledHandle = job.Schedule(); + return _lastScheduledHandle; + } + + /// + /// Merge the built geometry into the decal meshes' CPU buffers. Must be called after the build + /// job has completed. The caller uploads each decal mesh afterwards. + /// + public void ApplyToDecalMeshes(IList decalMeshes) + { + for (var dm = 0; dm < _decalMeshCount; dm++) + decalMeshes[dm].AppendFromJobOutput( + _outPositions, _outNormals, _outUvs, _outTangents, _outBoneWeights, _outIndices, + _decalMeshVertexOffsets[dm], _decalMeshVertexCounts[dm], + _decalMeshIndexOffsets[dm], _decalMeshIndexCounts[dm]); + } + + private void BuildClipPlanes(float3 basePoint, float3 ex, float3 ey, float3 ez, + float width, float height, float depth) + { + var halfDepth = depth * 0.5f; + // Order must match the old ClipPlane enum (Left, Right, Bottom, Top, Front, Back). + _clipPlanes[0] = new float4(ex, width / 2.0f - math.dot(ex, basePoint)); + _clipPlanes[1] = new float4(-ex, width / 2.0f + math.dot(ex, basePoint)); + _clipPlanes[2] = new float4(ey, height / 2.0f - math.dot(ey, basePoint)); + _clipPlanes[3] = new float4(-ey, height / 2.0f + math.dot(ey, basePoint)); + _clipPlanes[4] = new float4(-ez, halfDepth + math.dot(ez, basePoint)); + _clipPlanes[5] = new float4(ez, halfDepth - math.dot(ez, basePoint)); + } + + private void EnsurePerDecalMeshCapacity(int decalMeshCount) + { + EnsureCapacity(ref _decalMeshComponentIndices, decalMeshCount); + EnsureCapacity(ref _decalMeshVertexOffsets, decalMeshCount); + EnsureCapacity(ref _decalMeshIndexOffsets, decalMeshCount); + EnsureCapacity(ref _decalMeshVertexCounts, decalMeshCount); + EnsureCapacity(ref _decalMeshIndexCounts, decalMeshCount); + } + + private void EnsureOutputCapacity(int vertexCount, int indexCount, int maxDecalMeshVertexCount) + { + EnsureCapacity(ref _outPositions, vertexCount); + EnsureCapacity(ref _outNormals, vertexCount); + EnsureCapacity(ref _outUvs, vertexCount); + EnsureCapacity(ref _outTangents, vertexCount); + EnsureCapacity(ref _outBoneWeights, vertexCount); + EnsureCapacity(ref _outIndices, indexCount); + EnsureCapacity(ref _tangentAccumulation, maxDecalMeshVertexCount); + EnsureCapacity(ref _bitangentAccumulation, maxDecalMeshVertexCount); + } + + private static void EnsureCapacity(ref NativeArray buffer, int requiredLength) where T : struct + { + if (buffer.IsCreated && buffer.Length >= requiredLength) return; + + var newLength = buffer.IsCreated + ? math.max(requiredLength, buffer.Length * 2) + : math.max(requiredLength, 1); + if (buffer.IsCreated) buffer.Dispose(); + buffer = new NativeArray(newLength, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + } + + private static void DisposeIfCreated(ref NativeArray buffer) where T : struct + { + if (buffer.IsCreated) buffer.Dispose(); + } + + private static float4x4 ToFloat4x4(Matrix4x4 m) + { + return new float4x4(m.GetColumn(0), m.GetColumn(1), m.GetColumn(2), m.GetColumn(3)); + } + } +} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobPipeline.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobPipeline.cs.meta new file mode 100644 index 0000000..686bfdf --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshJobPipeline.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dc4a5d382e841fe49ada71af96f79850 \ No newline at end of file diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshTangents.cs b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshTangents.cs new file mode 100644 index 0000000..8ae07ec --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshTangents.cs @@ -0,0 +1,116 @@ +using Unity.Collections; +using Unity.Mathematics; + +namespace AirSticker.Runtime.Scripts.Core.Jobs +{ + /// + /// NativeArray port of , used by the mesh build job. + /// + /// + /// Same per-triangle accumulation + Gram-Schmidt orthogonalization (Lengyel) as the managed version, + /// rewritten on NativeArray / Unity.Mathematics types so it runs inside a job (and is Burst-ready). + /// The accumulation scratch is passed in by the caller so the job can reuse it across decal meshes. + /// + internal static class DecalMeshTangents + { + /// + /// Compute the tangents of the appended vertex range [vertexStart, vertexStart + vertexCount). + /// + public static void ComputeTangents( + NativeArray positions, + NativeArray normals, + NativeArray uvs, + NativeArray indices, + NativeArray tangents, + NativeArray tangentAccumulation, + NativeArray bitangentAccumulation, + int vertexStart, + int vertexCount, + int indexStart, + int indexCount) + { + if (vertexCount <= 0) return; + + for (var i = 0; i < vertexCount; i++) + { + tangentAccumulation[i] = float3.zero; + bitangentAccumulation[i] = float3.zero; + } + + var indexEnd = indexStart + indexCount; + for (var i = indexStart; i < indexEnd; i += 3) + { + var i0 = indices[i]; + var i1 = indices[i + 1]; + var i2 = indices[i + 2]; + + var p0 = positions[i0]; + var p1 = positions[i1]; + var p2 = positions[i2]; + var w0 = uvs[i0]; + var w1 = uvs[i1]; + var w2 = uvs[i2]; + + var x1 = p1.x - p0.x; + var x2 = p2.x - p0.x; + var y1 = p1.y - p0.y; + var y2 = p2.y - p0.y; + var z1 = p1.z - p0.z; + var z2 = p2.z - p0.z; + + var s1 = w1.x - w0.x; + var s2 = w2.x - w0.x; + var t1 = w1.y - w0.y; + var t2 = w2.y - w0.y; + + var det = s1 * t2 - s2 * t1; + // Degenerate UVs (e.g. a polygon almost perpendicular to the decal plane) have no meaningful + // tangent direction, so skip the accumulation. + if (det > -1e-8f && det < 1e-8f) continue; + + var r = 1.0f / det; + var tangentDir = new float3( + (t2 * x1 - t1 * x2) * r, + (t2 * y1 - t1 * y2) * r, + (t2 * z1 - t1 * z2) * r); + var bitangentDir = new float3( + (s1 * x2 - s2 * x1) * r, + (s1 * y2 - s2 * y1) * r, + (s1 * z2 - s2 * z1) * r); + + tangentAccumulation[i0 - vertexStart] += tangentDir; + tangentAccumulation[i1 - vertexStart] += tangentDir; + tangentAccumulation[i2 - vertexStart] += tangentDir; + bitangentAccumulation[i0 - vertexStart] += bitangentDir; + bitangentAccumulation[i1 - vertexStart] += bitangentDir; + bitangentAccumulation[i2 - vertexStart] += bitangentDir; + } + + for (var i = 0; i < vertexCount; i++) + { + var normal = normals[vertexStart + i]; + var tangent = tangentAccumulation[i]; + + // Gram-Schmidt orthogonalization against the normal. + tangent -= normal * math.dot(normal, tangent); + var magnitude = math.length(tangent); + if (magnitude > 1e-8f) + tangent /= magnitude; + else + // No valid accumulation (degenerate UVs), so use any direction orthogonal to the normal. + tangent = BuildOrthogonalUnitVector(normal); + + var w = math.dot(math.cross(normal, tangent), bitangentAccumulation[i]) < 0.0f ? -1.0f : 1.0f; + tangents[vertexStart + i] = new float4(tangent, w); + } + } + + private static float3 BuildOrthogonalUnitVector(float3 normal) + { + var axis = math.abs(normal.x) < 0.9f ? new float3(1.0f, 0.0f, 0.0f) : new float3(0.0f, 1.0f, 0.0f); + var orthogonal = axis - normal * math.dot(normal, axis); + var magnitude = math.length(orthogonal); + return magnitude > 1e-8f ? orthogonal / magnitude : new float3(1.0f, 0.0f, 0.0f); + } + } +} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshTangents.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshTangents.cs.meta new file mode 100644 index 0000000..79a8caf --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/DecalMeshTangents.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8ea3971ebee5bf64eaf25ce6e300f329 \ No newline at end of file diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ReceiverConvexPolygonsMesh.cs b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ReceiverConvexPolygonsMesh.cs new file mode 100644 index 0000000..3e02842 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ReceiverConvexPolygonsMesh.cs @@ -0,0 +1,101 @@ +using System; +using Unity.Collections; +using Unity.Mathematics; +using UnityEngine; + +namespace AirSticker.Runtime.Scripts.Core.Jobs +{ + /// + /// Source triangle polygons of a receiver object, laid out as a struct-of-arrays for the job pipeline. + /// + /// + /// This replaces the managed ConvexPolygon soup that the old pipeline cached. + /// It is built once by TrianglePolygonsFactory, cached per receiver object in the triangle + /// polygons pool, and disposed when the receiver dies. All geometry is in model space and static; + /// the world-space positions are recomputed every launch by the skinning job. + /// + /// The receiver components (MeshRenderer / SkinnedMeshRenderer / Terrain) are unified into a single + /// global index space (), so the jobs never hold a managed + /// Component reference. The main thread maps a DecalMesh back to its component index through + /// . + /// + internal sealed class ReceiverConvexPolygonsMesh : IDisposable + { + public const int VerticesPerTriangle = 3; + + private bool _disposed; + + public ReceiverConvexPolygonsMesh(int triangleCount, int componentCount, Allocator allocator) + { + TriangleCount = triangleCount; + ComponentCount = componentCount; + var vertexCount = triangleCount * VerticesPerTriangle; + + SourcePositionsMs = + new NativeArray(vertexCount, allocator, NativeArrayOptions.UninitializedMemory); + SourceNormalsMs = + new NativeArray(vertexCount, allocator, NativeArrayOptions.UninitializedMemory); + SourceBoneWeights = + new NativeArray(vertexCount, allocator, NativeArrayOptions.UninitializedMemory); + TriangleComponentIndices = + new NativeArray(triangleCount, allocator, NativeArrayOptions.UninitializedMemory); + ComponentIsSkinned = + new NativeArray(math.max(1, componentCount), allocator, NativeArrayOptions.ClearMemory); + ComponentByIndex = new Component[componentCount]; + } + + /// + /// True while a launch's jobs are reading this mesh. The pool defers disposing it (even after its + /// receiver died) until this is cleared, so the jobs never read freed NativeArrays. Main thread only. + /// + internal bool InUse; + + /// Number of source triangles. + public int TriangleCount { get; } + + /// Number of receiver components under the receiver object. + public int ComponentCount { get; } + + // Model-space source geometry. One entry per triangle vertex (TriangleCount * 3), + // laid out as (tri0.v0, tri0.v1, tri0.v2, tri1.v0, ...). + public NativeArray SourcePositionsMs; + public NativeArray SourceNormalsMs; + public NativeArray SourceBoneWeights; + + // Per triangle (TriangleCount): the global receiver-component index it belongs to. + public NativeArray TriangleComponentIndices; + + // Per component (ComponentCount): whether it is a SkinnedMeshRenderer. + public NativeArray ComponentIsSkinned; + + // Per component (ComponentCount): the actual receiver Component. Main thread only — + // used to map a DecalMesh to its component index at build time. The jobs never touch this. + public Component[] ComponentByIndex; + + /// + /// The global component index of the given receiver component, or -1 if it is not one of this + /// receiver's components (in which case the decal mesh gets no geometry). + /// + public int IndexOfComponent(Component component) + { + if (component == null) return -1; + for (var i = 0; i < ComponentByIndex.Length; i++) + if (ReferenceEquals(ComponentByIndex[i], component)) + return i; + return -1; + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (SourcePositionsMs.IsCreated) SourcePositionsMs.Dispose(); + if (SourceNormalsMs.IsCreated) SourceNormalsMs.Dispose(); + if (SourceBoneWeights.IsCreated) SourceBoneWeights.Dispose(); + if (TriangleComponentIndices.IsCreated) TriangleComponentIndices.Dispose(); + if (ComponentIsSkinned.IsCreated) ComponentIsSkinned.Dispose(); + ComponentByIndex = null; + } + } +} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ReceiverConvexPolygonsMesh.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ReceiverConvexPolygonsMesh.cs.meta new file mode 100644 index 0000000..4252de0 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/ReceiverConvexPolygonsMesh.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 24d58374b946761439cf6f65ab2fa579 \ No newline at end of file diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/SkinningBroadPhaseJob.cs b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/SkinningBroadPhaseJob.cs new file mode 100644 index 0000000..a632a00 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/SkinningBroadPhaseJob.cs @@ -0,0 +1,110 @@ +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; + +namespace AirSticker.Runtime.Scripts.Core.Jobs +{ + /// + /// Fused skinning + broad-phase job. Runs once per source triangle in parallel. + /// + /// + /// For each triangle it applies the skinning (or the plain local-to-world matrix) to obtain the + /// three world-space vertex positions, computes the face normal, and then runs the broad-phase + /// rejection (face-normal orientation + plane distance + sphere/triangle distance) to decide whether + /// the triangle can be clipped by the decal box. The per-vertex world normal is intentionally not + /// computed: it is never used by the output mesh (which uses the model-space normal) nor by any later + /// stage, so computing it would only waste bandwidth. + /// + /// + [BurstCompile] + internal struct SkinningBroadPhaseJob : IJobParallelFor + { + // --- Source geometry (per triangle vertex / per triangle) --- + [ReadOnly] public NativeArray SourcePositionsMs; + [ReadOnly] public NativeArray SourceBoneWeights; + [ReadOnly] public NativeArray TriangleComponentIndices; + + // --- Per component palette --- + [ReadOnly] public NativeArray ComponentIsSkinned; + [ReadOnly] public NativeArray ComponentExistsRootBone; + [ReadOnly] public NativeArray ComponentLocalToWorld; + [ReadOnly] public NativeArray ComponentBoneMatrixOffset; + [ReadOnly] public NativeArray BoneMatrices; + + // --- Decal box parameters (broad phase) --- + public float3 CenterPositionOfDecalBox; + public float3 DecalSpaceNormalWs; + public float Radius; + public float SqrRadius; + public bool ProjectionBackside; + + // --- Outputs --- + // World positions of every triangle vertex. Each iteration writes a 3-element range, so the parallel + // restriction is disabled; the ranges are disjoint across iterations. + [NativeDisableParallelForRestriction] [WriteOnly] + public NativeArray WorldPositions; + + [WriteOnly] public NativeArray SurviveFlags; + + public void Execute(int triangleIndex) + { + var vertexBase = triangleIndex * ReceiverConvexPolygonsMesh.VerticesPerTriangle; + var componentIndex = TriangleComponentIndices[triangleIndex]; + + var useSkinning = ComponentIsSkinned[componentIndex] && ComponentExistsRootBone[componentIndex]; + + float3 p0, p1, p2; + if (useSkinning) + { + var boneOffset = ComponentBoneMatrixOffset[componentIndex]; + p0 = SkinPosition(vertexBase + 0, boneOffset); + p1 = SkinPosition(vertexBase + 1, boneOffset); + p2 = SkinPosition(vertexBase + 2, boneOffset); + } + else + { + var localToWorld = ComponentLocalToWorld[componentIndex]; + p0 = math.transform(localToWorld, SourcePositionsMs[vertexBase + 0]); + p1 = math.transform(localToWorld, SourcePositionsMs[vertexBase + 1]); + p2 = math.transform(localToWorld, SourcePositionsMs[vertexBase + 2]); + } + + WorldPositions[vertexBase + 0] = p0; + WorldPositions[vertexBase + 1] = p1; + WorldPositions[vertexBase + 2] = p2; + + // The face normal is only needed by the broad phase, so it is not stored. + var faceNormal = DecalGeometryMath.NormalizeSafe(math.cross(p1 - p0, p2 - p0)); + + SurviveFlags[triangleIndex] = SurvivesBroadPhase(faceNormal, p0, p1, p2) ? 1 : 0; + } + + private float3 SkinPosition(int vertexIndex, int boneOffset) + { + var boneWeight = SourceBoneWeights[vertexIndex]; + // Weighted blend of the four influencing bone matrices, matching the old + // Multiply/MultiplyAdd accumulation order so the result stays bit-identical. + var m = BoneMatrices[boneOffset + boneWeight.boneIndex0] * boneWeight.weight0; + m += BoneMatrices[boneOffset + boneWeight.boneIndex1] * boneWeight.weight1; + m += BoneMatrices[boneOffset + boneWeight.boneIndex2] * boneWeight.weight2; + m += BoneMatrices[boneOffset + boneWeight.boneIndex3] * boneWeight.weight3; + return math.transform(m, SourcePositionsMs[vertexIndex]); + } + + private bool SurvivesBroadPhase(float3 faceNormal, float3 v0, float3 v1, float3 v2) + { + if (!ProjectionBackside && math.dot(DecalSpaceNormalWs, faceNormal) < 0.0f) return false; + + // If the plane of the polygon doesn't intersect the sphere, the polygon can be rejected cheaply. + var distToPlane = math.dot(faceNormal, CenterPositionOfDecalBox - v0); + if (distToPlane > Radius || distToPlane < -Radius) return false; + + if (DecalGeometryMath.CalculateSqrDistancePointToTriangle(CenterPositionOfDecalBox, v0, v1, v2) > SqrRadius) + return false; + + return true; + } + } +} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Jobs/SkinningBroadPhaseJob.cs.meta b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/SkinningBroadPhaseJob.cs.meta new file mode 100644 index 0000000..0b59042 --- /dev/null +++ b/Assets/AirSticker/Runtime/Scripts/Core/Jobs/SkinningBroadPhaseJob.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a1aba333261e43e42943d01ab13b6d55 \ No newline at end of file diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Line.cs b/Assets/AirSticker/Runtime/Scripts/Core/Line.cs deleted file mode 100644 index 1828f42..0000000 --- a/Assets/AirSticker/Runtime/Scripts/Core/Line.cs +++ /dev/null @@ -1,93 +0,0 @@ -using UnityEngine; - -namespace AirSticker.Runtime.Scripts.Core -{ - /// - /// Line of convex polygons. - /// - public struct Line - { - public Vector3 StartPosition { get; private set; } - public Vector3 EndPosition { get; private set; } - public Vector3 StartToEndVec { get; private set; } - public Vector3 StartNormal { get; private set; } - public Vector3 EndNormal { get; private set; } - public BoneWeight StartWeight { get; private set; } - public BoneWeight EndWeight { get; private set; } - public Vector3 StartLocalPosition { get; private set; } - public Vector3 EndLocalPosition { get; private set; } - public Vector3 StartLocalNormal { get; private set; } - public Vector3 EndLocalNormal { get; private set; } - - /// - /// Initialize. - /// - public void Initialize(Vector3 startPositionInWorldSpace, Vector3 endPositionInWorldSpace, - Vector3 startNormalInWorldSpace, Vector3 endNormalInWorldSpace, - BoneWeight startWeight, BoneWeight endWeight, Vector3 startPositionInLocalSpace, - Vector3 endPositionInLocalSpace, Vector3 startNormalInLocalSpace, Vector3 endNormalInLocalSpace) - { - StartPosition = startPositionInWorldSpace; - EndPosition = endPositionInWorldSpace; - StartNormal = startNormalInWorldSpace; - EndNormal = endNormalInWorldSpace; - StartToEndVec = EndPosition - StartPosition; - StartWeight = startWeight; - EndWeight = endWeight; - StartLocalPosition = startPositionInLocalSpace; - EndLocalPosition = endPositionInLocalSpace; - StartLocalNormal = startNormalInLocalSpace; - EndLocalNormal = endNormalInLocalSpace; - } - - /// - /// After setting the end position of the line, calculate the from start to end vector. - /// - public void SetEndAndCalcStartToEnd(Vector3 newEndPositionInWorldSpace, Vector3 newEndNormalInWorldSpace, - Vector3 newEndPositionInLocalSpace, Vector3 newEndNormalInLocalSpace) - { - EndPosition = newEndPositionInWorldSpace; - EndNormal = newEndNormalInWorldSpace; - EndLocalPosition = newEndPositionInLocalSpace; - EndLocalNormal = newEndNormalInLocalSpace; - StartToEndVec = EndPosition - StartPosition; - } - - /// - /// After setting the start and end position of the line, calculate the from start to end vector. - /// - public void SetStartEndAndCalcStartToEnd( - Vector3 newStartPositionInWorldSpace, - Vector3 newEndPositionInWorldSpace, - Vector3 newStartNormalInWorldSpace, - Vector3 newEndNormalInWorldSpace, - Vector3 newStartPositionInLocalSpace, - Vector3 newEndPositionInLocalSpace, - Vector3 newStartNormalInLocalSpace, - Vector3 newEndNormalInLocalSpace) - { - StartPosition = newStartPositionInWorldSpace; - EndPosition = newEndPositionInWorldSpace; - StartNormal = newStartNormalInWorldSpace; - EndNormal = newEndNormalInWorldSpace; - - StartLocalPosition = newStartPositionInLocalSpace; - EndLocalPosition = newEndPositionInLocalSpace; - StartLocalNormal = newStartNormalInLocalSpace; - EndLocalNormal = newEndNormalInLocalSpace; - - StartToEndVec = EndPosition - StartPosition; - } - - public void SetStartEndBoneWeights(BoneWeight newStartWeight, BoneWeight newEndWeight) - { - StartWeight = newStartWeight; - EndWeight = newEndWeight; - } - - public void SetEndBoneWeight(BoneWeight newEndWeight) - { - EndWeight = newEndWeight; - } - } -} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/ReceiverObjectTrianglePolygonsPool.cs b/Assets/AirSticker/Runtime/Scripts/Core/ReceiverObjectTrianglePolygonsPool.cs index e463bc7..29e8cf1 100644 --- a/Assets/AirSticker/Runtime/Scripts/Core/ReceiverObjectTrianglePolygonsPool.cs +++ b/Assets/AirSticker/Runtime/Scripts/Core/ReceiverObjectTrianglePolygonsPool.cs @@ -1,40 +1,35 @@ using System.Collections.Generic; using System.Linq; +using AirSticker.Runtime.Scripts.Core.Jobs; using UnityEngine; namespace AirSticker.Runtime.Scripts.Core { - /// - /// The information of convex polygon. - /// - public class ConvexPolygonInfo - { - public ConvexPolygon ConvexPolygon { get; set; } - /// - /// This flag indicates whether the convex polygon is outside the clip space defined by the decal box. - /// - public bool IsOutsideClipSpace { get; set; } - } - internal interface IReceiverObjectTrianglePolygonsPool { - IReadOnlyDictionary> ConvexPolygonsPool { get; } + IReadOnlyDictionary Pool { get; } bool Contains(GameObject receiverObject); void GarbageCollect(); + void DisposeAll(); } /// - /// Triangle polygon pool of receiver object.
- /// Triangle polygons are registered in the pool with the receiver object as the key.
+ /// Triangle polygon pool of receiver objects.
+ /// The source triangle polygons () are registered with the + /// receiver object as the key so repeat projections skip the mesh extraction. ///
+ /// + /// The pooled meshes own NativeArrays, so a dead entry (its receiver was destroyed) is disposed when + /// it is garbage collected, and everything is disposed when the AirStickerSystem is destroyed. + /// public sealed class ReceiverObjectTrianglePolygonsPool : IReceiverObjectTrianglePolygonsPool { - private readonly Dictionary> _trianglePolygonsPool = - new Dictionary>(); + private readonly Dictionary _trianglePolygonsPool = + new Dictionary(); - IReadOnlyDictionary> IReceiverObjectTrianglePolygonsPool. - ConvexPolygonsPool => _trianglePolygonsPool; + IReadOnlyDictionary IReceiverObjectTrianglePolygonsPool.Pool => + _trianglePolygonsPool; /// /// Check to the triangle polygons of the receiver object is already registered. @@ -46,21 +41,38 @@ public bool Contains(GameObject receiverObject) } /// - /// If the receiver object that is registered is dead, it is removed from pool. + /// If the receiver object that is registered is dead, it is removed from the pool and disposed. /// void IReceiverObjectTrianglePolygonsPool.GarbageCollect() { - var deleteList = _trianglePolygonsPool.Where(item => item.Key == null).ToList(); - foreach (var item in deleteList) _trianglePolygonsPool.Remove(item.Key); + // A mesh whose jobs are still running (InUse) is kept even if its receiver died, so the jobs + // never read freed NativeArrays; a later GarbageCollect disposes it once InUse is cleared. + var deleteList = _trianglePolygonsPool + .Where(item => item.Key == null && (item.Value == null || !item.Value.InUse)).ToList(); + foreach (var item in deleteList) + { + item.Value?.Dispose(); + _trianglePolygonsPool.Remove(item.Key); + } } - - public void RegisterTrianglePolygons(GameObject receiverObject, List trianglePolygonInfos) + + void IReceiverObjectTrianglePolygonsPool.DisposeAll() { - if (receiverObject - && !this.Contains(receiverObject)) - _trianglePolygonsPool.Add(receiverObject, trianglePolygonInfos); + foreach (var item in _trianglePolygonsPool) item.Value?.Dispose(); + _trianglePolygonsPool.Clear(); } - + + internal void RegisterTrianglePolygons(GameObject receiverObject, ReceiverConvexPolygonsMesh trianglePolygons) + { + if (receiverObject && !Contains(receiverObject)) + _trianglePolygonsPool.Add(receiverObject, trianglePolygons); + } + + internal ReceiverConvexPolygonsMesh Get(GameObject receiverObject) + { + return _trianglePolygonsPool.TryGetValue(receiverObject, out var mesh) ? mesh : null; + } + public int GetPoolSize() { return _trianglePolygonsPool.Count; diff --git a/Assets/AirSticker/Runtime/Scripts/Core/TrianglePolygonsFactory.cs b/Assets/AirSticker/Runtime/Scripts/Core/TrianglePolygonsFactory.cs index 2171435..adc9013 100644 --- a/Assets/AirSticker/Runtime/Scripts/Core/TrianglePolygonsFactory.cs +++ b/Assets/AirSticker/Runtime/Scripts/Core/TrianglePolygonsFactory.cs @@ -1,19 +1,16 @@ -#if UNITY_EDITOR -// If this symbol is defined, the time of the BuildFromSkinMeshRenderer method is measured. -// It is defined for debugging. -// #define MEASUREMENT_METHOD_BuildFromSkinMeshRenderer -#endif - using System; using System.Collections; using System.Collections.Generic; +using AirSticker.Runtime.Scripts.Core.Jobs; using Unity.Collections; +using Unity.Mathematics; using UnityEngine; namespace AirSticker.Runtime.Scripts.Core { /// - /// Create Triangle polygons info from the mesh renderer or skinned mesh renderer. + /// Create the source triangle polygons () of a receiver + /// object from its mesh renderers, skinned mesh renderers and terrains. /// public class TrianglePolygonsFactory : IDisposable { @@ -21,8 +18,6 @@ public class TrianglePolygonsFactory : IDisposable private static readonly int MaxWorkingVertexCountForTerrain = 128 * 128; // 16,384 private static readonly int MaxWorkingVertexCount = 65536; private static readonly int MaxWorkingTriangleCount = 65536; - private readonly List _workingBoneWeights = new List(MaxWorkingVertexCount); - private readonly List _workingTrianglesForCalcPolygonCount = new List(MaxWorkingTriangleCount); private NativeArray _workingTriangles = new NativeArray(MaxWorkingTriangleCount, Allocator.Persistent); @@ -36,6 +31,9 @@ public class TrianglePolygonsFactory : IDisposable private bool _disposed; private static int _maxGeneratedPolygonPerFrame = 100000; + // The write cursor into the result's per-triangle SoA arrays, shared across the fill coroutines. + private int _writeTriangleCursor; + /// /// Maximum number of polygons processed per frame. /// Values less than 1 are clamped to 1, because this value is used as a modulo divisor. @@ -45,9 +43,6 @@ public static int MaxGeneratedPolygonPerFrame get => _maxGeneratedPolygonPerFrame; set => _maxGeneratedPolygonPerFrame = Mathf.Max(1, value); } -#if MEASUREMENT_METHOD_BuildFromSkinMeshRenderer - public static float[] Time_BuildFromSkinMeshRenderer { get; set; } = new float[3]; -#endif public void Dispose() { @@ -59,139 +54,83 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Build the receiver's source triangle polygons. On success the result is written to + /// resultHolder[0]; it is left null if a mesh is not Read/Write enabled (nothing to build). + /// internal IEnumerator BuildFromReceiverObject( - MeshFilter[] meshFilters, MeshRenderer[] meshRenderers, SkinnedMeshRenderer[] skinnedMeshRenderers, Terrain[] terrains, - List trianglePolygonInfos) + ReceiverConvexPolygonsMesh[] resultHolder) { var numTerrainMeshPolygon = GetNumPolygonsFromTerrains(terrains, 1.0f); - var terrainMehResolutionScale = Mathf.Sqrt(MaxWorkingVertexCountForTerrain / (float)numTerrainMeshPolygon); - CalculateCapacityConvexPolygonInfos(meshFilters, skinnedMeshRenderers, terrains, trianglePolygonInfos, - terrainMehResolutionScale); - yield return BuildFromMeshFilter(meshFilters, meshRenderers, trianglePolygonInfos); - yield return BuildFromSkinMeshRenderer(skinnedMeshRenderers, trianglePolygonInfos); - yield return BuildFromTerrain(terrains, terrainMehResolutionScale, trianglePolygonInfos); - yield return null; - } - - private static int GetNumPolygonsFromSkinModelRenderers(SkinnedMeshRenderer[] skinnedMeshRenderers) - { - var numPolygon = 0; - - foreach (var renderer in skinnedMeshRenderers) + var terrainMeshResolutionScale = numTerrainMeshPolygon > 0 + ? Mathf.Sqrt(MaxWorkingVertexCountForTerrain / (float)numTerrainMeshPolygon) + : 1.0f; + + var meshTriangleCount = GetNumPolygonsFromMeshRenderers(meshRenderers); + var skinTriangleCount = GetNumPolygonsFromSkinModelRenderers(skinnedMeshRenderers); + var terrainTriangleCount = GetNumPolygonsFromTerrains(terrains, terrainMeshResolutionScale); + + // A negative count means a mesh is not readable; that source contributes no geometry (its decal + // mesh stays empty), matching the old per-source behavior. + var buildMesh = meshTriangleCount > 0; + var buildSkin = skinTriangleCount > 0; + if (!buildMesh) meshTriangleCount = 0; + if (!buildSkin) skinTriangleCount = 0; + + var triangleCount = meshTriangleCount + skinTriangleCount + terrainTriangleCount; + var componentCount = meshRenderers.Length + skinnedMeshRenderers.Length + terrains.Length; + + var result = new ReceiverConvexPolygonsMesh(triangleCount, componentCount, Allocator.Persistent); + + // Unify the receiver components into one global index space: mesh renderers, then skinned mesh + // renderers, then terrains. + var skinnedBase = meshRenderers.Length; + var terrainBase = meshRenderers.Length + skinnedMeshRenderers.Length; + for (var i = 0; i < meshRenderers.Length; i++) result.ComponentByIndex[i] = meshRenderers[i]; + for (var j = 0; j < skinnedMeshRenderers.Length; j++) { - if (!renderer || renderer.sharedMesh == null) return -1; - var mesh = renderer.sharedMesh; - if (mesh.isReadable == false) - { - Debug.LogError($"The mesh of the skinned mesh renderer named {renderer.name} is not readable. Please set the Read/Write Enabled flag in the model import settings."); - return -1; - } - - numPolygon += mesh.triangles.Length / 3; + result.ComponentByIndex[skinnedBase + j] = skinnedMeshRenderers[j]; + result.ComponentIsSkinned[skinnedBase + j] = true; } - return numPolygon; - } - - private static int GetNumPolygonsFromTerrain(Terrain terrain, float terrainMehResolutionScale) - { - var terrainData = terrain.terrainData; - var vertexCountX = (int)(terrainData.heightmapResolution * terrainMehResolutionScale); - var vertexCountY = (int)(terrainData.heightmapResolution * terrainMehResolutionScale); - return (vertexCountX - 1) * (vertexCountY - 1) * 2; - } - - private static int GetNumPolygonsFromTerrains(Terrain[] terrains, float terrainMehResolutionScale) - { - var numPolygon = 0; - foreach (var terrain in terrains) - { - numPolygon += GetNumPolygonsFromTerrain(terrain, terrainMehResolutionScale); - } + for (var k = 0; k < terrains.Length; k++) result.ComponentByIndex[terrainBase + k] = terrains[k]; - return numPolygon; - } + _writeTriangleCursor = 0; + if (buildMesh) yield return FillFromMeshRenderers(meshRenderers, result); + if (buildSkin) yield return FillFromSkinnedMeshRenderers(skinnedMeshRenderers, skinnedBase, result); + yield return FillFromTerrains(terrains, terrainBase, terrainMeshResolutionScale, result); - private static int GetNumPolygonsFromMeshFilters(MeshFilter[] meshFilters) - { - var numPolygon = 0; - foreach (var meshFilter in meshFilters) + if (_writeTriangleCursor != triangleCount) { - if (!meshFilter || meshFilter.sharedMesh == null) return -1; - var mesh = meshFilter.sharedMesh; - if(mesh.isReadable == false) - { - Debug.LogError($"The mesh of the mesh filter named {meshFilter.name} is not readable. Please set the Read/Write Enabled flag in the model import settings."); - return -1; - } - var numPoly = mesh.triangles.Length / 3; - numPolygon += numPoly; + // A source was destroyed during the frame-sliced fill, so the SoA is only partially written. + // Discard it instead of registering a mesh whose uninitialized regions the jobs would read. + result.Dispose(); + yield break; } - return numPolygon; + resultHolder[0] = result; } - private static void CalculateCapacityConvexPolygonInfos(MeshFilter[] meshFilters, - SkinnedMeshRenderer[] skinnedMeshRenderers, Terrain[] terrains, List convexPolygonInfos, - float terrainMehResolutionScale) + // Driven by the mesh renderers (not the mesh filter array) so componentIndex == rendererNo always + // matches the mesh-renderer region of ComponentByIndex, even when a child has a MeshFilter without a + // MeshRenderer or vice versa. The polygon count (GetNumPolygonsFromMeshRenderers) is driven the same + // way, so the total stays consistent with BuildFromReceiverObject's completeness check. + private IEnumerator FillFromMeshRenderers(MeshRenderer[] meshRenderers, ReceiverConvexPolygonsMesh result) { - var capacity = 0; - var numPolygonsFromMeshFilters = GetNumPolygonsFromMeshFilters(meshFilters); - var numPolygonsFromSkinModelRenderers = GetNumPolygonsFromSkinModelRenderers(skinnedMeshRenderers); - // GetNumPolygonsFromMeshFilters and GetNumPolygonsFromSkinModelRenderers may return -1 if they cannot obtain the polygon count. - if (numPolygonsFromMeshFilters > 0) capacity += numPolygonsFromMeshFilters; - if( numPolygonsFromSkinModelRenderers > 0) capacity += numPolygonsFromSkinModelRenderers; - - capacity += GetNumPolygonsFromTerrains(terrains, terrainMehResolutionScale); - if (capacity > 0) convexPolygonInfos.Capacity = capacity; - } - - private IEnumerator BuildFromMeshFilter(MeshFilter[] meshFilters, MeshRenderer[] meshRenderers, - List convexPolygonInfos) - { - var numBuildConvexPolygon = GetNumPolygonsFromMeshFilters(meshFilters); - if (numBuildConvexPolygon < 0) yield break; - var newConvexPolygonInfos = new ConvexPolygonInfo[numBuildConvexPolygon]; - - // Calculate size of some buffers and store the count of the polygons. - var bufferSize = 0; - var polygonCounts = new List(); - foreach (var meshFilter in meshFilters) + var polygonNoInFill = 0; + for (var rendererNo = 0; rendererNo < meshRenderers.Length; rendererNo++) { - if (!meshFilter || meshFilter.sharedMesh == null) - continue; + var meshRenderer = meshRenderers[rendererNo]; + if (!meshRenderer) continue; + var meshFilter = meshRenderer.GetComponent(); + if (!meshFilter || meshFilter.sharedMesh == null) continue; var mesh = meshFilter.sharedMesh; - var subMeshCount = mesh.subMeshCount; - for (var meshNo = 0; meshNo < subMeshCount; meshNo++) - { - mesh.GetTriangles(_workingTrianglesForCalcPolygonCount, meshNo); - var numPoly = _workingTrianglesForCalcPolygonCount.Count / 3; - bufferSize += numPoly * VertexCountOfTrianglePolygon; - polygonCounts.Add(numPoly); - } - } + // The mesh renderers are the first components, so componentIndex == rendererNo. + var componentIndex = rendererNo; - // Allocate some buffers. - var positionBuffer = new Vector3[bufferSize]; - var boneWeightBuffer = new BoneWeight[bufferSize]; - var normalBuffer = new Vector3[bufferSize]; - var lineBuffer = new Line[bufferSize]; - var localPositionBuffer = new Vector3[bufferSize]; - var localNormalBuffer = new Vector3[bufferSize]; - var startOffsetOfBuffer = 0; - - var rendererNo = 0; - var newConvexPolygonNo = 0; - var indexOfPolygonCounts = 0; - foreach (var meshFilter in meshFilters) - { - if (!meshFilter || meshFilter.sharedMesh == null) - // Mesh filter is deleted, so process is terminated. - yield break; - var mesh = meshFilter.sharedMesh; using var meshDataArray = Mesh.AcquireReadOnlyMeshData(mesh); var meshData = meshDataArray[0]; meshData.GetVertices(_workingVertexPositions); @@ -200,335 +139,208 @@ private IEnumerator BuildFromMeshFilter(MeshFilter[] meshFilters, MeshRenderer[] for (var meshNo = 0; meshNo < subMeshCount; meshNo++) { meshData.GetIndices(_workingTriangles, meshNo); - var numPoly = polygonCounts[indexOfPolygonCounts++]; + var numPoly = (int)(mesh.GetIndexCount(meshNo) / 3); for (var i = 0; i < numPoly; i++) { - if ((newConvexPolygonNo + 1) % MaxGeneratedPolygonPerFrame == 0) - // Maximum number of polygons processed per frame is MaxGeneratedPolygonPerFrame. - yield return null; - if (!meshFilter || meshFilter.sharedMesh == null) - // Mesh filter is deleted, so process is terminated. - yield break; - var v0_no = _workingTriangles[i * 3]; - var v1_no = _workingTriangles[i * 3 + 1]; - var v2_no = _workingTriangles[i * 3 + 2]; - - localPositionBuffer[startOffsetOfBuffer] = _workingVertexPositions[v0_no]; - localPositionBuffer[startOffsetOfBuffer + 1] = _workingVertexPositions[v1_no]; - localPositionBuffer[startOffsetOfBuffer + 2] = _workingVertexPositions[v2_no]; - - localNormalBuffer[startOffsetOfBuffer] = _workingVertexNormals[v0_no]; - localNormalBuffer[startOffsetOfBuffer + 1] = _workingVertexNormals[v1_no]; - localNormalBuffer[startOffsetOfBuffer + 2] = _workingVertexNormals[v2_no]; - - boneWeightBuffer[startOffsetOfBuffer] = default; - boneWeightBuffer[startOffsetOfBuffer + 1] = default; - boneWeightBuffer[startOffsetOfBuffer + 2] = default; - newConvexPolygonInfos[newConvexPolygonNo] = new ConvexPolygonInfo + if (polygonNoInFill != 0 && polygonNoInFill % MaxGeneratedPolygonPerFrame == 0) { - ConvexPolygon = new ConvexPolygon( - positionBuffer, - normalBuffer, - boneWeightBuffer, - lineBuffer, - localPositionBuffer, - localNormalBuffer, - meshRenderers[rendererNo], - startOffsetOfBuffer, - VertexCountOfTrianglePolygon, - rendererNo, - VertexCountOfTrianglePolygon) - }; - newConvexPolygonNo++; - startOffsetOfBuffer += VertexCountOfTrianglePolygon; + yield return null; + if (!meshRenderer || !meshFilter || meshFilter.sharedMesh == null) yield break; + } + + polygonNoInFill++; + var v0 = _workingTriangles[i * 3]; + var v1 = _workingTriangles[i * 3 + 1]; + var v2 = _workingTriangles[i * 3 + 2]; + WriteTriangle(result, componentIndex, + _workingVertexPositions[v0], _workingVertexPositions[v1], _workingVertexPositions[v2], + _workingVertexNormals[v0], _workingVertexNormals[v1], _workingVertexNormals[v2], + default, default, default); } } - - rendererNo++; } - - convexPolygonInfos.AddRange(newConvexPolygonInfos); } - private IEnumerator BuildFromSkinMeshRenderer(SkinnedMeshRenderer[] skinnedMeshRenderers, - List trianglePolygonInfos) + private IEnumerator FillFromSkinnedMeshRenderers( + SkinnedMeshRenderer[] skinnedMeshRenderers, int componentBase, ReceiverConvexPolygonsMesh result) { -#if MEASUREMENT_METHOD_BuildFromSkinMeshRenderer - var sw = new Stopwatch(); - sw.Start(); -#endif - var numBuildConvexPolygon = GetNumPolygonsFromSkinModelRenderers(skinnedMeshRenderers); - if (numBuildConvexPolygon < 0) yield break; - - var newConvexPolygonInfos = new ConvexPolygonInfo[numBuildConvexPolygon]; - var boneWeights = new BoneWeight[3]; - var newConvexPolygonNo = 0; - - // Calculate size of some buffers and store the count of the polygons. - var bufferSize = 0; - var polygonCounts = new List(); + var workingBoneWeights = new List(MaxWorkingVertexCount); + var polygonNoInFill = 0; for (var rendererNo = 0; rendererNo < skinnedMeshRenderers.Length; rendererNo++) { var skinnedMeshRenderer = skinnedMeshRenderers[rendererNo]; - if (!skinnedMeshRenderer || skinnedMeshRenderer.sharedMesh == null) - // The skinned mesh renderer is deleted, so skip. - continue; + if (!skinnedMeshRenderer || skinnedMeshRenderer.sharedMesh == null) yield break; var mesh = skinnedMeshRenderer.sharedMesh; - var subMeshCount = mesh.subMeshCount; - for (var meshNo = 0; meshNo < subMeshCount; meshNo++) - { - mesh.GetTriangles(_workingTrianglesForCalcPolygonCount, meshNo); - var numPoly = _workingTrianglesForCalcPolygonCount.Count / 3; - bufferSize += numPoly * VertexCountOfTrianglePolygon; - polygonCounts.Add(numPoly); - } - } - - // Allocate some buffers. - var positionBuffer = new Vector3[bufferSize]; - var localPositionBuffer = new Vector3[bufferSize]; - var boneWeightBuffer = new BoneWeight[bufferSize]; - var normalBuffer = new Vector3[bufferSize]; - var localNormalBuffer = new Vector3[bufferSize]; - var lineBuffer = new Line[bufferSize]; - var startOffsetOfBuffer = 0; -#if MEASUREMENT_METHOD_BuildFromSkinMeshRenderer - sw.Stop(); - Time_BuildFromSkinMeshRenderer[0] = sw.ElapsedMilliseconds; - sw = new Stopwatch(); - sw.Start(); -#endif - var indexOfPolygonCount = 0; - for (var rendererNo = 0; rendererNo < skinnedMeshRenderers.Length; rendererNo++) - { - var skinnedMeshRenderer = skinnedMeshRenderers[rendererNo]; - if (!skinnedMeshRenderer || skinnedMeshRenderer.sharedMesh == null) - // The skinned mesh renderer is deleted, so process is terminated. - yield break; - var mesh = skinnedMeshRenderer.sharedMesh; - if (mesh.isReadable == false) - { - yield break; - } + if (mesh.isReadable == false) yield break; + var componentIndex = componentBase + rendererNo; + var hasRootBone = skinnedMeshRenderer.rootBone != null; using var meshDataArray = Mesh.AcquireReadOnlyMeshData(mesh); var meshData = meshDataArray[0]; meshData.GetVertices(_workingVertexPositions); meshData.GetNormals(_workingVertexNormals); + mesh.GetBoneWeights(workingBoneWeights); var subMeshCount = meshData.subMeshCount; - mesh.GetBoneWeights(_workingBoneWeights); for (var meshNo = 0; meshNo < subMeshCount; meshNo++) { meshData.GetIndices(_workingTriangles, meshNo); - var numPoly = polygonCounts[indexOfPolygonCount++]; + var numPoly = (int)(mesh.GetIndexCount(meshNo) / 3); for (var i = 0; i < numPoly; i++) { - if ((newConvexPolygonNo + 1) % MaxGeneratedPolygonPerFrame == 0) - // Maximum number of polygons processed per frame is MaxGeneratedPolygonPerFrame. - yield return null; - if (!skinnedMeshRenderer || skinnedMeshRenderer.sharedMesh == null) - // The skinned mesh renderer is deleted, so process is terminated. - yield break; - var v0No = _workingTriangles[i * 3]; - var v1No = _workingTriangles[i * 3 + 1]; - var v2No = _workingTriangles[i * 3 + 2]; - - // Calculate world matrix. - if (skinnedMeshRenderer.rootBone != null) + if (polygonNoInFill != 0 && polygonNoInFill % MaxGeneratedPolygonPerFrame == 0) { - boneWeights[0] = _workingBoneWeights[v0No]; - boneWeights[1] = _workingBoneWeights[v1No]; - boneWeights[2] = _workingBoneWeights[v2No]; - boneWeightBuffer[startOffsetOfBuffer] = boneWeights[0]; - boneWeightBuffer[startOffsetOfBuffer + 1] = boneWeights[1]; - boneWeightBuffer[startOffsetOfBuffer + 2] = boneWeights[2]; - } - else - { - boneWeightBuffer[startOffsetOfBuffer] = default; - boneWeightBuffer[startOffsetOfBuffer + 1] = default; - boneWeightBuffer[startOffsetOfBuffer + 2] = default; + yield return null; + if (!skinnedMeshRenderer || skinnedMeshRenderer.sharedMesh == null) yield break; } - localPositionBuffer[startOffsetOfBuffer] = _workingVertexPositions[v0No]; - localPositionBuffer[startOffsetOfBuffer + 1] = _workingVertexPositions[v1No]; - localPositionBuffer[startOffsetOfBuffer + 2] = _workingVertexPositions[v2No]; - - localNormalBuffer[startOffsetOfBuffer] = _workingVertexNormals[v0No]; - localNormalBuffer[startOffsetOfBuffer + 1] = _workingVertexNormals[v1No]; - localNormalBuffer[startOffsetOfBuffer + 2] = _workingVertexNormals[v2No]; - - - newConvexPolygonInfos[newConvexPolygonNo] = new ConvexPolygonInfo - { - ConvexPolygon = new ConvexPolygon( - positionBuffer, - normalBuffer, - boneWeightBuffer, - lineBuffer, - localPositionBuffer, - localNormalBuffer, - skinnedMeshRenderer, - startOffsetOfBuffer, - 3, - rendererNo, - VertexCountOfTrianglePolygon) - }; - newConvexPolygonNo++; - startOffsetOfBuffer += VertexCountOfTrianglePolygon; + polygonNoInFill++; + var v0 = _workingTriangles[i * 3]; + var v1 = _workingTriangles[i * 3 + 1]; + var v2 = _workingTriangles[i * 3 + 2]; + var w0 = hasRootBone ? workingBoneWeights[v0] : default; + var w1 = hasRootBone ? workingBoneWeights[v1] : default; + var w2 = hasRootBone ? workingBoneWeights[v2] : default; + WriteTriangle(result, componentIndex, + _workingVertexPositions[v0], _workingVertexPositions[v1], _workingVertexPositions[v2], + _workingVertexNormals[v0], _workingVertexNormals[v1], _workingVertexNormals[v2], + w0, w1, w2); } } } - - trianglePolygonInfos.AddRange(newConvexPolygonInfos); -#if MEASUREMENT_METHOD_BuildFromSkinMeshRenderer - sw.Stop(); - Time_BuildFromSkinMeshRenderer[1] = sw.ElapsedMilliseconds; -#endif } - private IEnumerator BuildFromTerrain(Terrain[] terrains, float terrainMeshResolutionScale, - List convexPolygonInfos) + private IEnumerator FillFromTerrains( + Terrain[] terrains, int componentBase, float terrainMeshResolutionScale, ReceiverConvexPolygonsMesh result) { - var numBuildConvexPolygon = GetNumPolygonsFromTerrains(terrains, terrainMeshResolutionScale); - if (numBuildConvexPolygon < 0) yield break; - var newConvexPolygonInfos = new ConvexPolygonInfo[numBuildConvexPolygon]; - // Calculate size of some buffers and store the count of the polygons. - var bufferSize = 0; - var polygonCounts = new List(); - foreach (var terrain in terrains) - { - if (!terrain || terrain == null) - continue; - var numPoly = GetNumPolygonsFromTerrain(terrain, terrainMeshResolutionScale); - bufferSize += numPoly * VertexCountOfTrianglePolygon; - polygonCounts.Add(numPoly); - } - - // Allocate some buffers. - var positionBuffer = new Vector3[bufferSize]; - var boneWeightBuffer = new BoneWeight[bufferSize]; - var normalBuffer = new Vector3[bufferSize]; - var lineBuffer = new Line[bufferSize]; - var localPositionBuffer = new Vector3[bufferSize]; - var localNormalBuffer = new Vector3[bufferSize]; - var workingVertexPositions = new Vector3[bufferSize]; - var workingVertexNormals = new Vector3[bufferSize]; - var startOffsetOfBuffer = 0; - - var rendererNo = 0; - var newConvexPolygonNo = 0; - var indexOfPolygonCounts = 0; - - foreach (var terrain in terrains) + for (var terrainNo = 0; terrainNo < terrains.Length; terrainNo++) { - if (!terrain || terrain == null) - // Terrain is deleted, so process is terminated. - yield break; + var terrain = terrains[terrainNo]; + if (!terrain || terrain == null) yield break; + var componentIndex = componentBase + terrainNo; var terrainData = terrain.terrainData; - var invResolutionScale = 1.0f / terrainMeshResolutionScale; var vertexCountW = Math.Max(2, (int)(terrainData.heightmapResolution * terrainMeshResolutionScale)); var vertexCountH = Math.Max(2, (int)(terrainData.heightmapResolution * terrainMeshResolutionScale)); var size = terrainData.size; - // Build vertex buffer. + var vertexCount = vertexCountW * vertexCountH; + var positions = new Vector3[vertexCount]; + var normals = new Vector3[vertexCount]; var vertexNo = 0; for (var y = 0; y < vertexCountH; y++) + for (var x = 0; x < vertexCountW; x++) { - for (var x = 0; x < vertexCountW; x++) - { - var normalizedPosition = - new Vector2(x / (float)(vertexCountW - 1), y / (float)(vertexCountH - 1)); - workingVertexNormals[vertexNo] = terrainData.GetInterpolatedNormal( - x / (float)(vertexCountW - 1), - y / (float)(vertexCountH - 1)); - float height = terrainData.GetInterpolatedHeight(normalizedPosition.x, normalizedPosition.y); - workingVertexPositions[vertexNo] = new Vector3(size.x * normalizedPosition.x, - height, size.z * normalizedPosition.y); - vertexNo++; - } + var normalizedX = x / (float)(vertexCountW - 1); + var normalizedY = y / (float)(vertexCountH - 1); + normals[vertexNo] = terrainData.GetInterpolatedNormal(normalizedX, normalizedY); + var height = terrainData.GetInterpolatedHeight(normalizedX, normalizedY); + positions[vertexNo] = new Vector3(size.x * normalizedX, height, size.z * normalizedY); + vertexNo++; } - // Build Convex Polygon. - for (int y = 0; y < vertexCountH - 1; y++) + var polygonNoInFill = 0; + for (var y = 0; y < vertexCountH - 1; y++) + for (var x = 0; x < vertexCountW - 1; x++) { - for (int x = 0; x < vertexCountW - 1; x++) + if (polygonNoInFill != 0 && polygonNoInFill % MaxGeneratedPolygonPerFrame == 0) { - { - var v0_no = (y * vertexCountW) + x; - var v1_no = ((y + 1) * vertexCountW) + x; - var v2_no = (y * vertexCountW) + x + 1; - - localPositionBuffer[startOffsetOfBuffer] = workingVertexPositions[v0_no]; - localPositionBuffer[startOffsetOfBuffer + 1] = workingVertexPositions[v1_no]; - localPositionBuffer[startOffsetOfBuffer + 2] = workingVertexPositions[v2_no]; - - localNormalBuffer[startOffsetOfBuffer] = workingVertexNormals[v0_no]; - localNormalBuffer[startOffsetOfBuffer + 1] = workingVertexNormals[v1_no]; - localNormalBuffer[startOffsetOfBuffer + 2] = workingVertexNormals[v2_no]; - - boneWeightBuffer[startOffsetOfBuffer] = default; - boneWeightBuffer[startOffsetOfBuffer + 1] = default; - boneWeightBuffer[startOffsetOfBuffer + 2] = default; - - newConvexPolygonInfos[newConvexPolygonNo] = new ConvexPolygonInfo - { - ConvexPolygon = new ConvexPolygon( - positionBuffer, - normalBuffer, - boneWeightBuffer, - lineBuffer, - localPositionBuffer, - localNormalBuffer, - terrain, - startOffsetOfBuffer, - VertexCountOfTrianglePolygon, - 0, - VertexCountOfTrianglePolygon) - }; - newConvexPolygonNo++; - startOffsetOfBuffer += VertexCountOfTrianglePolygon; - } - { - var v0_no = ((y + 1) * vertexCountW) + x; - var v1_no = ((y + 1) * vertexCountW) + x + 1; - var v2_no = (y * vertexCountW) + x + 1; - - localPositionBuffer[startOffsetOfBuffer] = workingVertexPositions[v0_no]; - localPositionBuffer[startOffsetOfBuffer + 1] = workingVertexPositions[v1_no]; - localPositionBuffer[startOffsetOfBuffer + 2] = workingVertexPositions[v2_no]; - - localNormalBuffer[startOffsetOfBuffer] = workingVertexNormals[v0_no]; - localNormalBuffer[startOffsetOfBuffer + 1] = workingVertexNormals[v1_no]; - localNormalBuffer[startOffsetOfBuffer + 2] = workingVertexNormals[v2_no]; - - boneWeightBuffer[startOffsetOfBuffer] = default; - boneWeightBuffer[startOffsetOfBuffer + 1] = default; - boneWeightBuffer[startOffsetOfBuffer + 2] = default; - - newConvexPolygonInfos[newConvexPolygonNo] = new ConvexPolygonInfo - { - ConvexPolygon = new ConvexPolygon( - positionBuffer, - normalBuffer, - boneWeightBuffer, - lineBuffer, - localPositionBuffer, - localNormalBuffer, - terrain, - startOffsetOfBuffer, - VertexCountOfTrianglePolygon, - 0, - VertexCountOfTrianglePolygon) - }; - newConvexPolygonNo++; - startOffsetOfBuffer += VertexCountOfTrianglePolygon; - } + yield return null; + if (!terrain || terrain == null) yield break; } + + polygonNoInFill += 2; + + var i00 = y * vertexCountW + x; + var i10 = (y + 1) * vertexCountW + x; + var i01 = y * vertexCountW + x + 1; + var i11 = (y + 1) * vertexCountW + x + 1; + + WriteTriangle(result, componentIndex, + positions[i00], positions[i10], positions[i01], + normals[i00], normals[i10], normals[i01], + default, default, default); + WriteTriangle(result, componentIndex, + positions[i10], positions[i11], positions[i01], + normals[i10], normals[i11], normals[i01], + default, default, default); } } + } - convexPolygonInfos.AddRange(newConvexPolygonInfos); + private void WriteTriangle(ReceiverConvexPolygonsMesh result, int componentIndex, + float3 p0, float3 p1, float3 p2, float3 n0, float3 n1, float3 n2, + BoneWeight w0, BoneWeight w1, BoneWeight w2) + { + var t = _writeTriangleCursor * VertexCountOfTrianglePolygon; + result.SourcePositionsMs[t] = p0; + result.SourcePositionsMs[t + 1] = p1; + result.SourcePositionsMs[t + 2] = p2; + result.SourceNormalsMs[t] = n0; + result.SourceNormalsMs[t + 1] = n1; + result.SourceNormalsMs[t + 2] = n2; + result.SourceBoneWeights[t] = w0; + result.SourceBoneWeights[t + 1] = w1; + result.SourceBoneWeights[t + 2] = w2; + result.TriangleComponentIndices[_writeTriangleCursor] = componentIndex; + _writeTriangleCursor++; + } + + private static int GetNumPolygonsFromSkinModelRenderers(SkinnedMeshRenderer[] skinnedMeshRenderers) + { + var numPolygon = 0; + foreach (var renderer in skinnedMeshRenderers) + { + if (!renderer || renderer.sharedMesh == null) return -1; + var mesh = renderer.sharedMesh; + if (mesh.isReadable == false) + { + Debug.LogError( + $"The mesh of the skinned mesh renderer named {renderer.name} is not readable. Please set the Read/Write Enabled flag in the model import settings."); + return -1; + } + + numPolygon += mesh.triangles.Length / 3; + } + + return numPolygon; + } + + private static int GetNumPolygonsFromTerrain(Terrain terrain, float terrainMeshResolutionScale) + { + var terrainData = terrain.terrainData; + var vertexCountX = (int)(terrainData.heightmapResolution * terrainMeshResolutionScale); + var vertexCountY = (int)(terrainData.heightmapResolution * terrainMeshResolutionScale); + return (vertexCountX - 1) * (vertexCountY - 1) * 2; + } + + private static int GetNumPolygonsFromTerrains(Terrain[] terrains, float terrainMeshResolutionScale) + { + var numPolygon = 0; + foreach (var terrain in terrains) numPolygon += GetNumPolygonsFromTerrain(terrain, terrainMeshResolutionScale); + return numPolygon; + } + + // Renderer-driven so it stays consistent with FillFromMeshRenderers: a renderer without a MeshFilter + // (or without a mesh) contributes nothing, and only a non-readable mesh aborts the whole mesh path. + private static int GetNumPolygonsFromMeshRenderers(MeshRenderer[] meshRenderers) + { + var numPolygon = 0; + foreach (var meshRenderer in meshRenderers) + { + if (!meshRenderer) continue; + var meshFilter = meshRenderer.GetComponent(); + if (!meshFilter || meshFilter.sharedMesh == null) continue; + var mesh = meshFilter.sharedMesh; + if (mesh.isReadable == false) + { + Debug.LogError( + $"The mesh of the mesh filter named {meshFilter.name} is not readable. Please set the Read/Write Enabled flag in the model import settings."); + return -1; + } + + numPolygon += mesh.triangles.Length / 3; + } + + return numPolygon; } } } diff --git a/Assets/AirSticker/package.json b/Assets/AirSticker/package.json index 708536c..e4ddbab 100644 --- a/Assets/AirSticker/package.json +++ b/Assets/AirSticker/package.json @@ -2,9 +2,11 @@ "name": "jp.co.cyberagent.air-sticker", "displayName": "Air Sticker", "version": "1.1.1", - "unity": "2020.3", + "unity": "6000.0", "license": "MIT", "dependencies": { + "com.unity.burst": "1.8.12", + "com.unity.mathematics": "1.3.2" }, "author": { "name": "CyberAgent" diff --git a/Assets/Demo/Demo_Benchmark.meta b/Assets/Demo/Demo_Benchmark.meta new file mode 100644 index 0000000..304d28f --- /dev/null +++ b/Assets/Demo/Demo_Benchmark.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 86e5324a8adf40639c0b9a88ccde3c11 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Demo/Demo_Benchmark/Demo_Benchmark.unity b/Assets/Demo/Demo_Benchmark/Demo_Benchmark.unity new file mode 100644 index 0000000..5ab8303 --- /dev/null +++ b/Assets/Demo/Demo_Benchmark/Demo_Benchmark.unity @@ -0,0 +1,176 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &7700000001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7700000002} + - component: {fileID: 7700000003} + m_Layer: 0 + m_Name: AirStickerBenchmark + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &7700000002 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7700000001} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &7700000003 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7700000001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6281cd575a194aa9a335160cf9a94dd8, type: 3} + m_Name: + m_EditorClassIdentifier: + receiverPrefab: {fileID: 100268, guid: e39dd657be7298d408f1a135c76c3d9a, type: 3} + freezePose: 1 + widthHeightCoverage: 0.8 + depthMargin: 0.3 + aimDirection: {x: 0, y: 0, z: 1} + launchCount: 6 + runOnStart: 1 + startKey: 32 diff --git a/Assets/Demo/Demo_Benchmark/Demo_Benchmark.unity.meta b/Assets/Demo/Demo_Benchmark/Demo_Benchmark.unity.meta new file mode 100644 index 0000000..74d75f4 --- /dev/null +++ b/Assets/Demo/Demo_Benchmark/Demo_Benchmark.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 718a8c129e144fb580c9069ae5a22c6a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Demo/Demo_Benchmark/Scripts.meta b/Assets/Demo/Demo_Benchmark/Scripts.meta new file mode 100644 index 0000000..be7bd78 --- /dev/null +++ b/Assets/Demo/Demo_Benchmark/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d0107d3e6e2e4b228e38723a2b999935 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Demo/Demo_Benchmark/Scripts/AirStickerBenchmark.cs b/Assets/Demo/Demo_Benchmark/Scripts/AirStickerBenchmark.cs new file mode 100644 index 0000000..016dd0c --- /dev/null +++ b/Assets/Demo/Demo_Benchmark/Scripts/AirStickerBenchmark.cs @@ -0,0 +1,223 @@ +using System.Collections; +using AirSticker.Runtime.Scripts; +using AirSticker.Runtime.Scripts.Core; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using UnityEngine; + +namespace Demo.Benchmark +{ + /// + /// Deterministic Air Sticker benchmark for a clean Burst on/off A/B measurement. + /// + /// + /// Unlike Demo03's aging test (random position / material / receiver every launch), this projects the + /// SAME decal box onto the SAME receiver at a FIXED (frozen) pose, and resets the decal meshes before + /// every launch, so each launch performs identical clip/build work. That makes the launches comparable + /// across a Burst-ON and a Burst-OFF run. + /// + /// How to use for the A/B: + /// - Open this scene and enter play mode (it self-sets-up: spawns the AirStickerSystem, a camera, a + /// light, and the receiver prefab, then runs automatically). + /// - Read the [AirSticker][Perf] "clip stage" / "build stage" logs. Use launch #2+ as the steady state + /// (launch #0/#1 include triangle extraction / warm-up). + /// - Toggle Jobs > Burst > Enable Compilation (editor) or make a Burst-ON vs Burst-OFF build + /// (device), and compare the same launch numbers. The workload is identical, so the difference is + /// purely Burst. + /// + public class AirStickerBenchmark : MonoBehaviour + { + [Header("Receiver")] + [Tooltip("Receiver prefab (a skinned mesh). Instantiated at runtime. If null, an existing scene receiver is used.")] + [SerializeField] private GameObject receiverPrefab; + + [Tooltip("Freeze the receiver at its bind pose (disable Animators) so every launch skins identically.")] + [SerializeField] private bool freezePose = true; + + [Header("Decal box")] + [Tooltip("Box width/height as a fraction of the receiver bounds (bigger = more surviving polygons = heavier clip stage).")] + [SerializeField] private float widthHeightCoverage = 0.8f; + + [SerializeField] private float depthMargin = 0.3f; + + [Tooltip("World-space projection direction (the decal box points this way).")] + [SerializeField] private Vector3 aimDirection = Vector3.forward; + + [Header("Run")] + [SerializeField] private int launchCount = 6; + [SerializeField] private bool runOnStart = true; + [SerializeField] private KeyCode startKey = KeyCode.Space; + + private const int BenchmarkGroupId = 987654; + private Material _material; + private GameObject _receiver; + private bool _running; + + private IEnumerator Start() + { + AirStickerPerformanceLog.Enabled = true; + LogBurstStatus(); + + EnsureSystem(); + EnsureCameraAndLight(); + + _receiver = ResolveReceiver(); + if (_receiver == null) + { + Debug.LogError("[AirSticker][Bench] No receiver found. Assign a receiver prefab in the inspector."); + yield break; + } + + if (freezePose) + foreach (var animator in _receiver.GetComponentsInChildren()) + animator.enabled = false; + + _material = CreateDecalMaterial(); + + // Let the skinning settle to the (frozen) pose before measuring. + yield return null; + yield return null; + + if (runOnStart) yield return RunBenchmark(); + } + + private void Update() + { + if (!_running && Input.GetKeyDown(startKey)) StartCoroutine(RunBenchmark()); + } + + private IEnumerator RunBenchmark() + { + _running = true; + + var bounds = ComputeBounds(_receiver); + var dir = aimDirection.sqrMagnitude > 1e-6f ? aimDirection.normalized : Vector3.forward; + var widthHeight = Mathf.Max(0.01f, Mathf.Max(bounds.size.x, bounds.size.y) * widthHeightCoverage); + // Span the receiver along the aim direction so the box reaches the surface. + var depth = Vector3.Scale(bounds.size, + new Vector3(Mathf.Abs(dir.x), Mathf.Abs(dir.y), Mathf.Abs(dir.z))).magnitude + depthMargin; + var projectorPos = bounds.center - dir * (depth * 0.5f); + var projectorRot = Quaternion.LookRotation(dir); + + Debug.Log($"[AirSticker][Bench] start: {launchCount} launches, " + + $"box=({widthHeight:F2} x {widthHeight:F2} x {depth:F2}), reset each launch (identical work). " + + "Use launch #2+ as the steady state."); + + for (var i = 0; i < launchCount; i++) + { + // Reset so every launch performs identical clip/build work. + AirStickerProjector.RemoveDecalMeshes(BenchmarkGroupId); + + var owner = new GameObject("Bench_Projector"); + owner.transform.SetPositionAndRotation(projectorPos, projectorRot); + + var finished = false; + AirStickerProjector.CreateAndLaunch( + owner, _receiver, _material, widthHeight, widthHeight, depth, true, + _ => finished = true, 0.005f, BenchmarkGroupId); + + while (!finished) yield return null; + Debug.Log($"[AirSticker][Bench] launch #{i} done"); + Destroy(owner); + yield return null; + } + + AirStickerProjector.RemoveDecalMeshes(BenchmarkGroupId); + Debug.Log("[AirSticker][Bench] finished. Compare launch #2+ between Burst ON and Burst OFF."); + _running = false; + } + + private GameObject ResolveReceiver() + { + if (receiverPrefab != null) + { + var instance = Instantiate(receiverPrefab); + instance.name = receiverPrefab.name; + return instance; + } + + foreach (var skinnedMeshRenderer in FindObjectsByType(FindObjectsSortMode.None)) + if (skinnedMeshRenderer.name != "AirStickerRenderer") + return skinnedMeshRenderer.transform.root.gameObject; + foreach (var meshFilter in FindObjectsByType(FindObjectsSortMode.None)) + return meshFilter.transform.root.gameObject; + return null; + } + + private static void EnsureSystem() + { + if (FindObjectsByType(FindObjectsSortMode.None).Length == 0) + new GameObject("AirStickerSystem").AddComponent(); + } + + private static void EnsureCameraAndLight() + { + if (Camera.main == null) + { + var cameraObject = new GameObject("Benchmark Camera") { tag = "MainCamera" }; + var camera = cameraObject.AddComponent(); + camera.transform.SetPositionAndRotation(new Vector3(0f, 1f, -3f), Quaternion.Euler(10f, 0f, 0f)); + } + + if (FindObjectsByType(FindObjectsSortMode.None).Length == 0) + { + var lightObject = new GameObject("Benchmark Light"); + var directionalLight = lightObject.AddComponent(); + directionalLight.type = LightType.Directional; + lightObject.transform.rotation = Quaternion.Euler(50f, -30f, 0f); + } + } + + private static Bounds ComputeBounds(GameObject receiver) + { + var renderers = receiver.GetComponentsInChildren(); + if (renderers.Length == 0) return new Bounds(receiver.transform.position, Vector3.one); + var bounds = renderers[0].bounds; + foreach (var renderer in renderers) bounds.Encapsulate(renderer.bounds); + return bounds; + } + + private static Material CreateDecalMaterial() + { + // The material only affects rendering, not the geometry-generation timing being measured. + var shader = Shader.Find("Universal Render Pipeline/Lit") + ?? Shader.Find("Sprites/Default") + ?? Shader.Find("Unlit/Color"); + return new Material(shader); + } + + /// + /// Definitively reports whether Burst is actually compiling the jobs in this build, so a device + /// A/B can be trusted. If this logs NO for a "Burst ON" build, Burst is silently falling back to + /// managed (check Project Settings > Burst AOT Settings, not just the editor Jobs menu). + /// + private static void LogBurstStatus() + { + using var probe = new NativeArray(1, Allocator.TempJob); + new BurstProbeJob { Result = probe }.Schedule().Complete(); + var bursted = probe[0] == 0; + Debug.Log($"[AirSticker][Bench] Burst active for jobs: {(bursted ? "YES" : "NO (managed fallback)")}"); + } + + // The [BurstDiscard] method is stripped when Burst compiles Execute, so Result stays 0 under Burst + // and becomes 1 when the job runs as plain managed code. + [BurstCompile] + private struct BurstProbeJob : IJob + { + public NativeArray Result; + + public void Execute() + { + Result[0] = 0; + MarkManaged(Result); + } + + [BurstDiscard] + private static void MarkManaged(NativeArray result) + { + result[0] = 1; + } + } + } +} diff --git a/Assets/AirSticker/Runtime/Scripts/Core/Line.cs.meta b/Assets/Demo/Demo_Benchmark/Scripts/AirStickerBenchmark.cs.meta similarity index 83% rename from Assets/AirSticker/Runtime/Scripts/Core/Line.cs.meta rename to Assets/Demo/Demo_Benchmark/Scripts/AirStickerBenchmark.cs.meta index dcefaaf..7a9196a 100644 --- a/Assets/AirSticker/Runtime/Scripts/Core/Line.cs.meta +++ b/Assets/Demo/Demo_Benchmark/Scripts/AirStickerBenchmark.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 31be3f4d1fc7d574492c814324a8d6ad +guid: 6281cd575a194aa9a335160cf9a94dd8 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Tests/TestBroadPhaseConvexPolygonsDetection.cs b/Assets/Tests/TestBroadPhaseConvexPolygonsDetection.cs deleted file mode 100644 index f888592..0000000 --- a/Assets/Tests/TestBroadPhaseConvexPolygonsDetection.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System.Collections.Generic; -using AirSticker.Runtime.Scripts.Core; -using NUnit.Framework; -using UnityEngine; - -namespace Tests -{ - public class TestBroadPhaseConvexPolygonsDetection - { - /// - /// Create the convex polygon info of the triangle from vertex positions in model space. - /// - private static ConvexPolygonInfo CreateTrianglePolygonInfo( - Component receiverComponent, Vector3 v0, Vector3 v1, Vector3 v2) - { - var polygon = new ConvexPolygon( - new Vector3[3], - new Vector3[3], - new BoneWeight[3], - new Line[3], - new[] { v0, v1, v2 }, - new Vector3[3], - receiverComponent, - 0, - 3, - 0, - 3); - polygon.PrepareToRunOnWorkerThread(); - polygon.CalculatePositionsAndNormalsInWorldSpace(null, new Matrix4x4[3], new BoneWeight[3]); - return new ConvexPolygonInfo { ConvexPolygon = polygon }; - } - - [Test] - public void TestLargePolygonIsNotCulled() - { - var receiverObject = new GameObject("Receiver"); - try - { - var receiverComponent = receiverObject.AddComponent(); - // The large triangle polygon on the plane of z = 0. - // Its face normal is (0, 0, -1). - var polygonInfos = new List - { - CreateTrianglePolygonInfo( - receiverComponent, - new Vector3(-10.0f, -10.0f, 0.0f), - new Vector3(0.0f, 10.0f, 0.0f), - new Vector3(10.0f, -10.0f, 0.0f)) - }; - - // The small decal box on the center of the triangle. - // All vertices of the triangle are outside the sphere that encompasses the decal box, - // but the triangle intersects the sphere. - var result = BroadPhaseConvexPolygonsDetection.Execute( - new Vector3(0.0f, 0.0f, -0.25f), - new Vector3(0.0f, 0.0f, -1.0f), - 0.3f, - 0.3f, - 1.0f, - polygonInfos, - false); - - Assert.AreEqual(1, result.Count); - } - finally - { - Object.DestroyImmediate(receiverObject); - } - } - - [Test] - public void TestPolygonOnFarPlaneIsCulled() - { - var receiverObject = new GameObject("Receiver"); - try - { - var receiverComponent = receiverObject.AddComponent(); - // The large triangle polygon on the plane of z = 5. - // The plane of the polygon is far from the decal box, - // so it should be culled by the pre-rejection of the plane distance. - var polygonInfos = new List - { - CreateTrianglePolygonInfo( - receiverComponent, - new Vector3(-10.0f, -10.0f, 5.0f), - new Vector3(0.0f, 10.0f, 5.0f), - new Vector3(10.0f, -10.0f, 5.0f)) - }; - - var result = BroadPhaseConvexPolygonsDetection.Execute( - new Vector3(0.0f, 0.0f, -0.25f), - new Vector3(0.0f, 0.0f, -1.0f), - 0.3f, - 0.3f, - 1.0f, - polygonInfos, - false); - - Assert.AreEqual(0, result.Count); - } - finally - { - Object.DestroyImmediate(receiverObject); - } - } - - [Test] - public void TestCoplanarFarPolygonIsCulled() - { - var receiverObject = new GameObject("Receiver"); - try - { - var receiverComponent = receiverObject.AddComponent(); - // The triangle polygon on the same plane as the decal box, but far to the side. - // The plane distance can't cull it, so it should be culled by - // the distance from the sphere center to the triangle. - var polygonInfos = new List - { - CreateTrianglePolygonInfo( - receiverComponent, - new Vector3(40.0f, -10.0f, 0.0f), - new Vector3(50.0f, 10.0f, 0.0f), - new Vector3(60.0f, -10.0f, 0.0f)) - }; - - var result = BroadPhaseConvexPolygonsDetection.Execute( - new Vector3(0.0f, 0.0f, -0.25f), - new Vector3(0.0f, 0.0f, -1.0f), - 0.3f, - 0.3f, - 1.0f, - polygonInfos, - false); - - Assert.AreEqual(0, result.Count); - } - finally - { - Object.DestroyImmediate(receiverObject); - } - } - - [Test] - public void TestBacksidePolygonCulling() - { - var receiverObject = new GameObject("Receiver"); - try - { - var receiverComponent = receiverObject.AddComponent(); - // The reversed winding triangle polygon. - // Its face normal is (0, 0, 1), that is opposite the decal box. - var polygonInfos = new List - { - CreateTrianglePolygonInfo( - receiverComponent, - new Vector3(-10.0f, -10.0f, 0.0f), - new Vector3(10.0f, -10.0f, 0.0f), - new Vector3(0.0f, 10.0f, 0.0f)) - }; - - // The polygon is culled when the projection to the backside is not allowed. - var result = BroadPhaseConvexPolygonsDetection.Execute( - new Vector3(0.0f, 0.0f, -0.25f), - new Vector3(0.0f, 0.0f, -1.0f), - 0.3f, - 0.3f, - 1.0f, - polygonInfos, - false); - Assert.AreEqual(0, result.Count); - - // The polygon is not culled when the projection to the backside is allowed. - result = BroadPhaseConvexPolygonsDetection.Execute( - new Vector3(0.0f, 0.0f, -0.25f), - new Vector3(0.0f, 0.0f, -1.0f), - 0.3f, - 0.3f, - 1.0f, - polygonInfos, - true); - Assert.AreEqual(1, result.Count); - } - finally - { - Object.DestroyImmediate(receiverObject); - } - } - - [Test] - public void TestCalculateSqrDistancePointToTriangle() - { - var a = new Vector3(0.0f, 0.0f, 0.0f); - var b = new Vector3(1.0f, 0.0f, 0.0f); - var c = new Vector3(0.0f, 1.0f, 0.0f); - - // The vertex region of a. - Assert.AreEqual(2.0f, - BroadPhaseConvexPolygonsDetection.CalculateSqrDistancePointToTriangle( - new Vector3(-1.0f, -1.0f, 0.0f), a, b, c), 1e-5f); - // The vertex region of b. - Assert.AreEqual(1.0f, - BroadPhaseConvexPolygonsDetection.CalculateSqrDistancePointToTriangle( - new Vector3(2.0f, 0.0f, 0.0f), a, b, c), 1e-5f); - // The vertex region of c. - Assert.AreEqual(1.0f, - BroadPhaseConvexPolygonsDetection.CalculateSqrDistancePointToTriangle( - new Vector3(0.0f, 2.0f, 0.0f), a, b, c), 1e-5f); - // The edge region of ab. - Assert.AreEqual(1.0f, - BroadPhaseConvexPolygonsDetection.CalculateSqrDistancePointToTriangle( - new Vector3(0.5f, -1.0f, 0.0f), a, b, c), 1e-5f); - // The edge region of ac. - Assert.AreEqual(1.0f, - BroadPhaseConvexPolygonsDetection.CalculateSqrDistancePointToTriangle( - new Vector3(-1.0f, 0.5f, 0.0f), a, b, c), 1e-5f); - // The edge region of bc. The closest point is (0.5, 0.5, 0). - Assert.AreEqual(0.5f, - BroadPhaseConvexPolygonsDetection.CalculateSqrDistancePointToTriangle( - new Vector3(1.0f, 1.0f, 0.0f), a, b, c), 1e-5f); - // The face region. - Assert.AreEqual(1.0f, - BroadPhaseConvexPolygonsDetection.CalculateSqrDistancePointToTriangle( - new Vector3(0.25f, 0.25f, 1.0f), a, b, c), 1e-5f); - // The point on the triangle. - Assert.AreEqual(0.0f, - BroadPhaseConvexPolygonsDetection.CalculateSqrDistancePointToTriangle( - new Vector3(0.25f, 0.25f, 0.0f), a, b, c), 1e-5f); - } - } -} diff --git a/Assets/Tests/TestBroadPhaseConvexPolygonsDetection.cs.meta b/Assets/Tests/TestBroadPhaseConvexPolygonsDetection.cs.meta deleted file mode 100644 index d7206b3..0000000 --- a/Assets/Tests/TestBroadPhaseConvexPolygonsDetection.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 52386372ee764564a7e9056f3292dbd4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/TestConvexPolygon.cs b/Assets/Tests/TestConvexPolygon.cs deleted file mode 100644 index 1f67e5d..0000000 --- a/Assets/Tests/TestConvexPolygon.cs +++ /dev/null @@ -1,81 +0,0 @@ -using AirSticker.Runtime.Scripts.Core; -using NUnit.Framework; -using UnityEngine; - -namespace Tests -{ - public class TestConvexPolygon - { - // A Test behaves as an ordinary method - [Test] - public void TestIntersectRayToTriangle() - { - var receiverObject = new GameObject("Receiver"); - try - { - var receiverComponent = receiverObject.AddComponent(); - - var verticesInModelSpace = new Vector3[3]; - verticesInModelSpace[0] = new Vector3(-0.5f, -0.5f, 0.0f); - verticesInModelSpace[1] = new Vector3(0.0f, 0.5f, 0.0f); - verticesInModelSpace[2] = new Vector3(0.5f, -0.5f, 0.0f); - var normalsInModelSpace = new Vector3[3]; - normalsInModelSpace[0] = new Vector3(0.0f, 0.0f, -1.0f); - normalsInModelSpace[1] = new Vector3(0.0f, 0.0f, -1.0f); - normalsInModelSpace[2] = new Vector3(0.0f, 0.0f, -1.0f); - - var convexPolygon = new ConvexPolygon( - new Vector3[3], - new Vector3[3], - new BoneWeight[3], - new Line[3], - verticesInModelSpace, - normalsInModelSpace, - receiverComponent, - 0, - 3, - 0, - 3); - // Calculate the face normal and the edges of the polygon. - // The receiver object has the identity transform, so the world space - // positions are the same as the model space positions. - convexPolygon.PrepareToRunOnWorkerThread(); - convexPolygon.CalculatePositionsAndNormalsInWorldSpace( - null, new Matrix4x4[3], new BoneWeight[3]); - - var rayStart = new Vector3(); - rayStart.x = 0.0f; - rayStart.y = 0.0f; - rayStart.z = 2.0f; - - var rayEnd = new Vector3(); - rayEnd.x = 0.0f; - rayEnd.y = 0.0f; - rayEnd.z = -2.0f; - - Vector3 hitPoint; - // Hit test. - var isIntersect = convexPolygon.IsIntersectRayToTriangle(out hitPoint, rayStart, rayEnd); - Assert.IsTrue(isIntersect); - Assert.AreEqual(0.0f, hitPoint.x, 1e-5f); - Assert.AreEqual(0.0f, hitPoint.y, 1e-5f); - Assert.AreEqual(0.0f, hitPoint.z, 1e-5f); - - rayStart.x = 1.0f; - rayStart.y = 0.0f; - rayStart.z = 2.0f; - - rayEnd.x = 1.0f; - rayEnd.y = 0.0f; - rayEnd.z = -2.0f; - // Miss test. - isIntersect = convexPolygon.IsIntersectRayToTriangle(out hitPoint, rayStart, rayEnd); - Assert.IsFalse(isIntersect); - } - finally - { - Object.DestroyImmediate(receiverObject); - } - } - } -} diff --git a/Assets/Tests/TestConvexPolygon.cs.meta b/Assets/Tests/TestConvexPolygon.cs.meta deleted file mode 100644 index 67411d2..0000000 --- a/Assets/Tests/TestConvexPolygon.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c857fe2c680d8914988eb889028be1ce -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/TestConvexPolygonClipping.cs b/Assets/Tests/TestConvexPolygonClipping.cs new file mode 100644 index 0000000..e482092 --- /dev/null +++ b/Assets/Tests/TestConvexPolygonClipping.cs @@ -0,0 +1,171 @@ +using AirSticker.Runtime.Scripts.Core.Jobs; +using NUnit.Framework; +using Unity.Collections; +using Unity.Mathematics; +using UnityEngine; + +namespace Tests +{ + /// + /// Standalone regression tests for the job-pipeline geometry (DecalGeometryMath / + /// ConvexPolygonClipping). Equivalence to the old ConvexPolygon / BroadPhase code was validated by + /// differential tests while both coexisted; after the old code was removed these assert the properties + /// and known values the new code must keep. + /// + public class TestConvexPolygonClipping + { + private const int MaxVertex = DecalMeshJobBuffers.MaxVertexCountPerConvexPolygon; + + [Test] + public void SqrDistancePointToTriangleKnownValues() + { + var a = new float3(0.0f, 0.0f, 0.0f); + var b = new float3(1.0f, 0.0f, 0.0f); + var c = new float3(0.0f, 1.0f, 0.0f); + + AssertSqrDistance(2.0f, new float3(-1.0f, -1.0f, 0.0f), a, b, c); // vertex region of a + AssertSqrDistance(1.0f, new float3(2.0f, 0.0f, 0.0f), a, b, c); // vertex region of b + AssertSqrDistance(1.0f, new float3(0.0f, 2.0f, 0.0f), a, b, c); // vertex region of c + AssertSqrDistance(1.0f, new float3(0.5f, -1.0f, 0.0f), a, b, c); // edge region of ab + AssertSqrDistance(1.0f, new float3(-1.0f, 0.5f, 0.0f), a, b, c); // edge region of ac + AssertSqrDistance(0.5f, new float3(1.0f, 1.0f, 0.0f), a, b, c); // edge region of bc + AssertSqrDistance(1.0f, new float3(0.25f, 0.25f, 1.0f), a, b, c); // face region + AssertSqrDistance(0.0f, new float3(0.25f, 0.25f, 0.0f), a, b, c); // on the triangle + } + + [Test] + public void ClipKeepsGeometryInsideDecalBox() + { + // Triangle spanning [-1, 1] on the z = 0 plane, and a smaller decal box that cuts its edges. + var triangle = new[] + { + new float3(-1.0f, -1.0f, 0.0f), + new float3(1.0f, -1.0f, 0.0f), + new float3(0.0f, 1.0f, 0.0f) + }; + var planes = BuildDecalBoxPlanes(new float3(0.0f, 0.0f, 0.0f), 1.0f, 1.0f, 2.0f); + + var count = Clip(triangle, planes, out var vertices); + + Assert.GreaterOrEqual(count, 3, "the triangle overlaps the box, so it should not be removed"); + for (var i = 0; i < count; i++) + for (var p = 0; p < planes.Length; p++) + { + var signedDistance = math.dot(planes[p].xyz, vertices[i]) + planes[p].w; + Assert.GreaterOrEqual(signedDistance, -1e-4f, + $"vertex {i} is outside plane {p} (signed distance {signedDistance})"); + } + } + + [Test] + public void ClipRemovesFullyOutsideTriangle() + { + var triangle = new[] + { + new float3(100.0f, 100.0f, 0.0f), + new float3(101.0f, 100.0f, 0.0f), + new float3(100.0f, 101.0f, 0.0f) + }; + var planes = BuildDecalBoxPlanes(new float3(0.0f, 0.0f, 0.0f), 1.0f, 1.0f, 2.0f); + + var count = Clip(triangle, planes, out _); + + Assert.AreEqual(0, count, "a triangle fully outside the box should be removed"); + } + + [Test] + public void ClipKeepsFullyInsideTriangleUnchanged() + { + var triangle = new[] + { + new float3(-0.2f, -0.2f, 0.0f), + new float3(0.2f, -0.2f, 0.0f), + new float3(0.0f, 0.2f, 0.0f) + }; + var planes = BuildDecalBoxPlanes(new float3(0.0f, 0.0f, 0.0f), 2.0f, 2.0f, 2.0f); + + var count = Clip(triangle, planes, out var vertices); + + Assert.AreEqual(3, count, "a triangle fully inside the box should be unchanged"); + for (var i = 0; i < 3; i++) + { + Assert.AreEqual(triangle[i].x, vertices[i].x, 1e-4f, $"vertex {i}.x"); + Assert.AreEqual(triangle[i].y, vertices[i].y, 1e-4f, $"vertex {i}.y"); + Assert.AreEqual(triangle[i].z, vertices[i].z, 1e-4f, $"vertex {i}.z"); + } + } + + // Clip the world-space triangle by the planes and return the resulting vertex count and positions. + // World == model space here, which is enough to exercise the clip math. + private static int Clip(float3[] triangle, float4[] planes, out float3[] worldVertices) + { + var clipWorld = new NativeArray(MaxVertex, Allocator.Temp); + var clipModel = new NativeArray(MaxVertex, Allocator.Temp); + var clipNormal = new NativeArray(MaxVertex, Allocator.Temp); + var clipBoneWeight = new NativeArray(MaxVertex, Allocator.Temp); + try + { + for (var i = 0; i < 3; i++) + { + clipWorld[i] = triangle[i]; + clipModel[i] = triangle[i]; + clipNormal[i] = new float3(0.0f, 0.0f, -1.0f); + clipBoneWeight[i] = default; + } + + var buffers = new ClipVertexBuffers + { + WorldPositions = clipWorld, + ModelPositions = clipModel, + ModelNormals = clipNormal, + BoneWeights = clipBoneWeight + }; + + var vertexCount = 3; + for (var p = 0; p < planes.Length; p++) + { + ConvexPolygonClipping.ClipByPlane(buffers, 0, ref vertexCount, planes[p], out var allOutside); + if (allOutside) + { + vertexCount = 0; + break; + } + } + + worldVertices = new float3[vertexCount]; + for (var i = 0; i < vertexCount; i++) worldVertices[i] = clipWorld[i]; + return vertexCount; + } + finally + { + clipWorld.Dispose(); + clipModel.Dispose(); + clipNormal.Dispose(); + clipBoneWeight.Dispose(); + } + } + + // Six decal-box planes (Left, Right, Bottom, Top, Front, Back), built like DecalMeshJobPipeline. + private static float4[] BuildDecalBoxPlanes(float3 basePoint, float width, float height, float depth) + { + var ex = new float3(1.0f, 0.0f, 0.0f); + var ey = new float3(0.0f, 1.0f, 0.0f); + var ez = new float3(0.0f, 0.0f, 1.0f); + var halfDepth = depth * 0.5f; + return new[] + { + new float4(ex, width / 2.0f - math.dot(ex, basePoint)), + new float4(-ex, width / 2.0f + math.dot(ex, basePoint)), + new float4(ey, height / 2.0f - math.dot(ey, basePoint)), + new float4(-ey, height / 2.0f + math.dot(ey, basePoint)), + new float4(-ez, halfDepth + math.dot(ez, basePoint)), + new float4(ez, halfDepth - math.dot(ez, basePoint)) + }; + } + + private static void AssertSqrDistance(float expected, float3 p, float3 a, float3 b, float3 c) + { + Assert.AreEqual(expected, DecalGeometryMath.CalculateSqrDistancePointToTriangle(p, a, b, c), 1e-5f); + } + } +} diff --git a/Assets/Tests/TestConvexPolygonClipping.cs.meta b/Assets/Tests/TestConvexPolygonClipping.cs.meta new file mode 100644 index 0000000..eeb65bb --- /dev/null +++ b/Assets/Tests/TestConvexPolygonClipping.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 61c1c5ac72e92f54aa5c08433985d999 \ No newline at end of file diff --git a/Assets/Tests/TestDecalMeshTangents.cs b/Assets/Tests/TestDecalMeshTangents.cs new file mode 100644 index 0000000..f4f5fa1 --- /dev/null +++ b/Assets/Tests/TestDecalMeshTangents.cs @@ -0,0 +1,124 @@ +using AirSticker.Runtime.Scripts.Core; +using AirSticker.Runtime.Scripts.Core.Jobs; +using NUnit.Framework; +using Unity.Collections; +using Unity.Mathematics; +using UnityEngine; + +namespace Tests +{ + /// + /// Differential test that asserts the NativeArray tangent port (DecalMeshTangents) matches the legacy + /// managed tangent calculator (DecalMeshTangentCalculator) for the same input. + /// + public class TestDecalMeshTangents + { + [Test] + public void NativeTangentsMatchLegacyForAppendedRange() + { + // A convex polygon (pentagon) placed at a non-zero vertex/index offset, to catch offset handling. + const int total = 8; + const int vertexStart = 2; + const int vertexCount = 5; + const int indexStart = 3; + + var localPositions = new[] + { + new Vector3(0.0f, 0.0f, 0.0f), + new Vector3(1.0f, 0.0f, 0.2f), + new Vector3(1.2f, 1.0f, -0.1f), + new Vector3(0.5f, 1.5f, 0.3f), + new Vector3(-0.2f, 1.0f, 0.1f) + }; + var localNormals = new[] + { + new Vector3(0.0f, 0.0f, 1.0f).normalized, + new Vector3(0.1f, 0.0f, 1.0f).normalized, + new Vector3(0.0f, 0.1f, 1.0f).normalized, + new Vector3(-0.1f, 0.05f, 1.0f).normalized, + new Vector3(0.05f, -0.1f, 1.0f).normalized + }; + var localUvs = new[] + { + new Vector2(0.1f, 0.1f), + new Vector2(0.9f, 0.15f), + new Vector2(0.95f, 0.9f), + new Vector2(0.5f, 1.0f), + new Vector2(0.05f, 0.85f) + }; + + var positions = new Vector3[total]; + var normals = new Vector3[total]; + var uvs = new Vector2[total]; + for (var i = 0; i < vertexCount; i++) + { + positions[vertexStart + i] = localPositions[i]; + normals[vertexStart + i] = localNormals[i]; + uvs[vertexStart + i] = localUvs[i]; + } + + // Triangle fan over the pentagon, using real (offset) vertex indices, placed at indexStart. + var indices = new int[indexStart + (vertexCount - 2) * 3]; + var indexWrite = indexStart; + for (var triNo = 0; triNo < vertexCount - 2; triNo++) + { + indices[indexWrite++] = vertexStart; + indices[indexWrite++] = vertexStart + triNo + 1; + indices[indexWrite++] = vertexStart + triNo + 2; + } + + var indexCount = (vertexCount - 2) * 3; + + // --- Legacy managed tangents --- + var legacyTangents = new Vector4[total]; + DecalMeshTangentCalculator.CalculateTangents( + positions, normals, uvs, indices, legacyTangents, + vertexStart, vertexCount, indexStart, indexCount); + + // --- NativeArray port --- + var nativePositions = new NativeArray(total, Allocator.Temp); + var nativeNormals = new NativeArray(total, Allocator.Temp); + var nativeUvs = new NativeArray(total, Allocator.Temp); + var nativeIndices = new NativeArray(indices.Length, Allocator.Temp); + var nativeTangents = new NativeArray(total, Allocator.Temp); + var tangentAccum = new NativeArray(vertexCount, Allocator.Temp); + var bitangentAccum = new NativeArray(vertexCount, Allocator.Temp); + try + { + for (var i = 0; i < total; i++) + { + nativePositions[i] = positions[i]; + nativeNormals[i] = normals[i]; + nativeUvs[i] = uvs[i]; + } + + for (var i = 0; i < indices.Length; i++) nativeIndices[i] = indices[i]; + + DecalMeshTangents.ComputeTangents( + nativePositions, nativeNormals, nativeUvs, nativeIndices, nativeTangents, + tangentAccum, bitangentAccum, + vertexStart, vertexCount, indexStart, indexCount); + + for (var i = 0; i < vertexCount; i++) + { + var expected = legacyTangents[vertexStart + i]; + var actual = nativeTangents[vertexStart + i]; + Assert.AreEqual(expected.x, actual.x, 1e-4f, $"tangent[{i}].x"); + Assert.AreEqual(expected.y, actual.y, 1e-4f, $"tangent[{i}].y"); + Assert.AreEqual(expected.z, actual.z, 1e-4f, $"tangent[{i}].z"); + Assert.AreEqual(expected.w, actual.w, 1e-4f, $"tangent[{i}].w"); + } + } + finally + { + nativePositions.Dispose(); + nativeNormals.Dispose(); + nativeUvs.Dispose(); + nativeIndices.Dispose(); + nativeTangents.Dispose(); + tangentAccum.Dispose(); + bitangentAccum.Dispose(); + } + } + } +} diff --git a/Assets/Tests/TestDecalMeshTangents.cs.meta b/Assets/Tests/TestDecalMeshTangents.cs.meta new file mode 100644 index 0000000..1b0402d --- /dev/null +++ b/Assets/Tests/TestDecalMeshTangents.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0ff4c2c269340594e8aaba5844edaf32 \ No newline at end of file diff --git a/Assets/Tests/Tests.asmdef b/Assets/Tests/Tests.asmdef index f8a2eeb..d9e84fb 100644 --- a/Assets/Tests/Tests.asmdef +++ b/Assets/Tests/Tests.asmdef @@ -4,7 +4,9 @@ "references": [ "UnityEngine.TestRunner", "UnityEditor.TestRunner", - "AirSticker" + "AirSticker", + "Unity.Mathematics", + "Unity.Collections" ], "includePlatforms": [ "Editor" diff --git a/JobSystemMigrationPlan.md b/JobSystemMigrationPlan.md index 2cb27a5..6189784 100644 --- a/JobSystemMigrationPlan.md +++ b/JobSystemMigrationPlan.md @@ -3,7 +3,7 @@ Air Sticker のデカール貼り付け処理(ポリゴン分割)を Worker Thread (ThreadPool) から Unity Job System + Burst へ移行するかの評価と、段階的な実装計画。 - 作成日: 2026-07-31 -- ステータス: Step 2 完了(2026-07-31、計測・見た目確認済み)。Step 0 の残計測(静的メッシュ/テレイン/連続 Launch)と Step 3 が未着手 +- ステータス: **Step 3a + 3c 完了・コミット済み(2026-07-31、`feature/job-system-step3`: f1ea701=3a / adcac77=3c / b05068f=ベンチマーク)**。実装・テスト・見た目・計測(エディタ + 実機 probe 付き有効 A/B)すべて確認済み。効果: 旧 ThreadPool 単一スレッド → Job 並列(Step 3a) → + Burst(Step 3c、実機 worker さらに ~1.58x)。残: 3d(メジャーリリース準備)、Step 0 の残計測(静的メッシュ/テレイン/連続 Launch) ## 運用ルール @@ -215,17 +215,152 @@ Air Sticker のデカール貼り付け処理(ポリゴン分割)を Worker Thre ### Step 3: 本丸の Job + Burst 化 -- [ ] `ConvexPolygon` を index ベースの struct SoA に再設計 - - `Component` 参照 → `rendererIndex` に置換 - - `Line` 構造体を廃止、エッジはオンザフライ計算 - - 固定ストライド(最大 64 頂点)を維持し `IJobParallelFor` を素直に書く -- [ ] ジョブチェーン構築: スキニング → ブロードフェーズ → クリップ → メッシュ構築 -- [ ] コルーチンのポーリングを `JobHandle.IsCompleted` に置き換え -- [ ] `package.json` に `com.unity.burst` 依存を追加(バージョンはUnity 2020.3 互換の 1.6 系) -- [ ] EditMode テストを新データ構造へ移植(先行して行い、リグレッション検出に使う) -- [ ] メジャーバージョンアップとしてリリース +#### 確定方針(2026-07-31 ユーザー決定) -**着手判断**: Step 0 の計測結果と「多数同時デカール需要があるか」で決める。 +計画当初の前提「Unity 2020.3 縛り → Burst 1.6 系」は**崩れている**。実開発環境は **Unity 6000.3.19f1(Unity 6.3)** で、`packages-lock.json` に **Burst 1.8.29 / Collections 2.6.6 / Mathematics 1.3.3** が推移的依存として既に存在する。一方、配布 `package.json` は `update-unity6.3` / `v2.0.0` ブランチでも `"unity": "2020.3"` / 依存ゼロを維持しており、Burst/Collections 2.x(Unity 2022.3+ 必須)の追加はこの方針と衝突する。この分岐についてユーザーに確認し、以下を決定した: + +- **進め方 = 段階式**。まず SoA 再設計 + `IJobParallelFor` 並列化を **Burst なし**で実装・計測し、Burst 依存の是非は計測後に別サブステップで判断する。これまでの「実装 → 計測 → コミット」の呼吸に合わせ、破壊的変更(Burst 依存追加)を最後まで遅延できる。 +- **設計基準 = Unity 6.x 前提**。`package.json` の最小サポートを Unity 6.x へ引き上げ、`Unity.Mathematics`(float3/float4x4) を使って最初から **Burst 対応可能な形**で設計する。将来の 2020.3 配慮は捨てる。 + +**依存フットプリントの最小化**: `NativeArray` / `IJobParallelFor` / `IJob` / `JobHandle` はコアモジュール(依存ゼロ)。固定ストライド 64 設計で可変長コンテナを避けるため **`com.unity.collections`(NativeList 等)は使わない**。追加する依存は `Unity.Mathematics` のみ(3a)、`com.unity.burst` は最終サブステップ(3c)。 + +#### 設計(新データモデル) + +- **`Component` 参照 → グローバル receiverComponentIndex に統一**。現行の `rendererNo` はソース種別(MeshRenderer / SkinnedMeshRenderer / Terrain)ごとに別の index 空間で、実際のマッチングは `Component` 参照で行っている。これを受けオブジェクト配下の全コンポーネント横断の単一 index に統一し、ジョブからマネージド参照を排除、build 時のマッチングも index 比較にする。 +- **`Line` 構造体を廃止**。エッジ i = (vert[i], vert[(i+1)%n]) はリング隣接で自明。`StartToEndVec` はオンザフライ計算。分割時に平面をまたぐ 2 エッジの端点データだけを分割前にローカルへ退避すれば等価。 +- **per-vertex の world normal を除去**。クリップの補間 t は world position のみに依存し、faceNormal は位置から再計算、最終メッシュは model normal を使うため、per-vertex world normal は**出力に一切使われないデッドデータ**(検証済み)。クリップ作業セットは worldPos + modelPos + modelNormal + boneWeight のみに削減(メモリトラフィック削減)。 +- **固定ストライド 64**(`DefaultMaxVertex`)を維持し `IJobParallelFor` を素直に書く。 +- SoA 構成: + - 受けごとの永続キャッシュ(factory が一度構築、プール保持): source model-space position/normal/boneWeight(triCount*3)、per-triangle の receiverComponentIndex、per-component の isSkinned。`NativeArray`(Persistent)。受け消滅時に Dispose。 + - per-launch 作業セット: 全三角形の worldPos(triCount*3)・faceNormal・survive フラグ、survivor の stride-64 バッファ。`NativeArray`(TempJob/Persistent プール)。 + +#### ジョブチェーン + +1. **skinning + broadphase 融合 `IJobParallelFor`**(三角形単位・並列): ボーン行列パレットをブレンドして worldPos を計算 → faceNormal → broadphase カリング(面法線 + 平面距離 + 球-三角形距離)→ survive フラグ。boneMatricesPallet はジャグ配列を `NativeArray` + per-component オフセットにフラット化。 +2. **コンパクション**(survive フラグの prefix-sum → survivor index、scatter で stride-64 へ集約)。prefix-sum は単スレッド(安価)、scatter は `IJobParallelFor` 可。 +3. **クリップ `IJobParallelFor`**(survivor 単位・並列): 6 平面を順に適用。`SplitAndRemoveByPlane` を stride-64 NativeArray スライス上に移植(Line なし・world normal なし)。 +4. **メッシュ構築**: DecalMesh(= 1 receiverComponentIndex)ごとに survivor を fan 展開 → MeshData。タンジェント計算(既存 `DecalMeshTangentCalculator` を移植)。 +5. コルーチンの `_executeLaunchingOnWorkerThread` ポーリングを `JobHandle.IsCompleted` ポーリングに置換(ステートマシン・`DecalProjectorLauncher` は変更不要)。 + +#### サブステップ(実装順) + +- [~] **3a**: 基盤 + 新 SoA データモデル + ジョブ化(**Burst 無効**)+ JobHandle 化 + テスト移植。MSBuild でコンパイル確認。**進行中**。以下の内訳: + - [x] 基盤: `package.json` を `"unity": "6000.0"` + `com.unity.mathematics` 依存へ、`AirSticker.asmdef` に `Unity.Mathematics` 参照追加。MSBuild(VS2022)でのコンパイル検証下地を整備(`*.csproj` はワイルドカード + `Library/ScriptAssemblies/*.dll` 参照。gitignore 済みで Unity 再生成)。float3+NativeArray+IJobParallelFor+Schedule のスモークテスト通過。 + - [x] 新 SoA データモデル: `Core/Jobs/ReceiverConvexPolygonsMesh.cs`(永続ソース、`Component` → グローバル componentIndex 統一)、`Core/Jobs/DecalMeshJobBuffers.cs`(per-launch プールバッファ、`AirStickerSystem` 所有想定でライフタイム管理)。 + - [x] ジョブ実装(Burst 無効): `Core/Jobs/DecalGeometryMath.cs`(純関数: 球-三角形距離 / Unity 意味論の `NormalizeSafe`)、`Core/Jobs/SkinningBroadPhaseJob.cs`(スキニング + faceNormal + broadphase、world normal 未計算)、`Core/Jobs/ConvexPolygonClipping.cs`(`SplitAndRemoveByPlane` を stride-64 スライスへ移植、Line 廃止 / world normal 除去 / 旧実装の t 再利用の癖まで忠実再現)、`Core/Jobs/ConvexPolygonClipJob.cs`(三角形単位で seed + 6 平面クリップ、非 survivor は早期 return。compaction なしで stride-64 を三角形数で確保 = 旧 survivor 基準プールより小メモリ)。 + - [x] 差分テスト: `Assets/Tests/TestConvexPolygonClipping.cs`。旧 `ConvexPolygon.SplitAndRemoveByPlane` / `BroadPhaseConvexPolygonsDetection` と新実装を同一入力で走らせ出力一致を検証(単一平面の両ブランチ網羅 + デカールボックス 6 平面列 + 球-三角形距離)。両実装が共存する今のうちに移植の等価性を固定する。**human のエディタ実行で PASS 確認が次の関門**。 + - [x] メッシュ構築ステージのジョブ化: `Core/Jobs/DecalMeshTangents.cs`(タンジェント計算の NativeArray 移植、差分テスト `TestDecalMeshTangents.cs` 付き)+ `Core/Jobs/DecalMeshBuildJob.cs`(serial `IJob`: clip 出力 → fan 展開 + UV + zOffset + タンジェント → 追記ジオメトリの NativeArray 出力)。**オフメイン維持**(Step 2 のタンジェント排除を退行させない)のため serial IJob。出力インデックスは出力配列ローカル空間で、main が DecalMesh へ merge する際に `(既存頂点数 - 出力頂点オフセット)` を加えてメッシュ絶対 index へ変換。 + - **全 compute stage(skinning / broadphase / clip / build)実装・コンパイル済み。数値ロジック(距離・クリップ・タンジェント)は差分テストで検証済み。** 統合スイッチも完了(以下すべて実装・コンパイル済み): + - [x] `TrianglePolygonsFactory` を新 SoA 出力へ改修: `ReceiverConvexPolygonsMesh` を構築(model 空間 position/normal/boneWeight を `NativeArray`/`BoneWeight`、per-triangle componentIndex、per-component isSkinned)。プールを `ReceiverConvexPolygonsMesh` 保持 + GC 時 / `AirStickerSystem.OnDestroy` で Dispose。 + - [x] オーケストレーター `Core/Jobs/DecalMeshJobPipeline.cs`: per-component パレット構築(`float4x4` フラット化)+ 2 セグメント(skinning+clip / build)スケジュール + カウント + merge。`BuildClipPlanes` はここへ移動。 + - [x] `AirStickerProjector`: ThreadPool ワーカー + フラグポーリングを 2 セグメント + `JobHandle.IsCompleted` ポーリングへ置換。`AirStickerPerformanceLog.Enabled` 時のみ同期 `Complete()` + Stopwatch で実 compute 時間を計測(Burst on/off 比較用)、通常時は非同期。 + - [x] `DecalMesh`: `AppendFromJobOutput`(NativeArray 出力 → 永続 CPU バッファへ append、index を `既存頂点数 - 出力オフセット` で補正)。旧 `AddTrianglePolygonsToDecalMesh` 削除。 + - [x] 旧 `ConvexPolygon`/`Line`/`BroadPhaseConvexPolygonsDetection`/ThreadPool パスを削除。ロールバック機構も削除(新設計は追記が全てジョブ完了後の main なのでゴースト発生せず不要)。 + - [x] 既存 EditMode テストを整理: `TestConvexPolygon` / `TestBroadPhaseConvexPolygonsDetection` 削除、`TestConvexPolygonClipping` を旧依存なしのスタンドアロン版(プロパティ検証 + 既知値)へ書き換え。 + - [x] **受け消滅時の use-after-free 対策**: プールの `NativeArray` 明示 Dispose により、Launch 中に受けが死ぬと GC がジョブ実行中の source を解放し得る(旧マネージド実装では無害だった新規ハザード)。`ReceiverConvexPolygonsMesh.InUse` ピンを追加し、Launch 中(schedule〜seg2 完了)はピン、GC は解除まで破棄を延期。projector 破棄時も解除。 + - 検証状況: **全 3 プロジェクト(AirSticker / Tests / Assembly-CSharp+Demo)MSBuild グリーン**。公開 API 維持。数値等価性・見た目・ジョブセーフティ(editor の collections checks)・実挙動は **human のエディタ実行が必須**(下記 3b)。 +- [x] **3b(検証・計測)**: human がエディタで EditMode テスト全緑 + デモ 01〜04 の見た目に差分なし + ジョブセーフティ/例外なしを確認(2026-07-31)。代表シナリオ計測で **Burst 無効の並列化だけで約 4.9 倍**を確認(下記)。 + +#### 計測結果: Step 3a(2026-07-31、エディタ Mono、大きめスキンメッシュ・近似シナリオ、2回連続 Launch) + +アップロードが 0.08ms/1346 頂点で Step 2 記録(0.09ms/1168 頂点)とほぼ一致するため、同一デモ・近似シナリオ(入力 8191/生存 4849 相当)と判断し Step 2 ベースラインと比較。**2回目 Launch(JIT ウォーム後)の値**で比較する(Step 0 の知見どおり 1回目は JIT ウォームアップ込みで比較に使わない — 参考: clip 11.07ms / build 3.67ms)。 + +| 項目 | Step 2(旧 ThreadPool・単一スレッド逐次) | Step 3a(Job 並列・**Burst 無効**) | +|---|---|---| +| skinning | 3.66 ms | ┐ | +| broadphase | 2.80 ms | ├ clip stage(3 つ融合・並列)= **1.56 ms** | +| clip | (下の clip+build に含む) | ┘ | +| build(fan+uv+tangent) | (clip+build 合計 3.17 ms) | build stage(serial・オフメイン)= **0.42 ms** | +| **ワーカー計算合計** | **9.63 ms** | **1.98 ms** | +| メッシュアップロード(main・最大) | 0.09 ms | 0.08 ms | + +**分かったこと**: + +1. **ワーカー計算 9.63ms → 1.98ms(約 4.9 倍)を Burst 無しの `IJobParallelFor` 並列化だけで達成。** 段階式の狙い(並列化でコア数スケール)がエディタのコア数で明確に確認できた。skinning/broadphase/clip をポリゴン単位で並列化した効果。 +2. **アップロードは不変(0.09→0.08ms)。** メインスレッドのコストに変化なし(Step 2 で確立したオフメイン・タンジェント/単一 Apply を維持)。 +3. **リグレッションなし・見た目差分なし・ジョブセーフティエラーなし。** 数値ロジックの差分テスト(距離・クリップ・タンジェント)も全緑。 +4. 計測は `AirStickerPerformanceLog.Enabled` 時のみ同期 `Complete()` で実 compute 時間を測る方式(通常時は非同期でメイン非ブロック)。build stage は現状 serial(未並列)で、将来の並列化候補だが 0.42ms と小さい。 +5. **残: IL2CPP/実機**での計測(下記で実施)。3c の Burst 有効化と合わせて確認する。 + +#### 計測結果: Step 3a(2026-07-31、IL2CPP / Android 実機 Pixel 8a、Demo03、3回連続 Launch) + +| 項目 | 1回目(warmup) | 2回目 | 3回目 | +|---|---|---|---| +| clip stage(skinning+broadphase+clip・並列) | 10.70 ms | 1.70 ms | 2.03 ms | +| build stage(fan+uv+tangent・serial) | 0.40 ms | 0.64 ms | 0.46 ms | +| ワーカー計算合計 | 11.10 ms | **2.34 ms** | **2.49 ms** | +| メッシュアップロード(main・最大) | 3.64 ms | 0.08 ms | 0.09 ms | + +**分かったこと**: + +1. **実機 steady-state(2/3回目)でワーカー計算 約 2.3–2.5ms**(Burst 無効・並列ジョブ)。**これが 3c の Burst A/B の実機ベースライン。** +2. **1回目(clip 10.70ms・upload 3.64ms)は cold 要因で突出。** IL2CPP は JIT なしだが、新設計のプール `NativeArray` を 1回目に初回書き込みするため cold cache + first-touch ページフォルト + ジョブワーカー初回スピンアップが乗る(alloc は計測窓外の main 側だが、ジョブの初回書き込みが窓内)。Step 0 の旧 ThreadPool 実機計測で 1回目突出が無かったのは、旧実装が確保済みマネージドバッファを再利用していたため。**steady-state 比較では 1回目を使わない**。 +3. **build stage は実機でも 0.4–0.6ms と小さく**、serial(未並列)でもボトルネックでない。将来並列化の優先度は低い。 +4. 旧パイプライン削除済みで同一シナリオの実機旧値は取得不可。計画 Step 0 の実機ログ(アロケ除去後の単一スレッド推定 ~20–28ms)との対比では、実機の多コア(Tensor G3)で並列化効果がエディタ(4.9x)以上に出ている見込み。 + +**判断**: Step 3a の目的(SoA + Job 化・Burst 無効の並列化効果の確認)をエディタ・実機の両方で達成、リグレッションなし・見た目差分なし。**コミット可**。 +- [~] **3c**: `com.unity.burst` 依存追加 + 3 ジョブに `[BurstCompile]` 付与 + asmdef に `Unity.Burst` 参照。**実装・全 3 プロジェクト MSBuild グリーン(2026-07-31)**。計測待ち。 + - A/B の取り方: `[BurstCompile]` を付けたまま **Jobs > Burst > Enable Compilation を OFF にすると Step 3a(Burst 無効)と同一挙動**になるので、エディタではトグルで A/B 可。実機は AOT なのでビルド時に焼き込み(Step 3a コミット f1ea701 = Burst 無効ベースライン、本 3c = Burst 有効を実機で比較)。 + - **要確認**: MSBuild は属性/using の解決までしか検証できない。Burst 本体の厳密コンパイル(マネージド参照・例外・未対応構文の不許可)は Unity ビルド時に走るため、**human のエディタ/実機ビルドで Burst コンパイルエラーが出ないか要確認**(コンソール or Burst インスペクタ)。ジョブは blittable + math 組み込み + マネージド参照なしで設計済みなので通る見込み。 + - 計測後にコミット。 + +#### 計測結果: Step 3c Burst A/B(2026-07-31、エディタ Mono、2回目 Launch = steady) + +| stage | Burst OFF | Burst ON | Burst 倍率 | +|---|---|---|---| +| clip stage(skinning+broadphase+clip・並列) | 1.61 ms | 0.21 ms | 7.7x | +| build stage(fan+uv+tangent・serial) | 0.41 ms | 0.11 ms | 3.7x | +| **ワーカー計算合計** | **2.02 ms** | **0.32 ms** | **6.3x** | +| アップロード(main・非 Burst) | 0.07 ms | 0.08 ms | 不変 | + +**分かったこと**: + +1. **Burst で worker 計算 2.02ms → 0.32ms(約 6.3x)。** 数学中心ループ(スキニング行列ブレンド・クリップ平面演算・タンジェント)に SIMD が効いた。 +2. **累積効果**: 旧 ThreadPool 単一スレッド(Step 2: 9.63ms)→ Job 並列 Burst 無効(2.02ms, 4.9x)→ **Job 並列 Burst 有効(0.32ms)= 旧比 約 30x**(エディタ Mono)。 +3. **Burst コンパイル成功の確認**: Burst ON の数値が出た = 3 ジョブが Burst コンパイル・実行できている(エラーなし)。`FloatMode.Fast` 未指定で IEEE 厳密のため結果は Burst 無効と一致(見た目不変)。 +4. Burst ON 1回目(clip 1.08ms)はエディタの Burst オンデマンドコンパイル込みで比較対象外。 +5. **残: 実機(Pixel 8a / IL2CPP)の Burst ON 計測**(Step 3a の Burst 無効実機ベースライン 約 2.3ms との比較)。実機 ARM NEON + AOT では Burst 効果がエディタ以上に出る見込み。 + +#### 計測結果: Step 3c Burst A/B(2026-07-31、**実機 Pixel 8a / IL2CPP、クリーンベンチマーク** Demo_Benchmark) + +`Demo_Benchmark` シーン(同一受け・バインドポーズ固定・同一デカール・各 Launch 前にリセット)で計測。両ランで各 DecalMesh の頂点数が完全一致(1821/4976/678/132/3382)= **同一ワークロードを確認**。 + +**Burst 稼働は probe(`[BurstDiscard]` トリック)で確定**: ON ビルド=`Burst active: YES`、OFF ビルド=`NO (managed fallback)`。**Burst AOT 無効化は Project Settings > Burst AOT Settings で行う**(Jobs メニューのトグルはエディタ JIT のみで AOT ビルドに効かない)。steady-state(#2–5): + +| stage | Burst OFF(NO) | Burst ON(YES) | Burst 倍率 | +|---|---|---|---| +| clip stage(skinning+broadphase+clip) | ~3.14 ms | ~2.13 ms | **1.48x** | +| build stage(fan+uv+tangent) | ~4.42 ms | ~2.67 ms | **1.65x** | +| **ワーカー計算合計** | **~7.56 ms** | **~4.80 ms** | **~1.58x** | + +(ノイズは大きめ。box=1.01³ で survivor が多く clip/build が重いシナリオ。) + +**分かったこと(重要・前回訂正)**: + +1. **実機(IL2CPP)で Burst は約 1.5x の実利がある**(clip 1.48x / build 1.65x / worker 1.58x)。数学中心の skinning はよく効き、分岐主体の broadphase/clip でも IL2CPP 比で 1.5x 程度は出る(計画の「IL2CPP 比 2〜6x」の下限寄り)。 +2. **前回(probe 前)の「実機 Burst ≈ 1x・無益」は誤り**だった。前回の "OFF" ビルドは Burst AOT が有効なまま(Jobs メニューのトグルはエディタ JIT のみ)で、ON≈OFF は「両方 Burst 有効」だったため。probe 導入で `OFF=NO` を確認し、有効な A/B で 1.5x を検出。 +3. **累積**: 旧 ThreadPool 単一スレッド → Job 並列(Step 3a、大幅短縮)→ + Burst(Step 3c、実機 worker さらに ~1.58x)。エディタ Mono 比の 7.7x は Mono が遅いための過大評価だが、実機でも 1.5x は実利。 +4. 計測手法(同期 Complete・1 Launch 1 サンプル)はノイズあり。厳密化するなら多サンプル平均が望ましいが、ON/OFF の分布は分離しており 1.5x の定性は堅い。 + +**判断**: 実機で **Burst ≈ 1.5x の実利を probe 付き有効 A/B で確認**。パフォーマンスライブラリとして意味のある差で、`com.unity.burst` は Unity 標準・広く使われる依存(本プロジェクトも元々推移的に保持)。依存ゼロは Step 3a の mathematics で既に外れている。→ **3c をコミットする方針**(最終判断はユーザー)。 + +#### Codex レビュー記録(2026-07-31、Step 3a/3c に対して実施・指摘 3 件すべて修正) + +`feature/job-system-step2-meshdata..feature/job-system-step3` の差分を Codex CLI にレビューさせ、検証のうえ 3 件とも修正した(再レビューで「指摘なし」を確認)。 + +1. **(blocker) `AirStickerSystem.OnDestroy` がジョブ実行中に NativeArray を破棄** — シーンアンロード/終了が Launch 中に起きると、Unity の破棄順非保証により System が先に破棄されジョブ実行中のバッファ/source を解放し use-after-free。修正: `DecalMeshJobPipeline._lastScheduledHandle` を追加し `Dispose()` 先頭で `Complete()`(`OnDestroy` は pipeline.Dispose → pool.DisposeAll の順)。 +2. **(major) 部分フィルで未初期化 SoA が登録される** — フレーム分割中に受けメッシュが消えると `FillFrom*` が `yield break` するが外側は結果を登録し、`SkinningBroadPhaseJob` が未初期化領域(`UninitializedMemory`)を読む。修正: `BuildFromReceiverObject` 末尾で `_writeTriangleCursor == triangleCount` を検証、不一致なら `result.Dispose()` してキャンセル。 +3. **(major) componentIndex 不整合** — `FillFromMeshFilters` が meshFilter index を componentIndex にするが `ComponentByIndex` は meshRenderer 基準。両配列がずれると Burst ジョブで範囲外/別コンポーネント参照。修正: メッシュ経路をレンダラ駆動化(`FillFromMeshRenderers` / `GetNumPolygonsFromMeshRenderers`)し count/fill を一致、`meshFilters` 引数を除去。 + +補足: Codex は clip 移植 / fan 展開 / index delta / ジョブ内マネージド参照については問題なしと確認。残る軽微事項として「projector が build コルーチン実行中に破棄されると in-flight の `result` NativeArray がリークし得る(境界的・稀)」は今回見送り(3d 以降で検討)。 + +- [ ] **3d**: メジャーバージョンアップ(`package.json` を 2.0.0 へ)としてリリース。残課題(下記 4 件)もこのタイミングで解消。 + +**着手判断(記録)**: Step 1-2 だけで既にワーカー計算は 65.7ms → 8.65ms(目標 1/10 近く)に到達済みで、単発デカール用途では体感差は出ない。Step 3 の主効果は「多数同時 / 大規模スキンメッシュ」でのコア数スケールと IL2CPP での Burst SIMD。段階式にしたのは、この効果を計測で確かめてから破壊的な Burst 依存を判断するため。 + +#### 補足: MSBuild コンパイル検証について + +エディタ起動中は Unity ヘッドレステスト実行が静かに失敗するため、実装中の検証は VS2022 MSBuild で `AirSticker.csproj` をビルドする方式を使う(数値等価性・見た目は human のエディタ実行が必要)。`*.csproj` は `.gitignore` 済み(Unity 再生成)なので、asmdef 変更後の csproj 同期のため一時的に `` を手動追加してよい。`Unity.Mathematics.dll` 等は `Library/ScriptAssemblies/` にコンパイル済み。 ## 受け入れ基準(案) @@ -236,6 +371,8 @@ Air Sticker のデカール貼り付け処理(ポリゴン分割)を Worker Thre 2026-07-31 のブランチレビュー(Step 0/1 実装後)で挙がった指摘のうち未対応のもの。**Step 3 完了後にまとめて対応する。** +**Step 3a/3c 完了時点の状況(2026-07-31)**: 下記のうち **#1(静的プール保持)と #4(`_broadPhaseConvexPolygonInfos` クリア)は Step 3 の再設計で自然解消**した。#1 は `BroadPhaseConvexPolygonsDetection` の静的プール自体を廃止し、`DecalMeshJobBuffers` / `DecalMeshJobPipeline` を `AirStickerSystem` が所有・`OnDestroy` で Dispose、`ReceiverConvexPolygonsMesh` も GC 時 Dispose(受け消滅時の use-after-free は `InUse` ピンで対処)。#4 は該当フィールドを持つ旧 `AirStickerProjector` を全面書き換えたため消滅。残るは **#2(Demo03 の計測ログ常時 ON)と #3(`AirStickerPerformanceLog` の公開 API 扱い)で、3d(リリース準備)で対応**。 + 1. **静的プールの恒久的メモリ保持** — `BroadPhaseConvexPolygonsDetection` の静的プールバッファは一度成長すると解放されない(実機ビルドではアプリ終了まで、エディタはドメインリロードまで)。計測シナリオ(生存 4849 ポリゴン)で `64 スロット × 4849 × 約 252B ≈ 78MB`、倍々成長のため最悪その約 2 倍。`AirStickerSystem.OnDestroy()` から呼ぶ `ReleaseBuffers()` のような明示解放フック、またはしきい値超過時のトリムを追加する。Step 3 の `NativeArray` 化でバッファ設計自体を見直すため、そのタイミングで一緒に解決するのが効率的 2. **Demo03 の計測ログ常時 ON** — `Demo03.Start()` の `AirStickerPerformanceLog.Enabled = true;` はエージングテストでコンソールを埋め、`Debug.Log` のコスト(特にワーカースレッド内)が「連続 Launch(キュー詰まり再現)」の計測値を歪める。Step 0 の計測がすべて完了したら削除するか UI トグル化する 3. **`AirStickerPerformanceLog` が配布パッケージの公開 API** — `Assets/AirSticker` 配下は UPM パッケージとして配布されるため、public static クラスは一度リリースすると利用者が依存し得る。移行完了後に削除するか、残す場合は一時的な計測基盤である旨を明記する。XML コメントが参照する本ドキュメントはパッケージ利用者(`?path=/Assets/AirSticker`)には配布されない点も直す