// Perfect Culling (C) 2021 Patrick König // using System; using System.Collections; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Serialization; namespace Koenigz.PerfectCulling { public class PerfectCullingVolume : PerfectCullingBakingBehaviour, CustomHandle.IResizableByHandle { [FormerlySerializedAs("VolumeSize")] [SerializeField] public Vector3 volumeSize = Vector3.one; [SerializeField] public Bounds volumeBakeBounds { get => new Bounds(transform.position, volumeSize); set { // TODO: Causes annoying offset, gonna need to solve this in a different way. transform.position = value.center; volumeSize = new Vector3( Mathf.Max(1, value.size.x), Mathf.Max(1, value.size.y), Mathf.Max(1, value.size.z)); } } public int CellCount => PerfectCullingMath.CalculateNumberOfCells(volumeSize, bakeCellSize); public bool VisualizeProbes { get; set; } public bool VisualizeGridCells { get; set; } public bool VisualizeHitLines { get; set; } public int RenderersCount => bakeGroups.Length; public static readonly List AllVolumes = new List(); [FormerlySerializedAs("VolumeBakeData")] public PerfectCullingVolumeBakeData volumeBakeData; public override PerfectCullingBakeData BakeData => volumeBakeData; #if false [Tooltip("Allows to prioritize volumes.")] [Range(1, 100)] public int priority = 1; #endif [FormerlySerializedAs("BakeCellSize")] [Tooltip("The size of a single cell. This needs to be a divisor of the scale of the volume.")] [SerializeField] public Vector3 bakeCellSize = new Vector3(10, 5, 10); [FormerlySerializedAs("MergeDownsampleIterations")] [Tooltip("After the bake completed for each cell all neighbor cells are merged into a single cell. This will reduce the number of cells without introducing culling issues. This is useful to reduce memory usage.")] [Range(0, 8)] public int mergeDownsampleIterations = 0; public PerfectCullingVisibilityLayer visibilityLayer = PerfectCullingVisibilityLayer.All; private void OnEnable() { // We got no baked data. We are useless and shouldn't even bother adding ourselves to the list. if (volumeBakeData == null || volumeBakeData.data.Length <= 0) { return; } AllVolumes.Add(this); SortVolumes(); } private void OnDisable() { // Execute this before ToggleAllRenderers - just in case we run into an exception. AllVolumes.Remove(this); SortVolumes(); // We turn everything back on because it might still be disabled from the previous frame. QueueToggleAllRenderers(true); // Take effect immediately // We also force null checks because OnDisable() might have been called as part of an active destruction process (scene change, etc.) ExecuteQueue(true); } private void SortVolumes() { #if false AllVolumes.Sort((v1, v2) => v1.priority.CompareTo(v2.priority)); #endif } private void OnDestroy() { // Makes sure that we don't keep this object alive volumeBakeData = null; } #if UNITY_EDITOR private void OnValidate() { Vector3 nScale = new Vector3((int)volumeSize.x, (int)volumeSize.y, (int)volumeSize.z); // Make sure the scale of the object actually supports this cell size bakeCellSize = new Vector3( Mathf.Min(bakeCellSize.x, nScale.x), Mathf.Min(bakeCellSize.y, nScale.y), Mathf.Min(bakeCellSize.z, nScale.z)); } private void OnDrawGizmos() { Matrix4x4 oldMatrix = Gizmos.matrix; Matrix4x4 newMatrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one); Gizmos.matrix = newMatrix; Handles.matrix = newMatrix; //Gizmos.color = Color.blue; //Gizmos.DrawWireCube(Vector3.zero, volumeSize * 1.01f); if (!VisualizeProbes && !VisualizeGridCells) { return; } Vector3 cellCount = PerfectCullingMath.CalculateCellCount(volumeSize, bakeCellSize); Handles.color = new Color(0.5f, 0.25f, 0f, 0.35f); Handles.zTest = CompareFunction.LessEqual; InitializeAllSamplingProviders(); { Camera currentViewCam = SceneView.currentDrawingSceneView.camera; int camSampleIndex = GetIndexForWorldPos(currentViewCam.transform.position, bakeCellSize, out bool _, false); PerfectCullingMath.UnflattenToXYZ(camSampleIndex, out int x, out int y, out int z, cellCount); const int Radius = 6; for (int xx = -Radius; xx < Radius; ++xx) { for (int yy = -Radius; yy < Radius; ++yy) { for (int zz = -Radius; zz < Radius; ++zz) { if (!PerfectCullingMath.IsXYZInBounds(x + xx, y + yy, z + zz, cellCount)) { continue; } int neighborCellIndex = PerfectCullingMath.FlattenXYZ(x + xx, y + yy, z + zz, cellCount); Vector3 pos = GetSamplingPositionAt(neighborCellIndex, bakeCellSize); if (VisualizeGridCells) { Handles.DrawWireCube(pos, bakeCellSize); } if (!SamplingProvidersIsPositionActive( GetSamplingPositionAt(neighborCellIndex, bakeCellSize, Space.World))) { continue; } if (VisualizeProbes) { Handles.SphereHandleCap(-1, pos, Quaternion.identity, 0.5f, EventType.Repaint); } } } } } if (volumeBakeData == null) { return; } foreach (PerfectCullingCamera camera in PerfectCullingCamera.AllCameras) { int i = GetIndexForWorldPos(camera.transform.position, bakeCellSize, out bool _); Vector3 pos = GetSamplingPositionAt(i, bakeCellSize); Gizmos.color = new Color(0.5f, 0f, 0f, 0.5f); Gizmos.DrawCube(pos, bakeCellSize); Gizmos.color = new Color(0.0f, 0.25f, 0f, 0.5f); Gizmos.matrix = oldMatrix; if (bakeCellSize == volumeBakeData.cellSize) { PerfectCullingTemp.ListUshort.Clear(); BakeData.SampleAtIndex(i, PerfectCullingTemp.ListUshort); foreach (ushort index in PerfectCullingTemp.ListUshort) { if (!VisualizeHitLines) { break; } bakeGroups[index].ForeachRenderer((renderer) => { Gizmos.DrawLine(transform.rotation * pos + transform.position, renderer.bounds.center); }); } } else { int otherI = GetIndexForWorldPos(camera.transform.position, volumeBakeData.cellCount, volumeBakeData.cellSize, out bool _); PerfectCullingTemp.ListUshort.Clear(); BakeData.SampleAtIndex(otherI, PerfectCullingTemp.ListUshort); foreach (ushort index in PerfectCullingTemp.ListUshort) { if (!VisualizeHitLines) { break; } bakeGroups[index].ForeachRenderer((renderer) => { Gizmos.DrawLine(transform.rotation * pos + transform.position, renderer.bounds.center); }); } Vector3 otherPos = GetSamplingPositionAt(otherI, volumeBakeData.cellCount, volumeBakeData.cellSize); Vector3 bakedVolumeSize = Vector3.Scale(volumeBakeData.cellCount, volumeBakeData.cellSize); Vector3 diff = volumeSize - bakedVolumeSize; Gizmos.color = new Color(1, 1, 0, 0.2f); Gizmos.matrix = newMatrix; Gizmos.DrawCube(otherPos - 0.5f * diff, volumeBakeData.cellSize); Gizmos.matrix = oldMatrix; } Gizmos.matrix = newMatrix; } } #endif public override void GetIndicesForWorldPos(Vector3 worldPos, List indices) { int flat = GetIndexForWorldPos(worldPos, volumeBakeData.cellSize, out bool _); GetIndicesForIndex(flat, indices); } public override List GetSamplingPositions(Space space = Space.Self) { List samplePositions = new List(CellCount); for (int rawIndexCell = 0; rawIndexCell < CellCount; ++rawIndexCell) { samplePositions.Add(GetSamplingPositionAt(rawIndexCell, bakeCellSize, space)); } return samplePositions; } public override bool PreBake() { // Needs to be an integer for clean divisions volumeSize = new Vector3( (int) volumeSize.x, (int) volumeSize.y, (int) volumeSize.z); volumeBakeData.SetVolumeBakeData(this); if ((int)volumeBakeData.cellCount.x == 0 || (int)volumeBakeData.cellCount.y == 0 || (int)volumeBakeData.cellCount.z == 0) { #if UNITY_EDITOR UnityEditor.EditorUtility.DisplayDialog("Invalid cell size.", "The cell size is invalid. Please double check that the scale of your volume supports at least one cell of the given size.", "OK"); #endif return false; } return true; } protected override void CullAdditionalOccluders(ref HashSet additionalOccluders) { if (additionalOccluders == null) { return; } Bounds bakeBounds = new Bounds(transform.position, volumeSize); HashSet relevantOccluders = new HashSet(); foreach (Renderer r in additionalOccluders) { if (!bakeBounds.Intersects(r.bounds)) { continue; } relevantOccluders.Add(r); } additionalOccluders = relevantOccluders; } public override void PostBake() { for (int i = 0; i < mergeDownsampleIterations; ++i) { #if UNITY_EDITOR UnityEditor.EditorUtility.DisplayProgressBar( "Performing Merge-Downsample step", $"Performing Merge-Downsample iteration {i + 1}/{mergeDownsampleIterations}", i / (float) mergeDownsampleIterations); #endif volumeBakeData.MergeDownsample(); } } public Vector3 AlignToGrid(Vector3 pos) { int idx = GetIndexForWorldPos(pos, bakeCellSize, out bool _); return GetSamplingPositionAt(idx, bakeCellSize, Space.World); } public override int GetIndexForWorldPos(Vector3 pos, out bool isOutOfBounds) { return GetIndexForWorldPos(pos, volumeBakeData.cellSize, out isOutOfBounds); } private int GetIndexForWorldPos(Vector3 pos, Vector3 cellSize, out bool isOutOfBounds, bool emptyCellScanAllowed = true) { Vector3 cellCount = PerfectCullingMath.CalculateCellCount(volumeSize, cellSize); return GetIndexForWorldPos(pos, cellCount, cellSize, out isOutOfBounds, emptyCellScanAllowed); } private List portalCells = new List(); public void AddPortalCell(PerfectCullingPortalCell portalCell) => portalCells.Add(portalCell); public void RemovePortalCell(PerfectCullingPortalCell portalCell) => portalCells.Remove(portalCell); private int GetIndexForWorldPos(Vector3 pos, Vector3 cellCount, Vector3 cellSize, out bool isOutOfBounds, bool emptyCellScanAllowed = true) { Quaternion orientation = volumeBakeData == null ? transform.rotation : volumeBakeData.orientation; int index = PerfectCullingMath.GetIndexForWorldPos(pos, transform.position, transform.rotation, volumeSize, orientation, cellCount, cellSize, out isOutOfBounds); if (isOutOfBounds) { PerfectCullingPortalCell closest = null; float dist = float.MaxValue; foreach (PerfectCullingPortalCell cell in portalCells) { float thisDist = (pos - cell.transform.position).sqrMagnitude; if (dist > thisDist) { closest = cell; dist = thisDist; } } if (closest != null) { return PerfectCullingMath.GetIndexForWorldPos(closest.transform.position, transform.position, transform.rotation, volumeSize, orientation, cellCount, cellSize, out isOutOfBounds); } } if (searchForNonEmptyCells && emptyCellScanAllowed) { int newIndex = volumeBakeData.SearchIndexForClosestNonEmptyCell(index); if (newIndex != -1) { index = newIndex; } } return index; } public override void SetBakeData(PerfectCullingBakeData bakeData) => volumeBakeData = bakeData as PerfectCullingVolumeBakeData; private Vector3 GetSamplingPositionAt(int index, Vector3 cellSize, Space space = Space.Self) { Vector3 gridSize = new Vector3(volumeSize.x / cellSize.x, volumeSize.y / cellSize.y, volumeSize.z / cellSize.z); return GetSamplingPositionAt(index, gridSize, cellSize, space); } private Vector3 GetSamplingPositionAt(int index, Vector3 gridSize, Vector3 cellSize, Space space = Space.Self) { Vector3 halfGridSize = gridSize * 0.5f; PerfectCullingMath.UnflattenToXYZ(index, out int x, out int y, out int z, gridSize); Vector3 localPos = (cellSize / 2 + new Vector3(x * cellSize.x, y * cellSize.y, z * cellSize.z) - new Vector3( halfGridSize.x * cellSize.x, halfGridSize.y * cellSize.y, halfGridSize.z * cellSize.z)); if (space == Space.World) { return transform.position + transform.rotation * localPos; } return localPos; } public Vector3 HandleSized { get => volumeBakeBounds.size; set => volumeBakeBounds = new Bounds(transform.position, value); } public override int GetBakeHash() { int hash = 13; unchecked { hash = hash * 17 + bakeGroups.Length; for (int i = 0; i < bakeGroups.Length; ++i) { hash = hash * 53 + (int)bakeGroups[i].groupType; hash = hash * 23 + bakeGroups[i].renderers.Length; } hash = hash * 41 + additionalOccluders.Count; } return hash; } #if UNITY_EDITOR private void OnDrawGizmosSelected() { // We draw this here so it will render during multi-selection Matrix4x4 trs = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one); Handles.color = new Color(0, 0, 1, 1.0f); Handles.zTest = CompareFunction.LessEqual; Handles.lighting = false; Handles.matrix = trs; Handles.DrawWireCube(Vector3.zero, volumeSize); // CubeHandleCap doesn't allow to specify non-uniform scale so we need to mess with the matrix Handles.matrix = trs * Matrix4x4.Scale(volumeSize); Handles.color = new Color(0, 0, 1, PerfectCullingConstants.VolumeInsideAlpha); Handles.CubeHandleCap(-1, Vector3.zero, Quaternion.identity, 1.0f, EventType.Repaint); } #endif } }