using System; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; using UnityEngine.SceneManagement; using System.Reflection; using System.Linq; namespace UnityEngine.Experimental.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); 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 { [SerializeField] public string sceneGUID; [SerializeField] public ProbeVolumeBakingProcessSettings settings; } [System.Serializable] internal class BakingSet { public string name; public List sceneGUIDs = new List(); public ProbeVolumeBakingProcessSettings settings; public ProbeReferenceVolumeProfile profile; } [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; /// 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); } foreach (var set in serializedBakingSets) bakingSets.Add(set); } // 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); } // Initialize baking set in case it's empty: if (bakingSets.Count == 0) { var set = CreateNewBakingSet("Default"); set.sceneGUIDs = serializedProfiles.Select(s => s.sceneGUID).ToList(); } 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 ProjectWindowUtil.CreateAsset(newProfile, name + ".asset"); #endif set.name = name; set.profile = newProfile; set.settings = new ProbeVolumeBakingProcessSettings { dilationSettings = new ProbeDilationSettings { enableDilation = true, dilationDistance = 1, dilationValidityThreshold = 0.25f, dilationIterations = 1, squaredDistWeighting = true, }, virtualOffsetSettings = new VirtualOffsetSettings { useVirtualOffset = true, outOfGeoOffset = 0.01f, searchMultiplier = 0.2f, } }; } 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 private int FindInflatingBrickSize(Vector3 size, ProbeVolume pv) { var refVol = ProbeReferenceVolume.instance; float minSizedDim = Mathf.Min(size.x, Mathf.Min(size.y, size.z)); float minBrickSize = refVol.MinBrickSize(); float minSideInBricks = Mathf.CeilToInt(minSizedDim / minBrickSize); int absoluteMaxSubdiv = ProbeReferenceVolume.instance.GetMaxSubdivision() - 1; minSideInBricks = Mathf.Max(minSideInBricks, Mathf.Pow(3, absoluteMaxSubdiv - (pv.overridesSubdivLevels ? pv.highestSubdivLevelOverride : 0))); int subdivLevel = Mathf.FloorToInt(Mathf.Log(minSideInBricks, 3)); return subdivLevel; } 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 float rightPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(FindInflatingBrickSize(new Vector3(maxPadding.x, originalBounds.size.y, originalBounds.size.z), pv)); float leftPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(FindInflatingBrickSize(new Vector3(minPadding.x, originalBounds.size.y, originalBounds.size.z), pv)); float topPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(FindInflatingBrickSize(new Vector3(originalBounds.size.x, maxPadding.y, originalBounds.size.z), pv)); float bottomPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(FindInflatingBrickSize(new Vector3(originalBounds.size.x, minPadding.y, originalBounds.size.z), pv)); float forwardPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(FindInflatingBrickSize(new Vector3(originalBounds.size.x, originalBounds.size.y, maxPadding.z), pv)); float backPaddingSubdivLevel = ProbeReferenceVolume.instance.BrickSize(FindInflatingBrickSize(new Vector3(originalBounds.size.x, originalBounds.size.y, minPadding.z), pv)); // 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) ); } internal void UpdateSceneBounds(Scene scene) { var volumes = UnityEngine.GameObject.FindObjectsOfType(); // If we have not yet loaded any asset, we haven't initialized the probe reference volume with any info from the profile. // As a result we need to prime with the profile info directly stored here. { 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) { if (volume.globalVolume) volume.UpdateGlobalVolume(scene); var volumeSceneGUID = GetSceneGUID(volume.gameObject.scene); if (volumeSceneGUID == sceneGUID) { var pos = volume.gameObject.transform.position; var extent = volume.GetExtents(); Bounds localBounds = new Bounds(pos, extent); InflateBound(ref localBounds, volume); if (!boundFound) { newBound = localBounds; boundFound = true; } else { newBound.Encapsulate(localBounds); } } } if (boundFound) { if (sceneBounds == null) { sceneBounds = new Dictionary(); hasProbeVolumes = new Dictionary(); } if (sceneBounds.ContainsKey(sceneGUID)) { sceneBounds[sceneGUID] = newBound; } else { sceneBounds.Add(sceneGUID, newBound); } } if (hasProbeVolumes.ContainsKey(sceneGUID)) hasProbeVolumes[sceneGUID] = boundFound; else hasProbeVolumes.Add(sceneGUID, boundFound); if (parentAsset != null) { EditorUtility.SetDirty(parentAsset); } } internal void EnsureSceneHasProbeVolumeIsValid(Scene scene) { var sceneGUID = GetSceneGUID(scene); var volumes = UnityEngine.GameObject.FindObjectsOfType(); foreach (var volume in volumes) { if (GetSceneGUID(volume.gameObject.scene) == sceneGUID) { hasProbeVolumes[sceneGUID] = true; return; } } hasProbeVolumes[sceneGUID] = false; } // It is important this is called after UpdateSceneBounds is called! internal void EnsurePerSceneData(Scene scene) { var sceneGUID = GetSceneGUID(scene); if (hasProbeVolumes.ContainsKey(sceneGUID) && hasProbeVolumes[sceneGUID]) { var perSceneData = UnityEngine.GameObject.FindObjectsOfType(); bool foundPerSceneData = false; foreach (var data in perSceneData) { if (GetSceneGUID(data.gameObject.scene) == sceneGUID) { foundPerSceneData = true; } } if (!foundPerSceneData) { GameObject go = new GameObject("ProbeVolumePerSceneData"); go.hideFlags |= HideFlags.HideInHierarchy; go.AddComponent(); SceneManager.MoveGameObjectToScene(go, scene); } } } internal void EnsureSceneIsInBakingSet(Scene scene) { var sceneGUID = GetSceneGUID(scene); foreach (var set in bakingSets) if (set.sceneGUIDs.Contains(sceneGUID)) return; // The scene is not in a baking set, we need to add it if (bakingSets.Count == 0) return; // Technically shouldn't be possible since it's blocked in the UI bakingSets[0].sceneGUIDs.Add(sceneGUID); SyncBakingSetSettings(); } 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 OnSceneSaved(Scene scene) { EnsureSceneHasProbeVolumeIsValid(scene); EnsureSceneIsInBakingSet(scene); EnsurePerSceneData(scene); UpdateSceneBounds(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); ProbeVolumeBakingProcessSettings settings = new ProbeVolumeBakingProcessSettings(); settings.dilationSettings = dilationSettings; settings.virtualOffsetSettings = virtualOffsetSettings; sceneBakingSettings[sceneGUID] = settings; } 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 new ProbeVolumeBakingProcessSettings(); } // 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(Scene scene) { var sceneGUID = GetSceneGUID(scene); return hasProbeVolumes != null && hasProbeVolumes.ContainsKey(sceneGUID) && hasProbeVolumes[sceneGUID]; } #endif } }