TG9six 03a642d635 first push
first push
2025-09-06 17:17:39 +04:00

504 lines
19 KiB
C#

// 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<PerfectCullingVolume> AllVolumes = new List<PerfectCullingVolume>();
[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 Vector3Int[] mergeDownsampleAxes = System.Array.Empty<Vector3Int>();
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<ushort> indices)
{
int flat = GetIndexForWorldPos(worldPos, volumeBakeData.cellSize, out bool _);
GetIndicesForIndex(flat, indices);
}
public override List<Vector3> GetSamplingPositions(Space space = Space.Self)
{
List<Vector3> samplePositions = new List<Vector3>(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
// TODO: We cannot use PerfectCullingEditorUtil.DisplayDialog here because it is not in the same assembly.
// It is not a big deal because we do not act on the return value though.
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;
}
public override void CullAdditionalOccluders(ref HashSet<Renderer> additionalOccluders)
{
if (additionalOccluders == null)
{
return;
}
Bounds bakeBounds = new Bounds(transform.position, volumeSize);
HashSet<Renderer> relevantOccluders = new HashSet<Renderer>();
foreach (Renderer r in additionalOccluders)
{
if (!bakeBounds.Intersects(r.bounds))
{
continue;
}
relevantOccluders.Add(r);
}
additionalOccluders = relevantOccluders;
}
public override void PostBake()
{
if (mergeDownsampleAxes.Length != mergeDownsampleIterations)
{
System.Array.Resize(ref mergeDownsampleAxes, mergeDownsampleIterations);
for (int i = 0; i < mergeDownsampleIterations; ++i)
{
if (mergeDownsampleAxes[i] == Vector3Int.zero)
{
mergeDownsampleAxes[i] = Vector3Int.one;
}
}
#if UNITY_EDITOR
EditorUtility.SetDirty(this);
#endif
}
for (int i = 0; i < mergeDownsampleIterations; ++i)
{
#if UNITY_EDITOR
UnityEditor.EditorUtility.DisplayProgressBar(
$"Performing Merge-Downsample step ({mergeDownsampleAxes.ToString()}", $"Performing Merge-Downsample iteration {i + 1}/{mergeDownsampleIterations}", i / (float) mergeDownsampleIterations);
#endif
volumeBakeData.MergeDownsample(mergeDownsampleAxes[i]);
}
}
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<PerfectCullingPortalCell> portalCells = new List<PerfectCullingPortalCell>();
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
}
}