Project_WL/Assets/ThirdParty/Koenigz/Perfect Culling/Scripts/PerfectCullingBakingBehavio...

509 lines
22 KiB
C#
Raw Normal View History

2024-12-15 01:39:39 +00:00
// Perfect Culling (C) 2021 Patrick König
//
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
namespace Koenigz.PerfectCulling
{
/// <summary>
/// This is the base class that provides an easy interface for baking.
/// Hopefully the built-in baking scripts are sufficient but if you need something else you can inherit it and roll your own.
/// </summary>
public abstract class PerfectCullingBakingBehaviour : MonoBehaviour
{
public enum EOutOfBoundsBehaviour
{
ClampToNearestCell,
Cull,
IgnoreDoNothing,
}
[Tooltip("Try to find a non-empty cell if we hit an empty one?")]
public bool searchForNonEmptyCells = false;
[Tooltip("What should happen if we encounter an empty cell? Cull everything or make everything visible?")]
public EEmptyCellCullBehaviour emptyCellCullBehaviour = EEmptyCellCullBehaviour.CullEverything;
[Tooltip("Should this volume be culled if the camera is not inside it or should the camera position be clamped to the nearest cell?")]
public EOutOfBoundsBehaviour outOfBoundsBehaviour = EOutOfBoundsBehaviour.ClampToNearestCell;
private static DefaultActiveSamplingProvider DefaultSamplingProvider = new DefaultActiveSamplingProvider();
public HashSet<IActiveSamplingProvider> SamplingProviders = new HashSet<IActiveSamplingProvider>()
{
DefaultSamplingProvider
};
public void AddSamplingProvider(IActiveSamplingProvider samplingProvider) =>
SamplingProviders.Add(samplingProvider);
public void RemoveSamplingProvider(IActiveSamplingProvider samplingProvider) =>
SamplingProviders.Remove(samplingProvider);
public void InitializeAllSamplingProviders()
{
foreach (var provider in SamplingProviders)
{
provider.InitializeSamplingProvider();
}
}
public bool SamplingProvidersIsPositionActive(Vector3 pos)
{
// PerfectCullingAlwaysIncludeVolume takes over priority as it allows to pull cells back in.
foreach (PerfectCullingAlwaysIncludeVolume alwaysIncludeVolume in DefaultSamplingProvider.AlwaysIncludeVolumes)
{
if (alwaysIncludeVolume.IsPositionActive(this, pos))
{
return true;
}
}
foreach (IActiveSamplingProvider provider in SamplingProviders)
{
if (!provider.IsSamplingPositionActive(this, pos))
{
return false;
}
}
return true;
}
[SerializeField] public PerfectCullingBakeGroup[] bakeGroups = System.Array.Empty<PerfectCullingBakeGroup>(); // Important to initialize or AddRange will fail
[SerializeField] public List<Renderer> additionalOccluders = new List<Renderer>();
public virtual PerfectCullingBakeData BakeData { get; } = null;
[System.NonSerialized] public int TotalVertexCount = 0;
private bool[] m_renderersState;
public virtual void Start()
{
TotalVertexCount = 0;
m_renderersState = new bool[bakeGroups.Length];
foreach (PerfectCullingBakeGroup group in bakeGroups)
{
group.Init();
#if UNITY_EDITOR
TotalVertexCount += group.vertexCount;
#endif
}
}
public IEnumerator PerformBakeAsync(bool saveScene, HashSet<Renderer> additionalOccludersHashset)
{
#if !UNITY_EDITOR
yield break;
#else
string currentScenePath = null;
try
{
if (bakeGroups.Length <= 0)
{
UnityEditor.EditorUtility.DisplayDialog("No renderers", "No renderers. Nothing to bake", "OK");
yield return new PerfectCullingBakeNotStartedYieldInstruction();
}
if (bakeGroups.Length == 1)
{
Debug.LogError($"{nameof(bakeGroups)} contains only one element thus no occlusion is possible. Each Renderer should go into it's own {nameof(PerfectCullingBakeGroup)}. Consider using {nameof(PerfectCullingUtil.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 (!PreBake())
{
yield return new PerfectCullingBakeNotStartedYieldInstruction();
}
PerfectCullingMonoGroup[] allMonoGroups = PerfectCullingUtil.FindMonoGroupsForBakingBehaviour(this);
HashSet<Renderer> copyAdditionalOccluders = new HashSet<Renderer>();
if (additionalOccludersHashset != null)
{
foreach (Renderer r in additionalOccludersHashset)
{
copyAdditionalOccluders.Add(r);
}
}
// Strip null references in additional occluders
if (additionalOccluders.RemoveAll((x) => x == null) > 0)
{
Debug.LogWarning($"Stripped some null references in {nameof(additionalOccluders)}");
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
foreach (Renderer r in additionalOccluders)
{
copyAdditionalOccluders.Add(r);
}
// Strip additional occluders that are already referenced by this behaviour
HashSet<Renderer> allReferencedRenderers = new HashSet<Renderer>();
foreach (PerfectCullingBakeGroup group in bakeGroups)
{
foreach (var r in group.renderers)
{
allReferencedRenderers.Add(r);
}
}
// Only keep occluders unreferenced
copyAdditionalOccluders = new HashSet<Renderer>(copyAdditionalOccluders.Where((x) => !allReferencedRenderers.Contains(x)));
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 bakeGroups)
{
if (!group.CollectMeshStats())
{
if (!UnityEditor.EditorUtility.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 bakeGroups)
{
group.RemoveInvalidRenderers();
if (!group.CollectMeshStats())
{
Debug.LogError("Failed to clean-up invalid renderers.");
yield return new PerfectCullingBakeNotStartedYieldInstruction();
}
}
}
foreach (var monoGroup in allMonoGroups)
{
monoGroup.PreSceneSave(this);
}
UnityEditor.EditorUtility.SetDirty(this);
UnityEditor.EditorUtility.SetDirty(BakeData);
if (saveScene && UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().path != PerfectCullingConstants.MultiSceneTempPath)
{
if (!UnityEditor.SceneManagement.EditorSceneManager.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);
Debug.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(this);
}
BakeData.bakeCompleted = false;
BakeData.PrepareForBake(this);
InitializeAllSamplingProviders();
List<Vector3> worldPositions = GetSamplingPositions(Space.World);
List<PerfectCullingBakeSettings.SamplingLocation> samplingLocations =
new List<PerfectCullingBakeSettings.SamplingLocation>(worldPositions.Count);
int activeSamplingPositionsCount = 0;
for (int i = 0; i < worldPositions.Count; ++i)
{
Vector3 pos = worldPositions[i];
bool active = SamplingProvidersIsPositionActive(worldPositions[i]);
samplingLocations.Add(new PerfectCullingBakeSettings.SamplingLocation(pos, active));
activeSamplingPositionsCount += active ? 1 : 0;
}
PerfectCullingBakeSettings bakeSettings = new PerfectCullingBakeSettings()
{
Groups = 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<Vector3> localPositions = GetSamplingPositions();
int totalBatchCounts = activeSamplingPositionsCount / baker.BatchCount;
int currentBatchCount = 0;
List<PerfectCullingBakeHandle> pending = new List<PerfectCullingBakeHandle>(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 {PerfectCullingUtil.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.
BakeData.SetRawData(i, System.Array.Empty<ushort>(), false);
continue;
}
++bakedCellCount;
PerfectCullingBakerHandle handle = baker.SamplePosition(transform.rotation * localPositions[i] + 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(pending);
yield return new PerfectCullingBakeAbortedYieldInstruction();
}
CompletePending(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(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(pending);
yield return new PerfectCullingBakeAbortedYieldInstruction();
}
CompletePending(pending);
sw.Stop();
Debug.Log($"Bake time: {PerfectCullingUtil.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(pending);
yield return new PerfectCullingBakeAbortedYieldInstruction();
}
PostBake();
foreach (PerfectCullingMonoGroup monoGroup in allMonoGroups)
{
monoGroup.PostBake(this);
}
if (UnityEditor.EditorUtility.DisplayCancelableProgressBar($"Compressing data and finishing bake",
"Compressing data and finishing bake",
(currentBatchCount / (float) totalBatchCounts)))
{
// Just so we properly Dispose() them.
CompletePending(pending);
yield return new PerfectCullingBakeAbortedYieldInstruction();
}
BakeData.CompleteBake();
BakeData.strBakeDate = System.DateTime.UtcNow.ToString("o");
BakeData.strRendererName = baker.RendererName;
BakeData.bakeDurationMilliseconds = sw.ElapsedMilliseconds;
}
// Hash calculation needs to happen here to make sure we Disposed PerfectCullingSceneColor because it might have temporarily modified the BakeGroup
BakeData.bakeHash = GetBakeHash();
BakeData.bakeCompleted = true;
UnityEditor.EditorUtility.SetDirty(BakeData);
UnityEditor.AssetDatabase.SaveAssets();
}
finally
{
UnityEditor.EditorUtility.ClearProgressBar();
}
#endif
}
void CompletePending(List<PerfectCullingBakeHandle> pending)
{
for (int k = 0; k < pending.Count; ++k)
{
pending[k].Handle.Complete();
BakeData.SetRawData(pending[k].Index, pending[k].Handle.indices);
}
pending.Clear();
}
public void QueueToggleAllRenderers(bool state)
{
for (var index = 0; index < bakeGroups.Length; index++)
{
m_renderersState[index] = state;
}
}
/// <summary>
/// Queue up renderer state change. This will not take effect until ExecuteQueue was called.
/// </summary>
public void QueueToggleRenderer(int index, bool state, out PerfectCullingBakeGroup modifiedBakeGroup)
{
m_renderersState[index] = state;
modifiedBakeGroup = bakeGroups[index];
}
/// <summary>
/// Applies renderers state changes.
/// </summary>
public void ExecuteQueue(bool forceNullCheck = false)
{
for (var index = 0; index < bakeGroups.Length; index++)
{
PerfectCullingBakeGroup r = bakeGroups[index];
r.Toggle(m_renderersState[index], forceNullCheck);
}
}
public virtual void SetBakeData(PerfectCullingBakeData bakeData) => throw new System.NotImplementedException();
public virtual List<Vector3> GetSamplingPositions(Space space = Space.Self) => throw new System.NotImplementedException();
public virtual void GetIndicesForWorldPos(Vector3 worldPos, List<ushort> indices) => throw new System.NotImplementedException();
public virtual int GetIndexForWorldPos(Vector3 worldPos, out bool isOutOfBounds) => throw new System.NotImplementedException();
public virtual void GetIndicesForIndex(int index, List<ushort> indices) =>
BakeData.SampleAtIndex(index, indices);
public virtual bool PreBake() => throw new System.NotImplementedException();
public virtual void PostBake() => throw new System.NotImplementedException();
public virtual int GetBakeHash() => throw new System.NotImplementedException();
protected virtual void CullAdditionalOccluders(ref HashSet<Renderer> additionalOccluders) =>
throw new System.NotImplementedException();
}
}