573 lines
18 KiB
C#
573 lines
18 KiB
C#
// Perfect Culling (C) 2021 Patrick König
|
|
//
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Koenigz.PerfectCulling.IO;
|
|
using UnityEngine;
|
|
|
|
namespace Koenigz.PerfectCulling
|
|
{
|
|
[PreferBinarySerialization]
|
|
public class PerfectCullingVolumeBakeData : PerfectCullingBakeData
|
|
{
|
|
[System.Serializable]
|
|
public struct VisibilitySet
|
|
{
|
|
public byte[] compressed;
|
|
public ushort len;
|
|
}
|
|
|
|
[System.Serializable]
|
|
public struct RawData
|
|
{
|
|
public ushort[] uncompressed;
|
|
}
|
|
|
|
public Vector3 cellCount;
|
|
public Vector3 cellSize;
|
|
|
|
public Quaternion orientation;
|
|
|
|
[HideInInspector]
|
|
public VisibilitySet[] data;
|
|
|
|
[HideInInspector]
|
|
public RawData[] rawData;
|
|
|
|
public int maxStoredIndex = -1;
|
|
public int numberOfGroups = -1;
|
|
|
|
public void SetVolumeBakeData(PerfectCullingVolume volume)
|
|
{
|
|
cellSize = volume.bakeCellSize;
|
|
cellCount = new Vector3(Mathf.Round(volume.volumeSize.x / cellSize.x), Mathf.Round(volume.volumeSize.y / cellSize.y), Mathf.Round(volume.volumeSize.z / cellSize.z));
|
|
}
|
|
|
|
public override void PrepareForBake(PerfectCullingBakingBehaviour bakingBehaviour)
|
|
{
|
|
orientation = bakingBehaviour.transform.rotation;
|
|
|
|
data = new VisibilitySet[bakingBehaviour.GetSamplingPositions().Count];
|
|
rawData = new RawData[data.Length];
|
|
|
|
maxStoredIndex = -1;
|
|
numberOfGroups = bakingBehaviour.bakeGroups.Length;
|
|
}
|
|
|
|
private const int MaxValue = 15;
|
|
|
|
private const int HeaderBitSize = 2;
|
|
static uint[] BITS = new uint[4]
|
|
{
|
|
1,
|
|
2,
|
|
3,
|
|
4
|
|
};
|
|
|
|
public override void SetRawData(int index, ushort[] indices, bool validateData = true)
|
|
{
|
|
if (validateData && indices.Length <= 0)
|
|
{
|
|
PerfectCullingLogger.LogWarning("Cell without any visible renderers. Should be highly unlikely to happen unless you are performing a multi-scene bake with additional occluders and/or using a very small rendering distance.");
|
|
}
|
|
|
|
rawData[index] = new RawData()
|
|
{
|
|
uncompressed = indices
|
|
};
|
|
|
|
foreach (ushort i in indices)
|
|
{
|
|
maxStoredIndex = Mathf.Max(i, maxStoredIndex);
|
|
}
|
|
}
|
|
|
|
void SetDataCompressed(int index, ushort[] indices, BitStreamReader streamReader, BitStreamWriter streamWriter)
|
|
{
|
|
if (indices.Length <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
void AddValue(int value, List<int> values)
|
|
{
|
|
while (value >= MaxValue)
|
|
{
|
|
values.Add(MaxValue);
|
|
value -= MaxValue;
|
|
}
|
|
values.Add(value);
|
|
}
|
|
|
|
// Perform delta encoding
|
|
List<int> deltaValues = new List<int>();
|
|
|
|
AddValue(indices[0], deltaValues);
|
|
|
|
for (int i = 1; i < indices.Length; ++i)
|
|
{
|
|
int currentDiff = indices[i] - indices[i - 1];
|
|
|
|
AddValue(currentDiff, deltaValues);
|
|
}
|
|
|
|
// Compress
|
|
streamWriter.Reset();
|
|
|
|
for (int i = 0; i < deltaValues.Count; ++i)
|
|
{
|
|
int bitIndex; // Figure out bit rate
|
|
|
|
if (deltaValues[i] <= 1) bitIndex = 0;
|
|
else if (deltaValues[i] <= 3) bitIndex = 1;
|
|
else if (deltaValues[i] <= 7) bitIndex = 2;
|
|
else if (deltaValues[i] <= 15) bitIndex = 3;
|
|
else throw new Exception("Hmm");
|
|
|
|
streamWriter.Write((uint)bitIndex, HeaderBitSize);
|
|
streamWriter.Write((uint)deltaValues[i], (int) BITS[bitIndex]);
|
|
}
|
|
|
|
streamWriter.Flush();
|
|
|
|
byte[] finalCompressedData = new byte[streamWriter.Length];
|
|
System.Array.Copy(streamWriter.Buffer, finalCompressedData, streamWriter.Length);
|
|
|
|
data[index] = new VisibilitySet()
|
|
{
|
|
compressed = finalCompressedData.Length == 0 ? null : finalCompressedData,
|
|
len = (ushort)deltaValues.Count
|
|
};
|
|
|
|
if (PerfectCullingConstants.SafetyChecks)
|
|
{
|
|
List<ushort> test = new List<ushort>();
|
|
SampleAtIndex(index, test, streamReader);
|
|
|
|
if (indices.Length != test.Count)
|
|
{
|
|
throw new Exception("Length mismatch: " + indices.Length + " vs " + test.Count);
|
|
}
|
|
|
|
for (int i = 0; i < indices.Length; ++i)
|
|
{
|
|
if (indices[i] != test[i])
|
|
{
|
|
throw new Exception("Mismatch " + indices[i] + " " + test[i] + " @ index " + index + " : " + i + " - Maybe the array was unsorted?");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void CompleteBake()
|
|
{
|
|
if (rawData == null || rawData.Length <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We only compress the data after we finished the entire bake.
|
|
// This prevents compressing and uncompressing unnecessarily during post processing steps.
|
|
data = new VisibilitySet[rawData.Length];
|
|
|
|
const int batchSize = 32;
|
|
|
|
int totalCount = rawData.Length;
|
|
|
|
IEnumerable<IGrouping<int, int>> batches = Enumerable.Range(0, totalCount)
|
|
.GroupBy(val => (val % batchSize));
|
|
|
|
int processedElementCount = 0;
|
|
|
|
var setDataTasks = batches.Select(groups =>
|
|
{
|
|
#pragma warning disable 1998
|
|
return Task.Run(async () =>
|
|
#pragma warning restore 1998
|
|
{
|
|
BitStreamReader bitStreamReader = new BitStreamReader();
|
|
BitStreamWriter bitStreamWriter = new BitStreamWriter();
|
|
|
|
int groupSize = 0;
|
|
|
|
foreach (var index in groups)
|
|
{
|
|
System.Array.Sort(rawData[index].uncompressed);
|
|
|
|
SetDataCompressed(index, rawData[index].uncompressed, bitStreamReader, bitStreamWriter);
|
|
|
|
++groupSize;
|
|
}
|
|
|
|
System.Threading.Interlocked.Add(ref processedElementCount, groupSize);
|
|
});
|
|
});
|
|
|
|
var task = Task.WhenAll(setDataTasks);
|
|
var taskAwaiter = task.GetAwaiter();
|
|
|
|
for (int currentValue = 0; currentValue != totalCount; currentValue = System.Threading.Interlocked.CompareExchange(ref processedElementCount, 0, 0))
|
|
{
|
|
#if UNITY_EDITOR
|
|
UnityEditor.EditorUtility.DisplayProgressBar("Compress and apply cell data",$"Cell: {currentValue}/{totalCount}", currentValue / (float) totalCount);
|
|
#endif
|
|
|
|
if (task.Wait(500))
|
|
{
|
|
// Task finished within timeout and we are done.
|
|
break;
|
|
}
|
|
}
|
|
|
|
System.Diagnostics.Debug.Assert(processedElementCount == totalCount);
|
|
|
|
// Unnecessary but just for completeness.
|
|
taskAwaiter.GetResult();
|
|
|
|
/*
|
|
for (int i = 0; i < rawData.Length; ++i)
|
|
{
|
|
PerfectCullingTemp.ListUshort.Clear();
|
|
PerfectCullingTemp.ListUshort.AddRange(rawData[i].uncompressed);
|
|
PerfectCullingTemp.ListUshort.Sort();
|
|
SetDataCompressed(i, PerfectCullingTemp.ListUshort.ToArray());
|
|
}
|
|
*/
|
|
|
|
rawData = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds neighbor cell content to each cell then down-samples the entire grid.
|
|
/// </summary>
|
|
public void MergeDownsample(Vector3Int mergeAxes)
|
|
{
|
|
if (Mathf.Approximately(cellCount.x, 1) && Mathf.Approximately(cellCount.y, 1) && Mathf.Approximately(cellCount.z, 1))
|
|
{
|
|
PerfectCullingLogger.LogWarning($"Unable to down-sample any further: {cellCount}");
|
|
|
|
return;
|
|
}
|
|
|
|
PerfectCullingVolumeBakeData tmpBakeData = ScriptableObject.CreateInstance<PerfectCullingVolumeBakeData>();
|
|
|
|
if (mergeAxes == Vector3Int.zero)
|
|
{
|
|
PerfectCullingLogger.LogWarning($"Nothing to merge for provided merge axes: {mergeAxes}");
|
|
|
|
return;
|
|
}
|
|
|
|
// Half resolution; divide by two
|
|
Vector3Int OptimizedCellSize = new Vector3Int(mergeAxes.x == 0 ? 1 : 2, mergeAxes.y == 0 ? 1 : 2, mergeAxes.z == 0 ? 1 : 2);
|
|
|
|
Vector3Int newTmpDim = new Vector3Int(
|
|
(int)( (cellCount.x % OptimizedCellSize.x == 0)
|
|
? cellCount.x
|
|
: cellCount.x + OptimizedCellSize.x - (cellCount.x % OptimizedCellSize.x)),
|
|
|
|
(int)( (cellCount.y % OptimizedCellSize.y == 0)
|
|
? cellCount.y
|
|
: cellCount.y + OptimizedCellSize.y - (cellCount.y % OptimizedCellSize.y)),
|
|
|
|
(int)( (cellCount.z % OptimizedCellSize.z == 0)
|
|
? cellCount.z
|
|
: cellCount.z + OptimizedCellSize.z - (cellCount.z % OptimizedCellSize.z)));
|
|
|
|
Vector3Int optDim = new Vector3Int(
|
|
((int)newTmpDim.x / (int)OptimizedCellSize.x),
|
|
((int)newTmpDim.y / (int)OptimizedCellSize.y),
|
|
((int)newTmpDim.z / (int)OptimizedCellSize.z));
|
|
|
|
//tmpBakeData.data = new VisibilitySet[optDim.x * optDim.y * optDim.z];
|
|
tmpBakeData.rawData = new RawData[optDim.x * optDim.y * optDim.z];
|
|
|
|
tmpBakeData.cellCount = optDim;;
|
|
tmpBakeData.cellSize = new Vector3(cellSize.x * OptimizedCellSize.x, cellSize.y*OptimizedCellSize.y,
|
|
cellSize.z *OptimizedCellSize.z);
|
|
|
|
int totalCount = Mathf.RoundToInt(cellCount.x * cellCount.y * cellCount.z);
|
|
|
|
|
|
#if false
|
|
HashSet<ushort> tmpHash = new HashSet<ushort>();
|
|
|
|
for (int index = 0; index < totalCount; ++index)
|
|
{
|
|
PerfectCullingMath.UnflattenToXYZ(index, out int x, out int y, out int z, cellCount);
|
|
|
|
int optX = x / (int)OptimizedCellSize.x;
|
|
int optY = y / (int)OptimizedCellSize.y;
|
|
int optZ = z / (int)OptimizedCellSize.z;
|
|
|
|
int tmpBakeDataSampleIndex = PerfectCullingMath.FlattenXYZ(optX, optY, optZ, optDim);
|
|
|
|
tmpHash.Clear();
|
|
|
|
// Merge neighbor cells
|
|
for (int xx = -1; xx <= 1; ++xx)
|
|
{
|
|
for (int yy = -1; yy <= 1; ++yy)
|
|
{
|
|
for (int zz = -1; zz <= 1; ++zz)
|
|
{
|
|
if (!PerfectCullingMath.IsXYZInBounds(x + xx, y + yy, z + zz, cellCount))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int sampleIndex = PerfectCullingMath.FlattenXYZ(x + xx, y + yy, z + zz, cellCount);
|
|
|
|
if (rawData[sampleIndex].uncompressed == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (ushort neighborIndex in rawData[sampleIndex].uncompressed)
|
|
{
|
|
tmpHash.Add(neighborIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add existing indices back in or they would be lost
|
|
if (tmpBakeData.rawData[tmpBakeDataSampleIndex].uncompressed != null)
|
|
{
|
|
foreach (ushort existingIndex in tmpBakeData.rawData[tmpBakeDataSampleIndex].uncompressed)
|
|
{
|
|
tmpHash.Add(existingIndex);
|
|
}
|
|
}
|
|
|
|
tmpBakeData.rawData[tmpBakeDataSampleIndex].uncompressed = tmpHash.ToArray();
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
if (index % 128 == 0)
|
|
{
|
|
UnityEditor.EditorUtility.DisplayProgressBar("Performing Merge-Downsample step",
|
|
$"Cell: {index}/{totalCount}", index / (float) totalCount);
|
|
}
|
|
#endif
|
|
}
|
|
#else
|
|
const int batchSize = 32;
|
|
|
|
IEnumerable<IGrouping<int, int>> batches = Enumerable.Range(0, totalCount)
|
|
.GroupBy(val => (val % batchSize));
|
|
|
|
int processedElementCount = 0;
|
|
|
|
var downsampleTasks = batches.Select(groups =>
|
|
{
|
|
#pragma warning disable 1998
|
|
return Task.Run(async () =>
|
|
#pragma warning restore 1998
|
|
{
|
|
HashSet<ushort> tmpHash = new HashSet<ushort>();
|
|
|
|
int groupSize = 0;
|
|
|
|
foreach (var index in groups)
|
|
{
|
|
PerfectCullingMath.UnflattenToXYZ(index, out int x, out int y, out int z, cellCount);
|
|
|
|
int optX = x / (int)OptimizedCellSize.x;
|
|
int optY = y / (int)OptimizedCellSize.y;
|
|
int optZ = z / (int)OptimizedCellSize.z;
|
|
|
|
int tmpBakeDataSampleIndex = PerfectCullingMath.FlattenXYZDouble(optX, optY, optZ, optDim);
|
|
|
|
tmpHash.Clear();
|
|
|
|
// Merge neighbor cells
|
|
for (int xx = -1; xx <= 1; ++xx)
|
|
{
|
|
for (int yy = -1; yy <= 1; ++yy)
|
|
{
|
|
for (int zz = -1; zz <= 1; ++zz)
|
|
{
|
|
if (mergeAxes.x == 0 && xx != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (mergeAxes.y == 0 && yy != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (mergeAxes.z == 0 && zz != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!PerfectCullingMath.IsXYZInBounds(x + xx, y + yy, z + zz, cellCount))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int sampleIndex = PerfectCullingMath.FlattenXYZDouble(x + xx, y + yy, z + zz, cellCount);
|
|
|
|
if (rawData[sampleIndex].uncompressed == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (ushort neighborIndex in rawData[sampleIndex].uncompressed)
|
|
{
|
|
tmpHash.Add(neighborIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add existing indices back in or they would be lost
|
|
if (tmpBakeData.rawData[tmpBakeDataSampleIndex].uncompressed != null)
|
|
{
|
|
foreach (ushort existingIndex in tmpBakeData.rawData[tmpBakeDataSampleIndex].uncompressed)
|
|
{
|
|
tmpHash.Add(existingIndex);
|
|
}
|
|
}
|
|
|
|
tmpBakeData.rawData[tmpBakeDataSampleIndex].uncompressed = tmpHash.ToArray();
|
|
|
|
++groupSize;
|
|
}
|
|
|
|
System.Threading.Interlocked.Add(ref processedElementCount, groupSize);
|
|
|
|
});
|
|
});
|
|
|
|
var task = Task.WhenAll(downsampleTasks);
|
|
var taskAwaiter = task.GetAwaiter();
|
|
|
|
for (int currentValue = 0; currentValue != totalCount; currentValue = System.Threading.Interlocked.CompareExchange(ref processedElementCount, 0, 0))
|
|
{
|
|
#if UNITY_EDITOR
|
|
UnityEditor.EditorUtility.DisplayProgressBar("Performing Merge-Downsample step",
|
|
$"Cell: {currentValue}/{totalCount}", currentValue / (float) totalCount);
|
|
#endif
|
|
|
|
if (task.Wait(500))
|
|
{
|
|
// Task finished within timeout and we are done.
|
|
break;
|
|
}
|
|
}
|
|
|
|
System.Diagnostics.Debug.Assert(processedElementCount == totalCount);
|
|
|
|
// Unnecessary but just for completeness.
|
|
taskAwaiter.GetResult();
|
|
#endif
|
|
|
|
rawData = tmpBakeData.rawData;
|
|
cellCount = tmpBakeData.cellCount;
|
|
cellSize = tmpBakeData.cellSize;
|
|
|
|
GameObject.DestroyImmediate(tmpBakeData);
|
|
}
|
|
|
|
// This is called at run-time and needs to be as efficient as possible
|
|
public override void SampleAtIndex(int index, List<ushort> indices)
|
|
{
|
|
SampleAtIndex(index, indices, m_bitStreamReader);
|
|
}
|
|
|
|
void SampleAtIndex(int index, List<ushort> indices, BitStreamReader bitStreamReader)
|
|
{
|
|
if (data[index].compressed == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bitStreamReader.Reset(data[index].compressed);
|
|
|
|
ushort accumulator = 0;
|
|
|
|
int len = data[index].len;
|
|
|
|
for (int i = 0; i < len; ++i)
|
|
{
|
|
uint bitIndex = bitStreamReader.Read(HeaderBitSize);
|
|
|
|
uint numberOfBits = BITS[bitIndex];
|
|
uint value = bitStreamReader.Read((int) numberOfBits);
|
|
|
|
accumulator += (ushort) value;
|
|
|
|
if (value >= MaxValue)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
indices.Add(accumulator);
|
|
}
|
|
}
|
|
|
|
// Called at run-time to scan for closest non-empty cell
|
|
public override int SearchIndexForClosestNonEmptyCell(int index)
|
|
{
|
|
if (data[index].len > 0)
|
|
{
|
|
return index;
|
|
}
|
|
|
|
PerfectCullingMath.UnflattenToXYZ(index, out int x, out int y, out int z, cellCount);
|
|
|
|
int smallestDist = int.MaxValue;
|
|
int resultIndex = -1;
|
|
|
|
for (int xx = -PerfectCullingConstants.MaxNonEmptyCellSearchRange; xx <= PerfectCullingConstants.MaxNonEmptyCellSearchRange; ++xx)
|
|
{
|
|
for (int yy = -PerfectCullingConstants.MaxNonEmptyCellSearchRange; yy <= PerfectCullingConstants.MaxNonEmptyCellSearchRange; ++yy)
|
|
{
|
|
for (int zz = -PerfectCullingConstants.MaxNonEmptyCellSearchRange; zz <= PerfectCullingConstants.MaxNonEmptyCellSearchRange; ++zz)
|
|
{
|
|
if (!PerfectCullingMath.IsXYZInBounds(x + xx, y + yy, z + zz, cellCount))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int sampleIndex = PerfectCullingMath.FlattenXYZDouble(x + xx, y + yy, z + zz, cellCount);
|
|
|
|
if (data[sampleIndex].len > 0)
|
|
{
|
|
int dist = (xx * xx) + (yy * yy) + (zz * zz);
|
|
|
|
if (smallestDist > dist)
|
|
{
|
|
smallestDist = dist;
|
|
resultIndex = sampleIndex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return resultIndex;
|
|
}
|
|
|
|
private static BitStreamReader m_bitStreamReader = new BitStreamReader();
|
|
|
|
public override void DrawInspectorGUI()
|
|
{
|
|
#if UNITY_EDITOR
|
|
UnityEditor.EditorGUILayout.Vector3Field($"Cell size", cellSize);
|
|
UnityEditor.EditorGUILayout.Vector3Field($"Cell count", cellCount);
|
|
UnityEditor.EditorGUILayout.Toggle($"Bake completed", bakeCompleted);
|
|
|
|
UnityEditor.EditorGUILayout.IntField("Data cells", data.Length);
|
|
#endif
|
|
}
|
|
}
|
|
} |