using System.Collections; using System.Collections.Generic; using System.Linq; using Koenigz.PerfectCulling; using UnityEngine; using UnityEngine.SceneManagement; namespace Koenigz.PerfectCulling { public static class EditorBake { public static IEnumerator PerformBakeAsync(this PerfectCullingBakingBehaviour behaviour, bool saveScene, HashSet additionalOccludersHashset, bool cullAdditionalOccluders) { #if !UNITY_EDITOR yield break; #else string currentScenePath = null; try { // Collect Renderers that should be excluded HashSet renderersToExcludeFromBake = new HashSet(); foreach (PerfectCullingRendererTag rendererTag in GameObject.FindObjectsOfType()) { if (rendererTag.ExcludeRendererFromBake) { Renderer r = rendererTag.GetComponent(); if (r == null) { continue; } renderersToExcludeFromBake.Add(r); } } // Strip Renderers that should be excluded foreach (var bakeGroup in behaviour.bakeGroups) { bakeGroup.renderers = bakeGroup.renderers.Where(r => !renderersToExcludeFromBake.Contains(r)).ToArray(); } // Strip empty groups //behaviour.bakeGroups = behaviour.bakeGroups.Where(bakeGroup => bakeGroup.renderers.Length > 0).ToArray(); if (behaviour.bakeGroups.Length <= 0) { PerfectCullingEditorUtil.DisplayDialog("No renderers", $"No renderers for {behaviour.name}. Nothing to bake", "OK"); yield return new PerfectCullingBakeNotStartedYieldInstruction(); } if (behaviour.bakeGroups.Length == 1 && (additionalOccludersHashset == null || additionalOccludersHashset.Count == 0)) { PerfectCullingLogger.LogError( $"{nameof(behaviour.bakeGroups)} contains only one element thus no occlusion is possible. Each Renderer should go into it's own {nameof(PerfectCullingBakeGroup)}. Consider using {nameof(PerfectCullingEditorUtil.CreateBakeGroupsForRenderers)}!"); yield return new PerfectCullingBakeNotStartedYieldInstruction(); } /* // Order renderers for improved local coherence Renderers = Renderers .OrderBy(m => m.transform.position.x) .OrderBy(m => m.transform.position.z) .OrderBy(m => m.transform.position.y) .ToArray();*/ UnityEditor.EditorUtility.DisplayProgressBar($"Initializing", "Initializing", 0); if (!behaviour.PreBake()) { yield return new PerfectCullingBakeNotStartedYieldInstruction(); } PerfectCullingMonoGroup[] allMonoGroups = PerfectCullingEditorUtil.FindMonoGroupsForBakingBehaviour(behaviour); HashSet copyAdditionalOccluders = new HashSet(); if (additionalOccludersHashset != null) { foreach (Renderer r in additionalOccludersHashset) { copyAdditionalOccluders.Add(r); } } // Strip null references in additional occluders if (behaviour.additionalOccluders.RemoveAll((x) => x == null) > 0) { Debug.LogWarning($"Stripped some null references in {nameof(behaviour.additionalOccluders)}"); #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(behaviour); #endif } foreach (Renderer r in behaviour.additionalOccluders) { copyAdditionalOccluders.Add(r); } // Strip additional occluders that are already referenced by this behaviour HashSet allReferencedRenderers = new HashSet(); foreach (PerfectCullingBakeGroup group in behaviour.bakeGroups) { foreach (var r in group.renderers) { allReferencedRenderers.Add(r); } } // Only keep occluders unreferenced copyAdditionalOccluders = new HashSet(copyAdditionalOccluders.Where((x) => !allReferencedRenderers.Contains(x))); if (cullAdditionalOccluders) { behaviour.CullAdditionalOccluders(ref copyAdditionalOccluders); } // We cannot perform this in play mode due to static batching. // So lets do it here. bool cleanupInvalidRenderers = false; foreach (PerfectCullingBakeGroup group in behaviour.bakeGroups) { if (!group.CollectMeshStats()) { if (!PerfectCullingEditorUtil.DisplayDialog("Error: Invalid renderers detected!", "Error: Bake groups contain references to invalid renderers.\n\nExamples:\n- Renderer is null\n- MeshFilter is null\n- Mesh is null", "Remove invalid renderers", "Cancel")) { yield return new PerfectCullingBakeNotStartedYieldInstruction(); } cleanupInvalidRenderers = true; break; } } if (cleanupInvalidRenderers) { foreach (PerfectCullingBakeGroup group in behaviour.bakeGroups) { group.RemoveInvalidRenderers(); if (!group.CollectMeshStats()) { PerfectCullingLogger.LogError("Failed to clean-up invalid renderers."); yield return new PerfectCullingBakeNotStartedYieldInstruction(); } } } foreach (var monoGroup in allMonoGroups) { monoGroup.PreSceneSave(behaviour); } UnityEditor.EditorUtility.SetDirty(behaviour); UnityEditor.EditorUtility.SetDirty(behaviour.BakeData); if (saveScene && UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().path != PerfectCullingConstants.MultiSceneTempPath) { if (!PerfectCullingEditorUtil.SaveModifiedScenesIfUserWantsTo(new Scene[] { UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene() })) { yield return new PerfectCullingBakeNotStartedYieldInstruction(); } } // Needs to happen after saving (path might have changed)! Scene currentScene = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene(); currentScenePath = currentScene.path; PerfectCullingBakingManager.VerifyCurrentScenePath(currentScenePath); PerfectCullingLogger.Log(currentScenePath); Scene newScene = string.IsNullOrEmpty(UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene() .name) // Check if untitled scene ? currentScene : UnityEditor.SceneManagement.EditorSceneManager.NewScene( UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Additive); UnityEditor.SceneManagement.EditorSceneManager.MergeScenes(currentScene, newScene); UnityEditor.SceneManagement.EditorSceneManager.SetActiveScene(newScene); foreach (var monoGroup in allMonoGroups) { monoGroup.PreBake(behaviour); } behaviour.BakeData.bakeCompleted = false; behaviour.BakeData.PrepareForBake(behaviour); behaviour.InitializeAllSamplingProviders(); List worldPositions = behaviour.GetSamplingPositions(Space.World); List samplingLocations = new List(worldPositions.Count); int activeSamplingPositionsCount = 0; for (int i = 0; i < worldPositions.Count; ++i) { Vector3 pos = worldPositions[i]; bool active = behaviour.SamplingProvidersIsPositionActive(worldPositions[i]); samplingLocations.Add(new PerfectCullingBakeSettings.SamplingLocation(pos, active)); activeSamplingPositionsCount += active ? 1 : 0; } PerfectCullingBakeSettings bakeSettings = new PerfectCullingBakeSettings() { Groups = behaviour.bakeGroups, AdditionalOccluders = copyAdditionalOccluders, ActiveSamplingPositionCount = activeSamplingPositionsCount, SamplingLocations = samplingLocations, Width = PerfectCullingSettings.Instance.bakeCameraResolutionWidth, Height = PerfectCullingSettings.Instance.bakeCameraResolutionHeight }; using (PerfectCullingBaker baker = PerfectCullingBakerFactory.CreateBaker(bakeSettings)) { System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); List localPositions = behaviour.GetSamplingPositions(); int totalBatchCounts = activeSamplingPositionsCount / baker.BatchCount; int currentBatchCount = 0; List pending = new List(localPositions.Count); const float SMOOTHING_FACTOR = 0.005f; float lastTime = Time.realtimeSinceStartup; int lastElement = 0; float lastSpeed = -1f; float averageSpeed = PerfectCullingSettings.Instance.bakeAverageSamplingSpeedMs / 1000f; int bakedCellCount = 0; for (int i = 0; i < localPositions.Count; ++i) { // We use bakedCellCount instead of i because we might not bake all cells string strBakingTitle = $"[ETA {PerfectCullingEditorUtil.FormatSeconds((activeSamplingPositionsCount - bakedCellCount) * averageSpeed)}], Avg. speed: {System.Math.Round(averageSpeed * 1000f, 2)} ms | "; if (!samplingLocations[i].Active) { // Don't validate. We don't want warnings for cells that are empty on purpose. behaviour.BakeData.SetRawData(i, System.Array.Empty(), false); continue; } ++bakedCellCount; PerfectCullingBakerHandle handle = baker.SamplePosition(behaviour.transform.rotation * localPositions[i] + behaviour.transform.position); pending.Add(new PerfectCullingBakeHandle() { Index = i, Handle = handle }); if (pending.Count >= baker.BatchCount) { // We call this here to grant some additional time to the GPU while we clean-up System.GC.Collect(); if (UnityEditor.EditorUtility.DisplayCancelableProgressBar( strBakingTitle + "Performing readback ", strBakingTitle + "Performing readback", (currentBatchCount / (float)totalBatchCounts))) { // Just so we properly Dispose() them. CompletePending(behaviour.BakeData, pending); yield return new PerfectCullingBakeAbortedYieldInstruction(); } CompletePending(behaviour.BakeData, pending); // Give Unity some breathing room. // This seems important because Unity internally might not de-allocate some resources otherwise. yield return null; ++currentBatchCount; if (UnityEditor.EditorUtility.DisplayCancelableProgressBar( strBakingTitle + $"Batch: {currentBatchCount}/{totalBatchCounts} ", "Performing sampling batches...", (currentBatchCount / (float)totalBatchCounts))) { // Just so we properly Dispose() them. CompletePending(behaviour.BakeData, pending); yield return new PerfectCullingBakeAbortedYieldInstruction(); } lastSpeed = (Time.realtimeSinceStartup - lastTime) / (currentBatchCount - lastElement) / (float)baker.BatchCount; averageSpeed = SMOOTHING_FACTOR * lastSpeed + (1 - SMOOTHING_FACTOR) * averageSpeed; lastTime = Time.realtimeSinceStartup; lastElement = currentBatchCount; } } if (UnityEditor.EditorUtility.DisplayCancelableProgressBar($"Finishing pending batches", "Finishing pending batches", (currentBatchCount / (float)totalBatchCounts))) { // Just so we properly Dispose() them. CompletePending(behaviour.BakeData, pending); yield return new PerfectCullingBakeAbortedYieldInstruction(); } CompletePending(behaviour.BakeData, pending); sw.Stop(); PerfectCullingLogger.Log( $"Bake time: {PerfectCullingEditorUtil.FormatSeconds(sw.ElapsedMilliseconds * 0.001f)} | {(sw.ElapsedMilliseconds / (float)localPositions.Count)} ms per sample"); if (PerfectCullingSettings.Instance.autoUpdateBakeAverageSamplingSpeedMs) { PerfectCullingSettings.Instance.bakeAverageSamplingSpeedMs = (sw.ElapsedMilliseconds / (float)localPositions.Count); UnityEditor.EditorUtility.SetDirty(PerfectCullingSettings.Instance); } if (UnityEditor.EditorUtility.DisplayCancelableProgressBar($"Performing post bake steps", "Performing post bake steps", (currentBatchCount / (float)totalBatchCounts))) { // Just so we properly Dispose() them. CompletePending(behaviour.BakeData, pending); yield return new PerfectCullingBakeAbortedYieldInstruction(); } behaviour.PostBake(); foreach (PerfectCullingMonoGroup monoGroup in allMonoGroups) { monoGroup.PostBake(behaviour); } if (UnityEditor.EditorUtility.DisplayCancelableProgressBar($"Compressing data and finishing bake", "Compressing data and finishing bake", (currentBatchCount / (float)totalBatchCounts))) { // Just so we properly Dispose() them. CompletePending(behaviour.BakeData, pending); yield return new PerfectCullingBakeAbortedYieldInstruction(); } behaviour.BakeData.CompleteBake(); behaviour.BakeData.strBakeDate = System.DateTime.UtcNow.ToString("o"); behaviour.BakeData.strRendererName = baker.RendererName; behaviour.BakeData.bakeDurationMilliseconds = sw.ElapsedMilliseconds; } // Hash calculation needs to happen here to make sure we Disposed PerfectCullingSceneColor because it might have temporarily modified the BakeGroup behaviour.BakeData.bakeHash = behaviour.GetBakeHash(); behaviour.BakeData.bakeCompleted = true; UnityEditor.EditorUtility.SetDirty(behaviour.BakeData); PerfectCullingEditorUtil.SaveAssetIfDirty(behaviour.BakeData); } finally { UnityEditor.EditorUtility.ClearProgressBar(); } #endif } private static void CompletePending(PerfectCullingBakeData BakeData, List pending) { for (int k = 0; k < pending.Count; ++k) { pending[k].Handle.Complete(); BakeData.SetRawData(pending[k].Index, pending[k].Handle.indices); } pending.Clear(); } } }