using System.Linq; using System.Reflection; using System.Collections.Generic; using UnityEngine.SceneManagement; #if UNITY_EDITOR using UnityEditor; #endif namespace UnityEngine.Rendering { // Add Profile and baking settings. /// A class containing info about the bounds defined by the probe volumes in various scenes. [System.Serializable] public class ProbeVolumeSceneData : ISerializationCallbackReceiver { static PropertyInfo s_SceneGUID = typeof(Scene).GetProperty("guid", System.Reflection.BindingFlags.NonPublic | BindingFlags.Instance); static internal string GetSceneGUID(Scene scene) { Debug.Assert(s_SceneGUID != null, "Reflection for scene GUID failed"); return (string)s_SceneGUID.GetValue(scene); } [System.Serializable] struct SerializableBoundItem { [SerializeField] public string sceneGUID; [SerializeField] public Bounds bounds; } [System.Serializable] struct SerializableHasPVItem { [SerializeField] public string sceneGUID; [SerializeField] public bool hasProbeVolumes; } [System.Serializable] struct SerializablePVProfile { [SerializeField] public string sceneGUID; [SerializeField] public ProbeReferenceVolumeProfile profile; } [System.Serializable] struct SerializablePVBakeSettings { public string sceneGUID; public ProbeVolumeBakingProcessSettings settings; } [System.Serializable] internal class BakingSet { public string name; public List sceneGUIDs = new List(); public ProbeVolumeBakingProcessSettings settings; public ProbeReferenceVolumeProfile profile; public List lightingScenarios = new List(); internal string CreateScenario(string name) { if (lightingScenarios.Contains(name)) { string renamed; int index = 1; do renamed = $"{name} ({index++})"; while (lightingScenarios.Contains(renamed)); name = renamed; } lightingScenarios.Add(name); return name; } internal bool RemoveScenario(string name) { return lightingScenarios.Remove(name); } } [SerializeField] List serializedBounds; [SerializeField] List serializedHasVolumes; [SerializeField] List serializedProfiles; [SerializeField] List serializedBakeSettings; [SerializeField] List serializedBakingSets; internal Object parentAsset = null; internal string parentSceneDataPropertyName; /// A dictionary containing the Bounds defined by probe volumes for each scene (scene path is the key of the dictionary). public Dictionary sceneBounds; internal Dictionary hasProbeVolumes; internal Dictionary sceneProfiles; internal Dictionary sceneBakingSettings; internal List bakingSets; [SerializeField] string m_LightingScenario = ProbeReferenceVolume.defaultLightingScenario; string m_OtherScenario = null; float m_ScenarioBlendingFactor = 0.0f; internal string lightingScenario => m_LightingScenario; internal string otherScenario => m_OtherScenario; internal float scenarioBlendingFactor => m_ScenarioBlendingFactor; internal void SetActiveScenario(string scenario) { if (m_LightingScenario == scenario && m_ScenarioBlendingFactor == 0.0f) return; m_LightingScenario = scenario; m_OtherScenario = null; m_ScenarioBlendingFactor = 0.0f; foreach (var data in ProbeReferenceVolume.instance.perSceneDataList) data.UpdateActiveScenario(m_LightingScenario, m_OtherScenario); if (ProbeReferenceVolume.instance.enableScenarioBlending) { // Trigger blending system to replace old cells with the one from the new active scenario. // Although we technically don't need blending for that, it is better than unloading all cells // because it will replace them progressively. There is no real performance cost to using blending // rather than regular load thanks to the bypassBlending branch in AddBlendingBricks. ProbeReferenceVolume.instance.ScenarioBlendingChanged(true); } else ProbeReferenceVolume.instance.UnloadAllCells(); } internal void BlendLightingScenario(string otherScenario, float blendingFactor) { if (!ProbeReferenceVolume.instance.enableScenarioBlending) { if (!ProbeBrickBlendingPool.isSupported) Debug.LogError("Blending between lighting scenarios is not supported by this render pipeline."); else Debug.LogError("Blending between lighting scenarios is disabled in the render pipeline settings."); return; } blendingFactor = Mathf.Clamp01(blendingFactor); if (otherScenario == m_LightingScenario || string.IsNullOrEmpty(otherScenario)) otherScenario = null; if (otherScenario == null) blendingFactor = 0.0f; if (otherScenario == m_OtherScenario && Mathf.Approximately(blendingFactor, m_ScenarioBlendingFactor)) return; bool scenarioChanged = otherScenario != m_OtherScenario; m_OtherScenario = otherScenario; m_ScenarioBlendingFactor = blendingFactor; if (scenarioChanged) { foreach (var data in ProbeReferenceVolume.instance.perSceneDataList) data.UpdateActiveScenario(m_LightingScenario, m_OtherScenario); } ProbeReferenceVolume.instance.ScenarioBlendingChanged(scenarioChanged); } /// /// Constructor for ProbeVolumeSceneData. /// /// The asset holding this ProbeVolumeSceneData, it will be dirtied every time scene bounds or settings are changed. /// The name of the property holding the ProbeVolumeSceneData in the parentAsset. public ProbeVolumeSceneData(Object parentAsset, string parentSceneDataPropertyName) { this.parentAsset = parentAsset; this.parentSceneDataPropertyName = parentSceneDataPropertyName; sceneBounds = new Dictionary(); hasProbeVolumes = new Dictionary(); sceneProfiles = new Dictionary(); sceneBakingSettings = new Dictionary(); bakingSets = new List(); serializedBounds = new List(); serializedHasVolumes = new List(); serializedProfiles = new List(); serializedBakeSettings = new List(); UpdateBakingSets(); } /// Set a reference to the object holding this ProbeVolumeSceneData. /// The object holding this ProbeVolumeSceneData, it will be dirtied every time scene bounds or settings are changed. /// The name of the property holding the ProbeVolumeSceneData in the parentAsset. public void SetParentObject(Object parent, string parentSceneDataPropertyName) { parentAsset = parent; this.parentSceneDataPropertyName = parentSceneDataPropertyName; UpdateBakingSets(); } /// /// OnAfterDeserialize implementation. /// public void OnAfterDeserialize() { // We haven't initialized the bounds, no need to do anything here. if (serializedBounds == null || serializedHasVolumes == null || serializedProfiles == null || serializedBakeSettings == null) return; sceneBounds = new Dictionary(); hasProbeVolumes = new Dictionary(); sceneProfiles = new Dictionary(); sceneBakingSettings = new Dictionary(); bakingSets = new List(); foreach (var boundItem in serializedBounds) { sceneBounds.Add(boundItem.sceneGUID, boundItem.bounds); } foreach (var boundItem in serializedHasVolumes) { hasProbeVolumes.Add(boundItem.sceneGUID, boundItem.hasProbeVolumes); } foreach (var profileItem in serializedProfiles) { sceneProfiles.Add(profileItem.sceneGUID, profileItem.profile); } foreach (var settingsItem in serializedBakeSettings) { sceneBakingSettings.Add(settingsItem.sceneGUID, settingsItem.settings); } if (string.IsNullOrEmpty(m_LightingScenario)) m_LightingScenario = ProbeReferenceVolume.defaultLightingScenario; foreach (var set in serializedBakingSets) { // Ensure baking set settings are up to date set.settings.Upgrade(); bakingSets.Add(set); } if (m_OtherScenario == "") m_OtherScenario = null; } // This function must not be called during the serialization (because of asset creation) void UpdateBakingSets() { foreach (var set in serializedBakingSets) { // Small migration code to ensure that old sets have correct settings if (set.profile == null) InitializeBakingSet(set, set.name); if (set.lightingScenarios.Count == 0) InitializeScenarios(set); } SyncBakingSetSettings(); } /// /// OnBeforeSerialize implementation. /// public void OnBeforeSerialize() { // We haven't initialized the bounds, no need to do anything here. if (sceneBounds == null || hasProbeVolumes == null || sceneBakingSettings == null || sceneProfiles == null || serializedBounds == null || serializedHasVolumes == null || serializedBakeSettings == null || serializedProfiles == null || serializedBakingSets == null) return; serializedBounds.Clear(); serializedHasVolumes.Clear(); serializedProfiles.Clear(); serializedBakeSettings.Clear(); serializedBakingSets.Clear(); foreach (var k in sceneBounds.Keys) { SerializableBoundItem item; item.sceneGUID = k; item.bounds = sceneBounds[k]; serializedBounds.Add(item); } foreach (var k in hasProbeVolumes.Keys) { SerializableHasPVItem item; item.sceneGUID = k; item.hasProbeVolumes = hasProbeVolumes[k]; serializedHasVolumes.Add(item); } foreach (var k in sceneBakingSettings.Keys) { SerializablePVBakeSettings item; item.sceneGUID = k; item.settings = sceneBakingSettings[k]; serializedBakeSettings.Add(item); } foreach (var k in sceneProfiles.Keys) { SerializablePVProfile item; item.sceneGUID = k; item.profile = sceneProfiles[k]; serializedProfiles.Add(item); } foreach (var set in bakingSets) serializedBakingSets.Add(set); } internal BakingSet CreateNewBakingSet(string name) { BakingSet set = new BakingSet(); // Initialize new baking set settings InitializeBakingSet(set, name); bakingSets.Add(set); return set; } void InitializeBakingSet(BakingSet set, string name) { var newProfile = ScriptableObject.CreateInstance(); #if UNITY_EDITOR var path = AssetDatabase.GenerateUniqueAssetPath($"Assets/{name}.asset"); AssetDatabase.CreateAsset(newProfile, path); #endif set.name = newProfile.name; set.profile = newProfile; set.settings = ProbeVolumeBakingProcessSettings.Default; InitializeScenarios(set); } void InitializeScenarios(BakingSet set) { set.lightingScenarios = new List() { ProbeReferenceVolume.defaultLightingScenario }; } internal void SyncBakingSetSettings() { // Sync all the scene settings in the set to avoid config mismatch. foreach (var set in bakingSets) { foreach (var guid in set.sceneGUIDs) { sceneBakingSettings[guid] = set.settings; sceneProfiles[guid] = set.profile; } } } #if UNITY_EDITOR static internal int MaxSubdivLevelInProbeVolume(Vector3 volumeSize, int maxSubdiv) { float maxSizedDim = Mathf.Max(volumeSize.x, Mathf.Max(volumeSize.y, volumeSize.z)); float maxSideInBricks = maxSizedDim / ProbeReferenceVolume.instance.MinDistanceBetweenProbes(); int absoluteMaxSubdiv = ProbeReferenceVolume.instance.GetMaxSubdivision() - 1; int subdivLevel = Mathf.FloorToInt(Mathf.Log(maxSideInBricks, 3)) - 1; return Mathf.Max(subdivLevel, absoluteMaxSubdiv - maxSubdiv); } private void InflateBound(ref Bounds bounds, ProbeVolume pv) { Bounds originalBounds = bounds; // Round the probe volume bounds to cell size float cellSize = ProbeReferenceVolume.instance.MaxBrickSize(); // Expand the probe volume bounds to snap on the cell size grid bounds.Encapsulate(new Vector3(cellSize * Mathf.Floor(bounds.min.x / cellSize), cellSize * Mathf.Floor(bounds.min.y / cellSize), cellSize * Mathf.Floor(bounds.min.z / cellSize))); bounds.Encapsulate(new Vector3(cellSize * Mathf.Ceil(bounds.max.x / cellSize), cellSize * Mathf.Ceil(bounds.max.y / cellSize), cellSize * Mathf.Ceil(bounds.max.z / cellSize))); // calculate how much padding we need to remove according to the brick generation in ProbePlacement.cs: var cellSizeVector = new Vector3(cellSize, cellSize, cellSize); var minPadding = (bounds.min - originalBounds.min); var maxPadding = (bounds.max - originalBounds.max); minPadding = cellSizeVector - new Vector3(Mathf.Abs(minPadding.x), Mathf.Abs(minPadding.y), Mathf.Abs(minPadding.z)); maxPadding = cellSizeVector - new Vector3(Mathf.Abs(maxPadding.x), Mathf.Abs(maxPadding.y), Mathf.Abs(maxPadding.z)); // Find the size of the brick we can put for every axis given the padding size int maxSubdiv = (pv.overridesSubdivLevels ? pv.highestSubdivLevelOverride : 0); float rightPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(MaxSubdivLevelInProbeVolume(new Vector3(maxPadding.x, originalBounds.size.y, originalBounds.size.z), maxSubdiv)); float leftPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(MaxSubdivLevelInProbeVolume(new Vector3(minPadding.x, originalBounds.size.y, originalBounds.size.z), maxSubdiv)); float topPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(MaxSubdivLevelInProbeVolume(new Vector3(originalBounds.size.x, maxPadding.y, originalBounds.size.z), maxSubdiv)); float bottomPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(MaxSubdivLevelInProbeVolume(new Vector3(originalBounds.size.x, minPadding.y, originalBounds.size.z), maxSubdiv)); float forwardPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(MaxSubdivLevelInProbeVolume(new Vector3(originalBounds.size.x, originalBounds.size.y, maxPadding.z), maxSubdiv)); float backPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(MaxSubdivLevelInProbeVolume(new Vector3(originalBounds.size.x, originalBounds.size.y, minPadding.z), maxSubdiv)); // Remove the extra padding caused by cell rounding bounds.min = bounds.min + new Vector3( leftPaddingSubdivLevel * Mathf.Floor(Mathf.Abs(bounds.min.x - originalBounds.min.x) / (float)leftPaddingSubdivLevel), bottomPaddingSubdivLevel * Mathf.Floor(Mathf.Abs(bounds.min.y - originalBounds.min.y) / (float)bottomPaddingSubdivLevel), backPaddingSubdivLevel * Mathf.Floor(Mathf.Abs(bounds.min.z - originalBounds.min.z) / (float)backPaddingSubdivLevel) ); bounds.max = bounds.max - new Vector3( rightPaddingSubdivLevel * Mathf.Floor(Mathf.Abs(bounds.max.x - originalBounds.max.x) / (float)rightPaddingSubdivLevel), topPaddingSubdivLevel * Mathf.Floor(Mathf.Abs(bounds.max.y - originalBounds.max.y) / (float)topPaddingSubdivLevel), forwardPaddingSubdivLevel * Mathf.Floor(Mathf.Abs(bounds.max.z - originalBounds.max.z) / (float)forwardPaddingSubdivLevel) ); } // Should be called after EnsureSceneIsInBakingSet otherwise GetProfileForScene might be out of date internal void UpdateSceneBounds(Scene scene, bool updateGlobalVolumes) { var volumes = Object.FindObjectsOfType(); float prevBrickSize = ProbeReferenceVolume.instance.MinBrickSize(); int prevMaxSubdiv = ProbeReferenceVolume.instance.GetMaxSubdivision(); { var profile = GetProfileForScene(scene); if (profile == null) { if (volumes.Length > 0) Debug.LogWarning("A probe volume is present in the scene but a profile has not been set. Please configure a profile for your scene in the Probe Volume Baking settings."); return; } ProbeReferenceVolume.instance.SetMinBrickAndMaxSubdiv(profile.minBrickSize, profile.maxSubdivision); } var sceneGUID = GetSceneGUID(scene); bool boundFound = false; Bounds newBound = new Bounds(); foreach (var volume in volumes) { bool forceUpdate = updateGlobalVolumes && volume.mode == ProbeVolume.Mode.Global; if (!forceUpdate && volume.gameObject.scene != scene) continue; if (volume.mode != ProbeVolume.Mode.Local) volume.UpdateGlobalVolume(volume.mode == ProbeVolume.Mode.Global ? GIContributors.ContributorFilter.All : GIContributors.ContributorFilter.Scene); var transform = volume.gameObject.transform; var obb = new ProbeReferenceVolume.Volume(Matrix4x4.TRS(transform.position, transform.rotation, volume.GetExtents()), 0, 0); Bounds localBounds = obb.CalculateAABB(); InflateBound(ref localBounds, volume); if (!boundFound) { newBound = localBounds; boundFound = true; } else { newBound.Encapsulate(localBounds); } } hasProbeVolumes[sceneGUID] = boundFound; if (boundFound) sceneBounds[sceneGUID] = newBound; ProbeReferenceVolume.instance.SetMinBrickAndMaxSubdiv(prevBrickSize, prevMaxSubdiv); if (parentAsset != null) EditorUtility.SetDirty(parentAsset); } // It is important this is called after UpdateSceneBounds is called otherwise SceneHasProbeVolumes might be out of date internal void EnsurePerSceneData(Scene scene) { var sceneGUID = GetSceneGUID(scene); if (SceneHasProbeVolumes(sceneGUID)) { bool foundPerSceneData = false; foreach (var data in ProbeReferenceVolume.instance.perSceneDataList) { if (GetSceneGUID(data.gameObject.scene) == sceneGUID) { foundPerSceneData = true; break; } } if (!foundPerSceneData) { GameObject go = new GameObject("ProbeVolumePerSceneData"); go.hideFlags |= HideFlags.HideInHierarchy; go.AddComponent(); SceneManager.MoveGameObjectToScene(go, scene); } } } internal string GetFirstProbeVolumeSceneGUID(ProbeVolumeSceneData.BakingSet set) { foreach (var guid in set.sceneGUIDs) { if (sceneBakingSettings.ContainsKey(guid) && sceneProfiles.ContainsKey(guid)) return guid; } return null; } internal void OnSceneSaving(Scene scene, string path = null) { var bakingSet = GetBakingSetForScene(scene); if (bakingSet == null) return; // If we are called from the scene callback, we want to update all global volumes that are potentially affected bool updateGlobalVolumes = path != null; UpdateSceneBounds(scene, updateGlobalVolumes); EnsurePerSceneData(scene); } internal void SetProfileForScene(Scene scene, ProbeReferenceVolumeProfile profile) { if (sceneProfiles == null) sceneProfiles = new Dictionary(); var sceneGUID = GetSceneGUID(scene); sceneProfiles[sceneGUID] = profile; } internal void SetProfileForScene(string sceneGUID, ProbeReferenceVolumeProfile profile) { if (sceneProfiles == null) sceneProfiles = new Dictionary(); sceneProfiles[sceneGUID] = profile; } internal void SetBakeSettingsForScene(Scene scene, ProbeDilationSettings dilationSettings, VirtualOffsetSettings virtualOffsetSettings) { if (sceneBakingSettings == null) sceneBakingSettings = new Dictionary(); var sceneGUID = GetSceneGUID(scene); sceneBakingSettings[sceneGUID] = new ProbeVolumeBakingProcessSettings(dilationSettings, virtualOffsetSettings); } internal ProbeReferenceVolumeProfile GetProfileForScene(Scene scene) { var sceneGUID = GetSceneGUID(scene); if (sceneProfiles != null && sceneProfiles.ContainsKey(sceneGUID)) return sceneProfiles[sceneGUID]; return null; } internal bool BakeSettingsDefinedForScene(Scene scene) { var sceneGUID = GetSceneGUID(scene); return sceneBakingSettings.ContainsKey(sceneGUID); } internal ProbeVolumeBakingProcessSettings GetBakeSettingsForScene(Scene scene) { var sceneGUID = GetSceneGUID(scene); if (sceneBakingSettings != null && sceneBakingSettings.ContainsKey(sceneGUID)) return sceneBakingSettings[sceneGUID]; return ProbeVolumeBakingProcessSettings.Default; } // This is sub-optimal, but because is called once when kicking off a bake internal BakingSet GetBakingSetForScene(Scene scene) { var sceneGUID = GetSceneGUID(scene); foreach (var set in bakingSets) { foreach (var guidInSet in set.sceneGUIDs) { if (guidInSet == sceneGUID) return set; } } return null; } internal bool SceneHasProbeVolumes(string sceneGUID) => hasProbeVolumes != null && hasProbeVolumes.TryGetValue(sceneGUID, out var hasPV) && hasPV; internal bool SceneHasProbeVolumes(Scene scene) => SceneHasProbeVolumes(GetSceneGUID(scene)); #endif } }