using System; using System.IO; using System.Collections.Generic; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; using UnityEngine.Rendering; namespace UnityEditor.Rendering { class ProbeVolumeBuildProcessor : BuildPlayerProcessor, IPostprocessBuildWithReport { const string kTempAPVStreamingAssetsPath = "TempAPVStreamingAssets"; string GetTempAPVStreamingAssetsPath() { var libraryPath = Path.GetFullPath("Library"); return Path.Combine(libraryPath, kTempAPVStreamingAssetsPath); } // Include an asset in the build. The mechanism for doing so depends on whether we are using StreamingAssets path. static void IncludeStreamableAsset(ProbeVolumeStreamableAsset asset, string basePath, bool useStreamingAsset) { if (useStreamingAsset) { asset.ClearAssetReferenceForBuild(); CopyStreamableAsset(asset, basePath); } else { asset.EnsureAssetLoaded(); } } // Ensure that an asset is not included in the build. static void StripStreambleAsset(ProbeVolumeStreamableAsset asset) { asset.ClearAssetReferenceForBuild(); } static void CopyStreamableAsset(ProbeVolumeStreamableAsset asset, string basePath) { var assetPath = asset.GetAssetPath(); if (!File.Exists(assetPath)) { Debug.LogError($"Missing APV data asset {assetPath}. Please make sure that the lighting has been baked properly."); return; } File.Copy(assetPath, Path.Combine(basePath, asset.assetGUID + ".bytes")); } void GetProbeVolumeProjectSettings(BuildTarget target, out bool supportProbeVolume, out ProbeVolumeSHBands maxSHBands) { // Grab all assets used for the build. List srpAssets = new List(); target.TryGetRenderPipelineAssets(srpAssets); maxSHBands = ProbeVolumeSHBands.SphericalHarmonicsL1; supportProbeVolume = false; foreach (var asset in srpAssets) { var probeVolumeEnabledRenderPipeline = asset as IProbeVolumeEnabledRenderPipeline; // If at least one asset needs L2 then we can return. if (probeVolumeEnabledRenderPipeline != null) { supportProbeVolume |= probeVolumeEnabledRenderPipeline.supportProbeVolume; if (probeVolumeEnabledRenderPipeline.maxSHBands == ProbeVolumeSHBands.SphericalHarmonicsL2) maxSHBands = ProbeVolumeSHBands.SphericalHarmonicsL2; } } } // Keep track of which assets we touched during the build, so we can restore them after the build. private static HashSet s_BakingSetsProcessedLastBuild = new(); public override void PrepareForBuild(BuildPlayerContext buildPlayerContext) { GetProbeVolumeProjectSettings(buildPlayerContext.BuildPlayerOptions.target, out bool supportProbeVolume, out var maxSHBands); if (!supportProbeVolume) return; // We need to make sure the baking set map is properly initialized. // Since it's done only at APV init, we call it explicitely here in case APV was not used yet in this session. ProbeVolumeBakingSet.SyncBakingSets(); var tempStreamingAssetsPath = GetTempAPVStreamingAssetsPath(); // Delete previously built data. This way we remove any data from scenes that are no longer in the build. if (Directory.Exists(tempStreamingAssetsPath)) { try { Directory.Delete(tempStreamingAssetsPath, recursive: true); } catch { // This can happen if for example the folder is write protected // This is not ideal but don't block project build } } Directory.CreateDirectory(tempStreamingAssetsPath); s_BakingSetsProcessedLastBuild.Clear(); foreach (var scene in buildPlayerContext.BuildPlayerOptions.scenes) { var sceneGUID = AssetDatabase.AssetPathToGUID(scene); var bakingSet = ProbeVolumeBakingSet.GetBakingSetForScene(sceneGUID); if (bakingSet != null) { // Already processed (different scenes can belong to the same baking set). if (s_BakingSetsProcessedLastBuild.Contains(bakingSet)) continue; if (!bakingSet.cellSharedDataAsset.IsValid()) // Not baked continue; // APV doesn't work with WebGL, so let's warn the user. if (buildPlayerContext.BuildPlayerOptions.target == BuildTarget.WebGL) { // WebGPU does support APV so only warn if WebGPU is not enabled. GraphicsDeviceType[] apis = PlayerSettings.GetGraphicsAPIs(BuildTarget.WebGL); var index = Array.FindIndex(apis, x => x == GraphicsDeviceType.WebGPU); if (index == -1) { Debug.LogError( $"The scene '{scene}' contains baked Adaptive Probe Volumes, but the build target is WebGL. " + "Adaptive Probe Volumes are not supported when targeting WebGL."); continue; } } var bakingSetGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(bakingSet)); var basePath = Path.Combine(tempStreamingAssetsPath, bakingSetGUID); Directory.CreateDirectory(basePath); bool useStreamingAsset = !GraphicsSettings.GetRenderPipelineSettings().probeVolumeDisableStreamingAssets; IncludeStreamableAsset(bakingSet.cellSharedDataAsset, basePath, useStreamingAsset); IncludeStreamableAsset(bakingSet.cellBricksDataAsset, basePath, useStreamingAsset); // For now we always strip support data in build as it's mostly unsupported. // Later we'll need a proper option to strip it or not. bool stripSupportData = true; if (stripSupportData) StripStreambleAsset(bakingSet.cellSupportDataAsset); else IncludeStreamableAsset(bakingSet.cellSupportDataAsset, basePath, useStreamingAsset); foreach (var scenario in bakingSet.scenarios) { IncludeStreamableAsset(scenario.Value.cellDataAsset, basePath, useStreamingAsset); if (maxSHBands == ProbeVolumeSHBands.SphericalHarmonicsL2) IncludeStreamableAsset(scenario.Value.cellOptionalDataAsset, basePath, useStreamingAsset); else StripStreambleAsset(scenario.Value.cellOptionalDataAsset); if (bakingSet.bakedProbeOcclusion) IncludeStreamableAsset(scenario.Value.cellProbeOcclusionDataAsset, basePath, useStreamingAsset); else StripStreambleAsset(scenario.Value.cellProbeOcclusionDataAsset); } s_BakingSetsProcessedLastBuild.Add(bakingSet); } } buildPlayerContext.AddAdditionalPathToStreamingAssets(tempStreamingAssetsPath, AdaptiveProbeVolumes.kAPVStreamingAssetsPath); } public void OnPostprocessBuild(BuildReport report) { if (s_BakingSetsProcessedLastBuild == null || s_BakingSetsProcessedLastBuild.Count == 0) return; // Go over each asset reference we touched during the last build, make sure asset references are intact. foreach (var bakingSet in s_BakingSetsProcessedLastBuild) { bakingSet.cellBricksDataAsset.EnsureAssetLoaded(); bakingSet.cellSharedDataAsset.EnsureAssetLoaded(); bakingSet.cellSupportDataAsset.EnsureAssetLoaded(); foreach (var scenario in bakingSet.scenarios) { scenario.Value.cellDataAsset.EnsureAssetLoaded(); scenario.Value.cellOptionalDataAsset.EnsureAssetLoaded(); scenario.Value.cellProbeOcclusionDataAsset.EnsureAssetLoaded(); } } s_BakingSetsProcessedLastBuild.Clear(); } } }