using System; using System.Collections.Generic; using UnityEngine.Profiling; using UnityEngine.Rendering; using Chunk = UnityEngine.Experimental.Rendering.ProbeBrickPool.BrickChunkAlloc; using Brick = UnityEngine.Experimental.Rendering.ProbeBrickIndex.Brick; using UnityEngine.SceneManagement; using Unity.Collections; #if UNITY_EDITOR using UnityEditor; #endif namespace UnityEngine.Experimental.Rendering { #if UNITY_EDITOR /// /// A manager to enqueue extra probe rendering outside of probe volumes. /// public class AdditionalGIBakeRequestsManager { // The baking ID for the extra requests // TODO: Need to ensure this never conflicts with bake IDs from others interacting with the API. // In our project, this is ProbeVolumes. internal static readonly int s_BakingID = 912345678; private static AdditionalGIBakeRequestsManager s_Instance = new AdditionalGIBakeRequestsManager(); /// /// Get the manager that governs the additional light probe rendering requests. /// public static AdditionalGIBakeRequestsManager instance { get { return s_Instance; } } internal void Init() { SubscribeOnBakeStarted(); } internal void Cleanup() { UnsubscribeOnBakeStarted(); } private static List m_SHCoefficients = new List(); private static List m_RequestPositions = new List(); private static int m_FreelistHead = -1; private static readonly Vector2 s_FreelistSentinel = new Vector2(float.MaxValue, float.MaxValue); /// /// Enqueue a request for probe rendering at the specified location. /// /// The position at which a probe is baked. /// An ID that can be used to retrieve the data once it has been computed public int EnqueueRequest(Vector3 capturePosition) { Debug.Assert(ComputeCapturePositionIsValid(capturePosition)); if (m_FreelistHead >= 0) { int requestID = m_FreelistHead; Debug.Assert(requestID < m_RequestPositions.Count); m_FreelistHead = ComputeFreelistNext(m_RequestPositions[requestID]); m_RequestPositions[requestID] = capturePosition; m_SHCoefficients[requestID] = new SphericalHarmonicsL2(); return requestID; } else { int requestID = m_RequestPositions.Count; m_RequestPositions.Add(capturePosition); m_SHCoefficients.Add(new SphericalHarmonicsL2()); return requestID; } } /// /// Enqueue a request for probe rendering at the specified location. /// /// An ID that can be used to retrieve the data once it has been computed /// An ID that can be used to retrieve the data once it has been computed public void DequeueRequest(int requestID) { Debug.Assert(requestID >= 0 && requestID < m_RequestPositions.Count); m_RequestPositions[requestID] = new Vector3(s_FreelistSentinel.x, s_FreelistSentinel.y, m_FreelistHead); m_SHCoefficients[requestID] = new SphericalHarmonicsL2(); m_FreelistHead = requestID; } private bool ComputeCapturePositionIsValid(Vector3 capturePosition) { return !((capturePosition.x == s_FreelistSentinel.x) && (capturePosition.y == s_FreelistSentinel.y)); } private int ComputeFreelistNext(Vector3 capturePosition) { Debug.Assert(ComputeRequestIsFree(capturePosition)); int freelistNext = (int)capturePosition.z; Debug.Assert(freelistNext >= -1 && freelistNext < m_RequestPositions.Count); return freelistNext; } private bool ComputeRequestIsFree(int requestID) { Debug.Assert(requestID >= 0 && requestID < m_RequestPositions.Count); Vector3 requestPosition = m_RequestPositions[requestID]; return ComputeRequestIsFree(requestPosition); } private bool ComputeRequestIsFree(Vector3 capturePosition) { return (capturePosition.x == s_FreelistSentinel.x) && (capturePosition.y == s_FreelistSentinel.y); } /// /// Retrieve the result of a capture request, it will return false if the request has not been fulfilled yet or the request ID is invalid. /// /// The request ID that has been given by the manager through a previous EnqueueRequest. /// The output SH coefficients that have been computed. /// Whether the request for light probe rendering has been fulfilled and sh is valid. public bool RetrieveProbeSH(int requestID, out SphericalHarmonicsL2 sh) { if (requestID >= 0 && requestID < m_SHCoefficients.Count && ComputeCapturePositionIsValid(m_RequestPositions[requestID])) { sh = m_SHCoefficients[requestID]; return true; } else { sh = new SphericalHarmonicsL2(); return false; } } /// /// Update the capture location for the probe request. /// /// The request ID that has been given by the manager through a previous EnqueueRequest. /// The position at which a probe is baked. public int UpdatePositionForRequest(int requestID, Vector3 newPosition) { if (requestID >= 0 && requestID < m_RequestPositions.Count) { Debug.Assert(ComputeCapturePositionIsValid(newPosition)); m_RequestPositions[requestID] = newPosition; m_SHCoefficients[requestID] = new SphericalHarmonicsL2(); return requestID; } else { return EnqueueRequest(newPosition); } } private void SubscribeOnBakeStarted() { UnsubscribeOnBakeStarted(); Lightmapping.bakeStarted += AddRequestsToLightmapper; } private void UnsubscribeOnBakeStarted() { Lightmapping.bakeStarted -= AddRequestsToLightmapper; RemoveRequestsFromLightmapper(); } internal void AddRequestsToLightmapper() { UnityEditor.Experimental.Lightmapping.SetAdditionalBakedProbes(s_BakingID, m_RequestPositions.ToArray()); Lightmapping.bakeCompleted -= OnAdditionalProbesBakeCompleted; Lightmapping.bakeCompleted += OnAdditionalProbesBakeCompleted; } private void RemoveRequestsFromLightmapper() { UnityEditor.Experimental.Lightmapping.SetAdditionalBakedProbes(s_BakingID, null); } private void OnAdditionalProbesBakeCompleted() { Lightmapping.bakeCompleted -= OnAdditionalProbesBakeCompleted; if (m_RequestPositions.Count == 0) return; var sh = new NativeArray(m_RequestPositions.Count, Allocator.Temp, NativeArrayOptions.UninitializedMemory); var validity = new NativeArray(m_RequestPositions.Count, Allocator.Temp, NativeArrayOptions.UninitializedMemory); var bakedProbeOctahedralDepth = new NativeArray(m_RequestPositions.Count * 64, Allocator.Temp, NativeArrayOptions.UninitializedMemory); UnityEditor.Experimental.Lightmapping.GetAdditionalBakedProbes(s_BakingID, sh, validity, bakedProbeOctahedralDepth); SetSHCoefficients(sh); ProbeReferenceVolume.instance.retrieveExtraDataAction?.Invoke(new ProbeReferenceVolume.ExtraDataActionInput()); sh.Dispose(); validity.Dispose(); bakedProbeOctahedralDepth.Dispose(); } private void SetSHCoefficients(NativeArray sh) { Debug.Assert(sh.Length == m_SHCoefficients.Count); for (int i = 0; i < sh.Length; ++i) { m_SHCoefficients[i] = sh[i]; } } } #endif /// /// Initialization parameters for the probe volume system. /// public struct ProbeVolumeSystemParameters { /// /// The memory budget determining the size of the textures containing SH data. /// public ProbeVolumeTextureMemoryBudget memoryBudget; /// /// The debug mesh used to draw probes in the debug view. /// public Mesh probeDebugMesh; /// /// The shader used to visualize the probes in the debug view. /// public Shader probeDebugShader; /// /// Scene data /// public ProbeVolumeSceneData sceneData; /// /// Sh bands /// public ProbeVolumeSHBands shBands; } /// /// Shading parameters for Probe Volumes /// public struct ProbeVolumeShadingParameters { /// /// Normal bias to apply to the position used to sample probe volumes. /// public float normalBias; /// /// View bias to apply to the position used to sample probe volumes. /// public float viewBias; /// /// Whether to scale the biases with the minimum distance between probes. /// public bool scaleBiasByMinDistanceBetweenProbes; /// /// Noise to be applied to the sampling position. It can hide seams issues between subdivision levels, but introduces noise. /// public float samplingNoise; } /// /// Possible values for the probe volume memory budget (determines the size of the textures used). /// [Serializable] public enum ProbeVolumeTextureMemoryBudget { /// Low Budget MemoryBudgetLow = 512, /// Medium Budget MemoryBudgetMedium = 1024, /// High Budget MemoryBudgetHigh = 2048, } /// /// Number of Spherical Harmonics bands that are used with Probe Volumes /// [Serializable] public enum ProbeVolumeSHBands { /// Up to the L1 band of Spherical Harmonics SphericalHarmonicsL1 = 1, /// Up to the L2 band of Spherical Harmonics SphericalHarmonicsL2 = 2, } /// /// The reference volume for the Probe Volume system. This defines the structure in which volume assets are loaded into. There must be only one, hence why it follow a singleton pattern. /// public partial class ProbeReferenceVolume { const int kProbeIndexPoolAllocationSize = 128; [System.Serializable] internal class Cell { public int index; public Vector3Int position; public List bricks; public Vector3[] probePositions; public SphericalHarmonicsL2[] sh; public float[] validity; public int minSubdiv; [System.NonSerialized] public int flatIdxInCellIndices = -1; [System.NonSerialized] public bool loaded = false; } class CellChunkInfo { public List chunks; } private class CellSortInfo : IComparable { internal string sourceAsset; internal Cell cell; internal float distanceToCamera = 0; internal Vector3 position; public int CompareTo(object obj) { CellSortInfo other = obj as CellSortInfo; if (distanceToCamera < other.distanceToCamera) return 1; else if (distanceToCamera > other.distanceToCamera) return -1; else return 0; } } internal struct Volume : IEquatable { internal Vector3 corner; internal Vector3 X; // the vectors are NOT normalized, their length determines the size of the box internal Vector3 Y; internal Vector3 Z; internal float maxSubdivisionMultiplier; internal float minSubdivisionMultiplier; public Volume(Matrix4x4 trs, float maxSubdivision, float minSubdivision) { X = trs.GetColumn(0); Y = trs.GetColumn(1); Z = trs.GetColumn(2); corner = (Vector3)trs.GetColumn(3) - X * 0.5f - Y * 0.5f - Z * 0.5f; this.maxSubdivisionMultiplier = maxSubdivision; this.minSubdivisionMultiplier = minSubdivision; } public Volume(Vector3 corner, Vector3 X, Vector3 Y, Vector3 Z, float maxSubdivision = 1, float minSubdivision = 0) { this.corner = corner; this.X = X; this.Y = Y; this.Z = Z; this.maxSubdivisionMultiplier = maxSubdivision; this.minSubdivisionMultiplier = minSubdivision; } public Volume(Volume copy) { X = copy.X; Y = copy.Y; Z = copy.Z; corner = copy.corner; maxSubdivisionMultiplier = copy.maxSubdivisionMultiplier; minSubdivisionMultiplier = copy.minSubdivisionMultiplier; } public Bounds CalculateAABB() { Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue); for (int x = 0; x < 2; x++) { for (int y = 0; y < 2; y++) { for (int z = 0; z < 2; z++) { Vector3 dir = new Vector3(x, y, z); Vector3 pt = corner + X * dir.x + Y * dir.y + Z * dir.z; min = Vector3.Min(min, pt); max = Vector3.Max(max, pt); } } } return new Bounds((min + max) / 2, max - min); } public void CalculateCenterAndSize(out Vector3 center, out Vector3 size) { size = new Vector3(X.magnitude, Y.magnitude, Z.magnitude); center = corner + X * 0.5f + Y * 0.5f + Z * 0.5f; } public void Transform(Matrix4x4 trs) { corner = trs.MultiplyPoint(corner); X = trs.MultiplyVector(X); Y = trs.MultiplyVector(Y); Z = trs.MultiplyVector(Z); } public override string ToString() { return $"Corner: {corner}, X: {X}, Y: {Y}, Z: {Z}, MaxSubdiv: {maxSubdivisionMultiplier}"; } public bool Equals(Volume other) { return corner == other.corner && X == other.X && Y == other.Y && Z == other.Z && minSubdivisionMultiplier == other.minSubdivisionMultiplier && maxSubdivisionMultiplier == other.maxSubdivisionMultiplier; } } internal struct RefVolTransform { public Matrix4x4 refSpaceToWS; public Vector3 posWS; public Quaternion rot; public float scale; } /// /// The resources that are bound to the runtime shaders for sampling Adaptive Probe Volume data. /// public struct RuntimeResources { /// /// Index data to fetch the correct location in the Texture3D. /// public ComputeBuffer index; /// /// Indices of the various index buffers for each cell. /// public ComputeBuffer cellIndices; /// /// Texture containing Spherical Harmonics L0 band data and first coefficient of L1_R. /// public Texture3D L0_L1rx; /// /// Texture containing the second channel of Spherical Harmonics L1 band data and second coefficient of L1_R. /// public Texture3D L1_G_ry; /// /// Texture containing the second channel of Spherical Harmonics L1 band data and third coefficient of L1_R. /// public Texture3D L1_B_rz; /// /// Texture containing the first coefficient of Spherical Harmonics L2 band data and first channel of the fifth. /// public Texture3D L2_0; /// /// Texture containing the second coefficient of Spherical Harmonics L2 band data and second channel of the fifth. /// public Texture3D L2_1; /// /// Texture containing the third coefficient of Spherical Harmonics L2 band data and third channel of the fifth. /// public Texture3D L2_2; /// /// Texture containing the fourth coefficient of Spherical Harmonics L2 band data. /// public Texture3D L2_3; } internal struct RegId { internal int id; public bool IsValid() => id != 0; public void Invalidate() => id = 0; public static bool operator ==(RegId lhs, RegId rhs) => lhs.id == rhs.id; public static bool operator !=(RegId lhs, RegId rhs) => lhs.id != rhs.id; public override bool Equals(object obj) { if ((obj == null) || !this.GetType().Equals(obj.GetType())) { return false; } else { RegId p = (RegId)obj; return p == this; } } public override int GetHashCode() => id; } bool m_IsInitialized = false; int m_ID = 0; RefVolTransform m_Transform; int m_MaxSubdivision; ProbeBrickPool m_Pool; ProbeBrickIndex m_Index; ProbeCellIndices m_CellIndices; List m_TmpSrcChunks = new List(); float[] m_PositionOffsets = new float[ProbeBrickPool.kBrickProbeCountPerDim]; Dictionary> m_Registry = new Dictionary>(); Bounds m_CurrGlobalBounds = new Bounds(); internal Dictionary cells = new Dictionary(); Dictionary m_ChunkInfo = new Dictionary(); internal ProbeVolumeSceneData sceneData; /// /// The input to the retrieveExtraDataAction action. /// public struct ExtraDataActionInput { // Empty, but defined to make this future proof without having to change public API } /// /// An action that is used by the SRP to retrieve extra data that was baked together with the bake /// public Action retrieveExtraDataAction; bool m_BricksLoaded = false; Dictionary m_CellToBricks = new Dictionary(); Dictionary m_BricksToCellUpdateInfo = new Dictionary(); // Information of the probe volume asset that is being loaded (if one is pending) Dictionary m_PendingAssetsToBeLoaded = new Dictionary(); // Information on probes we need to remove. Dictionary m_PendingAssetsToBeUnloaded = new Dictionary(); // Information of the probe volume asset that is being loaded (if one is pending) Dictionary m_ActiveAssets = new Dictionary(); // List of info for cells that are yet to be loaded. private List m_CellsToBeLoaded = new List(); // Ref counting here as a separate dictionary as a temporary measure to facilitate future changes that will soon go in. // cell.index, refCount Dictionary m_CellRefCounting = new Dictionary(); void InvalidateAllCellRefs() { m_CellRefCounting.Clear(); } bool m_NeedLoadAsset = false; bool m_ProbeReferenceVolumeInit = false; bool m_EnabledBySRP = false; internal bool isInitialized => m_ProbeReferenceVolumeInit; internal bool enabledBySRP => m_EnabledBySRP; struct InitInfo { public Vector3Int pendingMinCellPosition; public Vector3Int pendingMaxCellPosition; } InitInfo m_PendingInitInfo; bool m_NeedsIndexRebuild = false; bool m_HasChangedIndex = false; int m_CBShaderID = Shader.PropertyToID("ShaderVariablesProbeVolumes"); #if UNITY_EDITOR // By default on editor we load a lot of cells in one go to avoid having to mess with scene view // to see results, this value can still be changed via API. private int m_NumberOfCellsLoadedPerFrame = 10000; #else private int m_NumberOfCellsLoadedPerFrame = 2; #endif ProbeVolumeTextureMemoryBudget m_MemoryBudget; ProbeVolumeSHBands m_SHBands; /// /// The sh bands /// public ProbeVolumeSHBands shBands { get { return m_SHBands; } } internal bool clearAssetsOnVolumeClear = false; /// /// Get the memory budget for the Probe Volume system. /// public ProbeVolumeTextureMemoryBudget memoryBudget => m_MemoryBudget; static ProbeReferenceVolume _instance = new ProbeReferenceVolume(); /// /// Get the instance of the probe reference volume (singleton). /// public static ProbeReferenceVolume instance { get { return _instance; } } /// /// Set the number of cells that are loaded per frame when needed. /// /// public void SetNumberOfCellsLoadedPerFrame(int numberOfCells) { m_NumberOfCellsLoadedPerFrame = Mathf.Max(1, numberOfCells); } /// /// Initialize the Probe Volume system /// /// Initialization parameters. public void Initialize(in ProbeVolumeSystemParameters parameters) { if (m_IsInitialized) { Debug.LogError("Probe Volume System has already been initialized."); return; } m_MemoryBudget = parameters.memoryBudget; m_SHBands = parameters.shBands; InitializeDebug(parameters.probeDebugMesh, parameters.probeDebugShader); InitProbeReferenceVolume(kProbeIndexPoolAllocationSize, m_MemoryBudget, m_SHBands); m_IsInitialized = true; m_NeedsIndexRebuild = true; sceneData = parameters.sceneData; #if UNITY_EDITOR if (sceneData != null) { UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += sceneData.OnSceneSaved; } AdditionalGIBakeRequestsManager.instance.Init(); #endif m_EnabledBySRP = true; } /// /// Communicate to the Probe Volume system whether the SRP enables Probe Volume. /// It is important to keep in mind that this is not used by the system for anything else but book-keeping, /// the SRP is still responsible to disable anything Probe volume related on SRP side. /// /// The value public void SetEnableStateFromSRP(bool srpEnablesPV) { m_EnabledBySRP = srpEnablesPV; } // This is used for steps such as dilation that require the maximum order allowed to be loaded at all times. Should really never be used as a general purpose function. internal void ForceSHBand(ProbeVolumeSHBands shBands) { if (m_ProbeReferenceVolumeInit) CleanupLoadedData(); m_SHBands = shBands; m_ProbeReferenceVolumeInit = false; InitProbeReferenceVolume(kProbeIndexPoolAllocationSize, m_MemoryBudget, shBands); } /// /// Cleanup the Probe Volume system. /// public void Cleanup() { if (!m_ProbeReferenceVolumeInit) return; #if UNITY_EDITOR AdditionalGIBakeRequestsManager.instance.Cleanup(); #endif if (!m_IsInitialized) { Debug.LogError("Probe Volume System has not been initialized first before calling cleanup."); return; } CleanupLoadedData(); CleanupDebug(); m_IsInitialized = false; } /// /// Get approximate video memory impact, in bytes, of the system. /// /// An approximation of the video memory impact, in bytes, of the system public int GetVideoMemoryCost() { if (!m_ProbeReferenceVolumeInit) return 0; return m_Pool.estimatedVMemCost + m_Index.estimatedVMemCost + m_CellIndices.estimatedVMemCost; } void RemoveCell(Cell cell) { if (cell.loaded) { bool needsUnloading = true; if (m_CellRefCounting.ContainsKey(cell.index)) { m_CellRefCounting[cell.index]--; needsUnloading = m_CellRefCounting[cell.index] <= 0; if (needsUnloading) { m_CellRefCounting[cell.index] = 0; } } if (needsUnloading) { if (cells.ContainsKey(cell.index)) cells.Remove(cell.index); if (m_ChunkInfo.ContainsKey(cell.index)) m_ChunkInfo.Remove(cell.index); if (cell.flatIdxInCellIndices >= 0) m_CellIndices.MarkCellAsUnloaded(cell.flatIdxInCellIndices); RegId cellBricksID = new RegId(); if (m_CellToBricks.TryGetValue(cell, out cellBricksID)) { ReleaseBricks(cellBricksID); m_CellToBricks.Remove(cell); } } } cell.loaded = false; } void AddCell(Cell cell, List chunks) { if (m_CellRefCounting.ContainsKey(cell.index)) m_CellRefCounting[cell.index]++; else m_CellRefCounting.Add(cell.index, 1); cell.loaded = true; cells[cell.index] = cell; var cellChunks = new CellChunkInfo(); cellChunks.chunks = chunks; m_ChunkInfo[cell.index] = cellChunks; } bool CheckCompatibilityWithCollection(ProbeVolumeAsset asset, Dictionary collection) { if (collection.Count > 0) { // Any one is fine, they should all have the same properties. We need to go through them anyway as some might be pending deletion already. foreach (var collectionValue in collection.Values) { // We don't care about this to check against, it is already pending deletion. if (m_PendingAssetsToBeUnloaded.ContainsKey(collectionValue.GetSerializedFullPath())) continue; return collectionValue.CompatibleWith(asset); } } return true; } internal void AddPendingAssetLoading(ProbeVolumeAsset asset) { var key = asset.GetSerializedFullPath(); if (m_PendingAssetsToBeLoaded.ContainsKey(key)) { m_PendingAssetsToBeLoaded.Remove(key); } if (!CheckCompatibilityWithCollection(asset, m_ActiveAssets)) { Debug.LogError($"Trying to load Probe Volume data for a scene that has been baked with different settings than currently loaded ones. " + $"Please make sure all loaded scenes are in the same baking set."); return; } // If we don't have any loaded asset yet, we need to verify the other queued assets. if (!CheckCompatibilityWithCollection(asset, m_PendingAssetsToBeLoaded)) { Debug.LogError($"Trying to load Probe Volume data for a scene that has been baked with different settings from other scenes that are being loaded. " + $"Please make sure all loaded scenes are in the same baking set."); return; } m_PendingAssetsToBeLoaded.Add(key, asset); m_NeedLoadAsset = true; // Compute the max index dimension from all the loaded assets + assets we need to load Vector3Int indexDimension = Vector3Int.zero; Vector3Int minCellPosition = Vector3Int.zero; Vector3Int maxCellPosition = Vector3Int.zero; bool firstBound = true; foreach (var a in m_PendingAssetsToBeLoaded.Values) { minCellPosition = Vector3Int.Min(minCellPosition, a.minCellPosition); maxCellPosition = Vector3Int.Max(maxCellPosition, a.maxCellPosition); if (firstBound) { m_CurrGlobalBounds = a.globalBounds; firstBound = false; } else { m_CurrGlobalBounds.Encapsulate(a.globalBounds); } } foreach (var a in m_ActiveAssets.Values) { minCellPosition = Vector3Int.Min(minCellPosition, a.minCellPosition); maxCellPosition = Vector3Int.Max(maxCellPosition, a.maxCellPosition); if (firstBound) { m_CurrGlobalBounds = a.globalBounds; firstBound = false; } else { m_CurrGlobalBounds.Encapsulate(a.globalBounds); } } // |= because this can be called more than once before rebuild is done. m_NeedsIndexRebuild |= m_Index == null || m_PendingInitInfo.pendingMinCellPosition != minCellPosition || m_PendingInitInfo.pendingMaxCellPosition != maxCellPosition; m_PendingInitInfo.pendingMinCellPosition = minCellPosition; m_PendingInitInfo.pendingMaxCellPosition = maxCellPosition; } internal void AddPendingAssetRemoval(ProbeVolumeAsset asset) { var key = asset.GetSerializedFullPath(); if (m_PendingAssetsToBeUnloaded.ContainsKey(key)) { m_PendingAssetsToBeUnloaded.Remove(key); } m_PendingAssetsToBeUnloaded.Add(asset.GetSerializedFullPath(), asset); } internal void RemovePendingAsset(ProbeVolumeAsset asset) { var key = asset.GetSerializedFullPath(); for (int i = m_CellsToBeLoaded.Count - 1; i >= 0; i--) { if (m_CellsToBeLoaded[i].sourceAsset == key) m_CellsToBeLoaded.RemoveAt(i); } if (m_ActiveAssets.ContainsKey(key)) { m_ActiveAssets.Remove(key); } // Remove bricks and empty cells foreach (var cell in asset.cells) { RemoveCell(cell); } ClearDebugData(); } void PerformPendingIndexChangeAndInit() { if (m_NeedsIndexRebuild) { CleanupLoadedData(); InitProbeReferenceVolume(kProbeIndexPoolAllocationSize, m_MemoryBudget, m_SHBands); m_HasChangedIndex = true; m_NeedsIndexRebuild = false; } else { m_HasChangedIndex = false; } } internal void SetMinBrickAndMaxSubdiv(float minBrickSize, int maxSubdiv) { SetTRS(Vector3.zero, Quaternion.identity, minBrickSize); SetMaxSubdivision(maxSubdiv); } void LoadAsset(ProbeVolumeAsset asset) { if (asset.Version != (int)ProbeVolumeAsset.AssetVersion.Current) { Debug.LogWarning($"Trying to load an asset {asset.GetSerializedFullPath()} that has been baked with a previous version of the system. Please re-bake the data."); return; } var path = asset.GetSerializedFullPath(); // Load info coming originally from profile SetMinBrickAndMaxSubdiv(asset.minBrickSize, asset.maxSubdivision); for (int i = 0; i < asset.cells.Count; ++i) { var cell = asset.cells[i]; CellSortInfo sortInfo = new CellSortInfo(); sortInfo.cell = cell; sortInfo.position = ((Vector3)cell.position * MaxBrickSize() * 0.5f) + m_Transform.posWS; sortInfo.sourceAsset = asset.GetSerializedFullPath(); m_CellsToBeLoaded.Add(sortInfo); } } void PerformPendingLoading() { if ((m_PendingAssetsToBeLoaded.Count == 0 && m_ActiveAssets.Count == 0) || !m_NeedLoadAsset || !m_ProbeReferenceVolumeInit) return; m_Pool.EnsureTextureValidity(); // Load the ones that are already active but reload if we said we need to load if (m_HasChangedIndex) { // We changed index so all assets are going to be re-loaded, hence the refs will be repopulated from scratch InvalidateAllCellRefs(); foreach (var asset in m_ActiveAssets.Values) { LoadAsset(asset); } } foreach (var asset in m_PendingAssetsToBeLoaded.Values) { LoadAsset(asset); if (!m_ActiveAssets.ContainsKey(asset.GetSerializedFullPath())) { m_ActiveAssets.Add(asset.GetSerializedFullPath(), asset); } } m_PendingAssetsToBeLoaded.Clear(); // Mark the loading as done. m_NeedLoadAsset = false; } void PerformPendingDeletion() { if (!m_ProbeReferenceVolumeInit) { m_PendingAssetsToBeUnloaded.Clear(); // If we are not init, we have not loaded yet. } var dictionaryValues = m_PendingAssetsToBeUnloaded.Values; foreach (var asset in dictionaryValues) { RemovePendingAsset(asset); } m_PendingAssetsToBeUnloaded.Clear(); } int GetNumberOfBricksAtSubdiv(Cell cell, out Vector3Int minValidLocalIdxAtMaxRes, out Vector3Int sizeOfValidIndicesAtMaxRes) { minValidLocalIdxAtMaxRes = Vector3Int.zero; sizeOfValidIndicesAtMaxRes = Vector3Int.one; var posWS = new Vector3(cell.position.x * MaxBrickSize(), cell.position.y * MaxBrickSize(), cell.position.z * MaxBrickSize()); Bounds cellBounds = new Bounds(); cellBounds.min = posWS; cellBounds.max = posWS + (Vector3.one * MaxBrickSize()); Bounds intersectBound = new Bounds(); intersectBound.min = Vector3.Max(cellBounds.min, m_CurrGlobalBounds.min); intersectBound.max = Vector3.Min(cellBounds.max, m_CurrGlobalBounds.max); Vector3 size = intersectBound.max - intersectBound.min; var toStart = intersectBound.min - cellBounds.min; minValidLocalIdxAtMaxRes.x = Mathf.CeilToInt((toStart.x) / MinBrickSize()); minValidLocalIdxAtMaxRes.y = Mathf.CeilToInt((toStart.y) / MinBrickSize()); minValidLocalIdxAtMaxRes.z = Mathf.CeilToInt((toStart.z) / MinBrickSize()); var toEnd = intersectBound.max - cellBounds.min; sizeOfValidIndicesAtMaxRes.x = Mathf.CeilToInt((toEnd.x) / MinBrickSize()) - minValidLocalIdxAtMaxRes.x + 1; sizeOfValidIndicesAtMaxRes.y = Mathf.CeilToInt((toEnd.y) / MinBrickSize()) - minValidLocalIdxAtMaxRes.y + 1; sizeOfValidIndicesAtMaxRes.z = Mathf.CeilToInt((toEnd.z) / MinBrickSize()) - minValidLocalIdxAtMaxRes.z + 1; Vector3Int bricksForCell = new Vector3Int(); bricksForCell = sizeOfValidIndicesAtMaxRes / CellSize(cell.minSubdiv); return bricksForCell.x * bricksForCell.y * bricksForCell.z; } bool GetCellIndexUpdate(Cell cell, out ProbeBrickIndex.CellIndexUpdateInfo cellUpdateInfo) { cellUpdateInfo = new ProbeBrickIndex.CellIndexUpdateInfo(); int brickCountsAtResolution = GetNumberOfBricksAtSubdiv(cell, out var minValidLocalIdx, out var sizeOfValidIndices); cellUpdateInfo.cellPositionInBricksAtMaxRes = cell.position * CellSize(m_MaxSubdivision - 1); cellUpdateInfo.minSubdivInCell = cell.minSubdiv; cellUpdateInfo.minValidBrickIndexForCellAtMaxRes = minValidLocalIdx; cellUpdateInfo.maxValidBrickIndexForCellAtMaxResPlusOne = sizeOfValidIndices + minValidLocalIdx; return m_Index.AssignIndexChunksToCell(cell, brickCountsAtResolution, ref cellUpdateInfo); } void LoadPendingCells(bool loadAll = false) { int count = Mathf.Min(m_NumberOfCellsLoadedPerFrame, m_CellsToBeLoaded.Count); count = loadAll ? m_CellsToBeLoaded.Count : count; // This should never happen, *unless* an asset was baked with previous version of index buffer. if (m_PendingInitInfo.pendingMinCellPosition == m_PendingInitInfo.pendingMaxCellPosition && count > 1) return; if (count != 0) ClearDebugData(); for (int i = 0; i < count; ++i) { // Pop from queue. var sortInfo = m_CellsToBeLoaded[0]; var cell = sortInfo.cell; var path = sortInfo.sourceAsset; bool compressed = false; int allocatedBytes = 0; var dataLocation = ProbeBrickPool.CreateDataLocation(cell.sh.Length, compressed, m_SHBands, out allocatedBytes); ProbeBrickPool.FillDataLocation(ref dataLocation, cell.sh, m_SHBands); cell.flatIdxInCellIndices = m_CellIndices.GetFlatIdxForCell(cell.position); if (GetCellIndexUpdate(cell, out var cellUpdateInfo)) { List brickList = new List(); brickList.AddRange(cell.bricks); List chunkList = new List(); var regId = AddBricks(brickList, dataLocation, cellUpdateInfo, out chunkList); m_BricksToCellUpdateInfo.Add(regId, cellUpdateInfo); m_CellIndices.AddCell(cell.flatIdxInCellIndices, cellUpdateInfo); AddCell(cell, chunkList); m_CellToBricks[cell] = regId; dataLocation.Cleanup(); m_CellsToBeLoaded.RemoveAt(0); } else { // We need to first remove something to fit, can't load things further. return; } } } /// /// Perform all the operations that are relative to changing the content or characteristics of the probe reference volume. /// /// True when all cells are to be immediately loaded.. public void PerformPendingOperations(bool loadAllCells = false) { PerformPendingDeletion(); PerformPendingIndexChangeAndInit(); PerformPendingLoading(); LoadPendingCells(loadAllCells); } /// /// Initialize the reference volume. /// /// Size used for the chunk allocator that handles bricks. /// Probe reference volume memory budget. /// Probe reference volume SH bands. void InitProbeReferenceVolume(int allocationSize, ProbeVolumeTextureMemoryBudget memoryBudget, ProbeVolumeSHBands shBands) { var minCellPosition = m_PendingInitInfo.pendingMinCellPosition; var maxCellPosition = m_PendingInitInfo.pendingMaxCellPosition; if (!m_ProbeReferenceVolumeInit) { Profiler.BeginSample("Initialize Reference Volume"); m_Pool = new ProbeBrickPool(allocationSize, memoryBudget, shBands); m_Index = new ProbeBrickIndex(memoryBudget); m_CellIndices = new ProbeCellIndices(minCellPosition, maxCellPosition, (int)Mathf.Pow(3, m_MaxSubdivision - 1)); // initialize offsets m_PositionOffsets[0] = 0.0f; float probeDelta = 1.0f / ProbeBrickPool.kBrickCellCount; for (int i = 1; i < ProbeBrickPool.kBrickProbeCountPerDim - 1; i++) m_PositionOffsets[i] = i * probeDelta; m_PositionOffsets[m_PositionOffsets.Length - 1] = 1.0f; Profiler.EndSample(); m_ProbeReferenceVolumeInit = true; ClearDebugData(); m_NeedLoadAsset = true; } } /// /// Perform sorting of pending cells to be loaded. /// /// The position to sort against (closer to the position will be loaded first). public void SortPendingCells(Vector3 cameraPosition) { if (m_CellsToBeLoaded.Count > 0) { for (int i = 0; i < m_CellsToBeLoaded.Count; ++i) { m_CellsToBeLoaded[i].distanceToCamera = Vector3.Distance(cameraPosition, m_CellsToBeLoaded[i].position); } m_CellsToBeLoaded.Sort(); } } ProbeReferenceVolume() { m_Transform.posWS = Vector3.zero; m_Transform.rot = Quaternion.identity; m_Transform.scale = 1f; m_Transform.refSpaceToWS = Matrix4x4.identity; } /// /// Get the resources that are bound to the runtime shaders for sampling Adaptive Probe Volume data. /// /// The resources to bind to runtime shaders. public RuntimeResources GetRuntimeResources() { if (!m_ProbeReferenceVolumeInit) return default(RuntimeResources); RuntimeResources rr = new RuntimeResources(); m_Index.GetRuntimeResources(ref rr); m_CellIndices.GetRuntimeResources(ref rr); m_Pool.GetRuntimeResources(ref rr); return rr; } internal void SetTRS(Vector3 position, Quaternion rotation, float minBrickSize) { m_Transform.posWS = position; m_Transform.rot = rotation; m_Transform.scale = minBrickSize; m_Transform.refSpaceToWS = Matrix4x4.TRS(m_Transform.posWS, m_Transform.rot, Vector3.one * m_Transform.scale); } internal void SetMaxSubdivision(int maxSubdivision) => m_MaxSubdivision = System.Math.Min(maxSubdivision, ProbeBrickIndex.kMaxSubdivisionLevels); internal static int CellSize(int subdivisionLevel) => (int)Mathf.Pow(ProbeBrickPool.kBrickCellCount, subdivisionLevel); internal float BrickSize(int subdivisionLevel) => m_Transform.scale * CellSize(subdivisionLevel); internal float MinBrickSize() => m_Transform.scale; internal float MaxBrickSize() => BrickSize(m_MaxSubdivision - 1); internal Matrix4x4 GetRefSpaceToWS() => m_Transform.refSpaceToWS; internal RefVolTransform GetTransform() => m_Transform; internal int GetMaxSubdivision() => m_MaxSubdivision; internal int GetMaxSubdivision(float multiplier) => Mathf.CeilToInt(m_MaxSubdivision * multiplier); internal float GetDistanceBetweenProbes(int subdivisionLevel) => BrickSize(subdivisionLevel) / 3.0f; internal float MinDistanceBetweenProbes() => GetDistanceBetweenProbes(0); /// /// Returns whether any brick data has been loaded. /// /// public bool DataHasBeenLoaded() => m_BricksLoaded; internal void Clear() { if (m_ProbeReferenceVolumeInit) { m_Pool.Clear(); m_Index.Clear(); cells.Clear(); m_ChunkInfo.Clear(); } if (clearAssetsOnVolumeClear) { m_PendingAssetsToBeLoaded.Clear(); m_ActiveAssets.Clear(); } } // Runtime API starts here RegId AddBricks(List bricks, ProbeBrickPool.DataLocation dataloc, ProbeBrickIndex.CellIndexUpdateInfo cellUpdateInfo, out List ch_list) { Profiler.BeginSample("AddBricks"); // calculate the number of chunks necessary int ch_size = m_Pool.GetChunkSize(); ch_list = new List((bricks.Count + ch_size - 1) / ch_size); m_Pool.Allocate(ch_list.Capacity, ch_list); // copy chunks into pool m_TmpSrcChunks.Clear(); m_TmpSrcChunks.Capacity = ch_list.Count; Chunk c; c.x = 0; c.y = 0; c.z = 0; // currently this code assumes that the texture width is a multiple of the allocation chunk size for (int i = 0; i < ch_list.Count; i++) { m_TmpSrcChunks.Add(c); c.x += ch_size * ProbeBrickPool.kBrickProbeCountPerDim; if (c.x >= dataloc.width) { c.x = 0; c.y += ProbeBrickPool.kBrickProbeCountPerDim; if (c.y >= dataloc.height) { c.y = 0; c.z += ProbeBrickPool.kBrickProbeCountPerDim; } } } // Update the pool and index and ignore any potential frame latency related issues for now m_Pool.Update(dataloc, m_TmpSrcChunks, ch_list, m_SHBands); m_BricksLoaded = true; // create a registry entry for this request RegId id; m_ID++; id.id = m_ID; m_Registry.Add(id, ch_list); // Build index m_Index.AddBricks(id, bricks, ch_list, m_Pool.GetChunkSize(), m_Pool.GetPoolWidth(), m_Pool.GetPoolHeight(), cellUpdateInfo); Profiler.EndSample(); return id; } void ReleaseBricks(RegId id) { List ch_list; if (!m_Registry.TryGetValue(id, out ch_list)) { Debug.Log("Tried to release bricks with id=" + id.id + " but no bricks were registered under this id."); return; } // clean up the index m_Index.RemoveBricks(id, m_BricksToCellUpdateInfo[id]); // clean up the pool m_Pool.Deallocate(ch_list); m_Registry.Remove(id); m_BricksToCellUpdateInfo.Remove(id); } /// /// Update the constant buffer used by Probe Volumes in shaders. /// /// A command buffer used to perform the data update. /// Parameters to be used when sampling the probe volume. public void UpdateConstantBuffer(CommandBuffer cmd, ProbeVolumeShadingParameters parameters) { float normalBias = parameters.normalBias; float viewBias = parameters.viewBias; if (parameters.scaleBiasByMinDistanceBetweenProbes) { normalBias *= MinDistanceBetweenProbes(); viewBias *= MinDistanceBetweenProbes(); } ShaderVariablesProbeVolumes shaderVars; shaderVars._NormalBias = normalBias; shaderVars._PoolDim = m_Pool.GetPoolDimensions(); shaderVars._ViewBias = viewBias; shaderVars._PVSamplingNoise = parameters.samplingNoise; shaderVars._CellInMinBricks = (int)Mathf.Pow(3, m_MaxSubdivision - 1); shaderVars._CellIndicesDim = m_CellIndices.GetCellIndexDimension(); shaderVars._MinCellPosition = m_CellIndices.GetCellMinPosition(); shaderVars._MinBrickSize = MinBrickSize(); shaderVars._IndexChunkSize = ProbeBrickIndex.kIndexChunkSize; shaderVars._CellInMeters = MaxBrickSize(); ConstantBuffer.PushGlobal(cmd, shaderVars, m_CBShaderID); } /// /// Cleanup loaded data. /// void CleanupLoadedData() { m_BricksLoaded = false; if (m_ProbeReferenceVolumeInit) { m_Index.Cleanup(); m_CellIndices.Cleanup(); m_Pool.Cleanup(); } m_ProbeReferenceVolumeInit = false; ClearDebugData(); } } }