2026-06-21 10:45:48 +01:00
|
|
|
using UnityEditor;
|
2026-06-30 19:14:49 +01:00
|
|
|
using UnityEditor.SceneManagement;
|
|
|
|
|
using Aetheria.Rendering.PostProcessing;
|
2026-06-21 10:45:48 +01:00
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.Rendering;
|
|
|
|
|
using UnityEngine.Rendering.Universal;
|
2026-06-30 19:14:49 +01:00
|
|
|
using System.Linq;
|
|
|
|
|
using System;
|
2026-06-21 10:45:48 +01:00
|
|
|
|
|
|
|
|
public static class AetheriaUrpBootstrap
|
|
|
|
|
{
|
|
|
|
|
private const string SettingsFolder = "Assets/Settings/Rendering";
|
|
|
|
|
private const string PipelineAssetPath = SettingsFolder + "/AetheriaURPAsset.asset";
|
|
|
|
|
private const string RendererAssetPath = SettingsFolder + "/AetheriaUniversalRenderer.asset";
|
2026-06-30 19:14:49 +01:00
|
|
|
private const string MainMenuVolumeProfilePath = SettingsFolder + "/AetheriaMainMenuVolumeProfile.asset";
|
|
|
|
|
private const string DefaultPostProcessDataPath =
|
|
|
|
|
"Packages/com.unity.render-pipelines.universal/Runtime/Data/PostProcessData.asset";
|
|
|
|
|
|
|
|
|
|
[InitializeOnLoadMethod]
|
|
|
|
|
private static void EnsureAetheriaRendererFeaturesOnLoad()
|
|
|
|
|
{
|
|
|
|
|
EditorApplication.delayCall += EnsureUrpAssetsAndAssign;
|
|
|
|
|
}
|
2026-06-21 10:45:48 +01:00
|
|
|
|
|
|
|
|
[MenuItem("Aetheria/Rendering/Ensure URP Assets")]
|
|
|
|
|
public static void EnsureUrpAssetsAndAssign()
|
|
|
|
|
{
|
|
|
|
|
EnsureFolder(SettingsFolder);
|
|
|
|
|
|
|
|
|
|
var rendererData = AssetDatabase.LoadAssetAtPath<UniversalRendererData>(RendererAssetPath);
|
|
|
|
|
if (rendererData == null)
|
|
|
|
|
{
|
|
|
|
|
rendererData = ScriptableObject.CreateInstance<UniversalRendererData>();
|
|
|
|
|
AssetDatabase.CreateAsset(rendererData, RendererAssetPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pipelineAsset = AssetDatabase.LoadAssetAtPath<UniversalRenderPipelineAsset>(PipelineAssetPath);
|
|
|
|
|
if (pipelineAsset == null)
|
|
|
|
|
{
|
|
|
|
|
pipelineAsset = UniversalRenderPipelineAsset.Create(rendererData);
|
|
|
|
|
AssetDatabase.CreateAsset(pipelineAsset, PipelineAssetPath);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 19:14:49 +01:00
|
|
|
EnsureRendererFeature(
|
|
|
|
|
rendererData,
|
|
|
|
|
"AetheriaVolumeSamplingRendererFeature",
|
|
|
|
|
"Aetheria Volume Sampling");
|
|
|
|
|
EnsureRendererFeature(
|
|
|
|
|
rendererData,
|
|
|
|
|
"AetheriaVolumeCloudRendererFeature",
|
|
|
|
|
"Aetheria Volume Clouds");
|
|
|
|
|
EnsureRendererFeature(
|
|
|
|
|
rendererData,
|
|
|
|
|
"AetheriaStardustRendererFeature",
|
|
|
|
|
"Aetheria Stardust");
|
|
|
|
|
EnsureRendererFeature(
|
|
|
|
|
rendererData,
|
|
|
|
|
"Aetheria.Rendering.PostProcessing.AetheriaFogBlurRendererFeature",
|
|
|
|
|
"Aetheria Fog Blur");
|
|
|
|
|
EnsurePostProcessing(rendererData);
|
|
|
|
|
var mainMenuVolumeProfile = EnsureMainMenuVolumeProfile();
|
|
|
|
|
EnsureLoadedCameraPostProcessing();
|
|
|
|
|
EnsureLoadedMainMenuVolumes(mainMenuVolumeProfile);
|
|
|
|
|
|
2026-06-21 10:45:48 +01:00
|
|
|
GraphicsSettings.defaultRenderPipeline = pipelineAsset;
|
|
|
|
|
QualitySettings.renderPipeline = pipelineAsset;
|
|
|
|
|
|
|
|
|
|
EditorUtility.SetDirty(pipelineAsset);
|
|
|
|
|
EditorUtility.SetDirty(rendererData);
|
|
|
|
|
AssetDatabase.SaveAssets();
|
|
|
|
|
AssetDatabase.Refresh();
|
|
|
|
|
|
|
|
|
|
Debug.Log($"Aetheria URP asset assigned: {PipelineAssetPath}");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 19:14:49 +01:00
|
|
|
private static void EnsureRendererFeature(
|
|
|
|
|
UniversalRendererData rendererData,
|
|
|
|
|
string featureTypeName,
|
|
|
|
|
string featureName)
|
|
|
|
|
{
|
|
|
|
|
var featureType = AppDomain.CurrentDomain
|
|
|
|
|
.GetAssemblies()
|
|
|
|
|
.Select(assembly => assembly.GetType(featureTypeName))
|
|
|
|
|
.FirstOrDefault(type => type != null);
|
|
|
|
|
if (featureType == null || !typeof(ScriptableRendererFeature).IsAssignableFrom(featureType))
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning($"Aetheria URP bootstrap could not find renderer feature type {featureTypeName}.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var existing = AssetDatabase
|
|
|
|
|
.LoadAllAssetsAtPath(RendererAssetPath)
|
|
|
|
|
.OfType<ScriptableRendererFeature>()
|
|
|
|
|
.FirstOrDefault(feature => feature.GetType() == featureType);
|
|
|
|
|
if (existing == null)
|
|
|
|
|
{
|
|
|
|
|
existing = (ScriptableRendererFeature)ScriptableObject.CreateInstance(featureType);
|
|
|
|
|
existing.name = featureName;
|
|
|
|
|
AssetDatabase.AddObjectToAsset(existing, rendererData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!rendererData.rendererFeatures.Contains(existing))
|
|
|
|
|
rendererData.rendererFeatures.Add(existing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void EnsurePostProcessing(UniversalRendererData rendererData)
|
|
|
|
|
{
|
|
|
|
|
var serialized = new SerializedObject(rendererData);
|
|
|
|
|
var postProcessData = serialized.FindProperty("postProcessData");
|
|
|
|
|
if (postProcessData == null || postProcessData.objectReferenceValue != null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
postProcessData.objectReferenceValue =
|
|
|
|
|
AssetDatabase.LoadAssetAtPath<PostProcessData>(DefaultPostProcessDataPath);
|
|
|
|
|
serialized.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static VolumeProfile EnsureMainMenuVolumeProfile()
|
|
|
|
|
{
|
|
|
|
|
var profile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(MainMenuVolumeProfilePath);
|
|
|
|
|
if (profile == null)
|
|
|
|
|
{
|
|
|
|
|
profile = ScriptableObject.CreateInstance<VolumeProfile>();
|
|
|
|
|
profile.name = "AetheriaMainMenuVolumeProfile";
|
|
|
|
|
AssetDatabase.CreateAsset(profile, MainMenuVolumeProfilePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EnsureVolumeComponent(profile, out Tonemapping tonemapping);
|
|
|
|
|
tonemapping.active = true;
|
|
|
|
|
tonemapping.mode.overrideState = true;
|
|
|
|
|
tonemapping.mode.value = TonemappingMode.ACES;
|
|
|
|
|
|
|
|
|
|
EnsureVolumeComponent(profile, out ColorAdjustments colorAdjustments);
|
|
|
|
|
colorAdjustments.active = true;
|
|
|
|
|
colorAdjustments.postExposure.overrideState = true;
|
|
|
|
|
colorAdjustments.postExposure.value = -1f;
|
|
|
|
|
|
|
|
|
|
EnsureVolumeComponent(profile, out Bloom bloom);
|
|
|
|
|
bloom.active = true;
|
|
|
|
|
bloom.threshold.overrideState = true;
|
|
|
|
|
bloom.threshold.value = 1.5f;
|
|
|
|
|
bloom.intensity.overrideState = true;
|
|
|
|
|
bloom.intensity.value = 3f;
|
|
|
|
|
bloom.scatter.overrideState = true;
|
|
|
|
|
bloom.scatter.value = 0.9f;
|
|
|
|
|
bloom.tint.overrideState = true;
|
|
|
|
|
bloom.tint.value = Color.white;
|
|
|
|
|
|
|
|
|
|
EnsureVolumeComponent(profile, out Vignette vignette);
|
|
|
|
|
vignette.active = true;
|
|
|
|
|
vignette.intensity.overrideState = true;
|
|
|
|
|
vignette.intensity.value = 0.15f;
|
|
|
|
|
vignette.smoothness.overrideState = true;
|
|
|
|
|
vignette.smoothness.value = 0.45f;
|
|
|
|
|
|
|
|
|
|
EnsureVolumeComponent(profile, out AetheriaFogBlur fogBlur);
|
|
|
|
|
fogBlur.active = true;
|
|
|
|
|
fogBlur.focusDistance.overrideState = true;
|
|
|
|
|
fogBlur.focusDistance.value = 10f;
|
|
|
|
|
fogBlur.aperture.overrideState = true;
|
|
|
|
|
fogBlur.aperture.value = 5.6f;
|
|
|
|
|
fogBlur.focalLength.overrideState = true;
|
|
|
|
|
fogBlur.focalLength.value = 50f;
|
|
|
|
|
fogBlur.kernelSize.overrideState = true;
|
|
|
|
|
fogBlur.kernelSize.value = (int)AetheriaFogBlurKernelSize.Medium;
|
|
|
|
|
|
|
|
|
|
EditorUtility.SetDirty(profile);
|
|
|
|
|
return profile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void EnsureVolumeComponent<T>(VolumeProfile profile, out T component)
|
|
|
|
|
where T : VolumeComponent
|
|
|
|
|
{
|
|
|
|
|
if (!profile.TryGet(out component))
|
|
|
|
|
{
|
|
|
|
|
component = profile.Add<T>(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void EnsureLoadedCameraPostProcessing()
|
|
|
|
|
{
|
|
|
|
|
foreach (var camera in Resources.FindObjectsOfTypeAll<Camera>())
|
|
|
|
|
{
|
|
|
|
|
if (camera == null || !camera.gameObject.scene.IsValid() || !camera.gameObject.scene.isLoaded)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var hasAetheriaVolumeRenderer = camera.GetComponent<VolumeCloudRenderer>() != null;
|
|
|
|
|
if (!camera.CompareTag("MainCamera") && !hasAetheriaVolumeRenderer)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var data = camera.GetComponent<UniversalAdditionalCameraData>();
|
|
|
|
|
if (data == null)
|
|
|
|
|
data = camera.gameObject.AddComponent<UniversalAdditionalCameraData>();
|
|
|
|
|
|
|
|
|
|
var changed = false;
|
|
|
|
|
changed |= SetIfDifferent(() => data.renderPostProcessing, value => data.renderPostProcessing = value, true);
|
|
|
|
|
changed |= SetIfDifferent<LayerMask>(
|
|
|
|
|
() => data.volumeLayerMask,
|
|
|
|
|
value => data.volumeLayerMask = value,
|
|
|
|
|
LayerMask.GetMask("PostProcessing"));
|
|
|
|
|
changed |= SetIfDifferent(() => data.volumeTrigger, value => data.volumeTrigger = value, camera.transform);
|
|
|
|
|
changed |= SetIfDifferent(() => data.requiresDepthOption, value => data.requiresDepthOption = value, CameraOverrideOption.On);
|
|
|
|
|
changed |= SetIfDifferent(() => data.stopNaN, value => data.stopNaN = value, true);
|
|
|
|
|
changed |= SetIfDifferent(() => data.dithering, value => data.dithering = value, true);
|
|
|
|
|
|
|
|
|
|
changed |= RemoveLegacyPostProcessLayer(camera.gameObject);
|
|
|
|
|
|
|
|
|
|
if (!changed)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
EditorUtility.SetDirty(camera.gameObject);
|
|
|
|
|
EditorUtility.SetDirty(data);
|
|
|
|
|
EditorSceneManager.MarkSceneDirty(camera.gameObject.scene);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool RemoveLegacyPostProcessLayer(GameObject cameraObject)
|
|
|
|
|
{
|
|
|
|
|
var removed = false;
|
|
|
|
|
foreach (var component in cameraObject.GetComponents<Component>())
|
|
|
|
|
{
|
|
|
|
|
if (component == null)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var type = component.GetType();
|
|
|
|
|
if (type.FullName != "UnityEngine.Rendering.PostProcessing.PostProcessLayer")
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
UnityEngine.Object.DestroyImmediate(component);
|
|
|
|
|
removed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return removed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void EnsureLoadedMainMenuVolumes(VolumeProfile mainMenuVolumeProfile)
|
|
|
|
|
{
|
|
|
|
|
if (mainMenuVolumeProfile == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
foreach (var volume in Resources.FindObjectsOfTypeAll<Volume>())
|
|
|
|
|
{
|
|
|
|
|
if (volume == null || !volume.gameObject.scene.IsValid() || !volume.gameObject.scene.isLoaded)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var isMainMenuVolume = volume.gameObject.name == "Postprocessing" &&
|
|
|
|
|
volume.gameObject.scene.path.Replace('\\', '/').EndsWith("/Main Menu.unity", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
if (!isMainMenuVolume)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var changed = false;
|
|
|
|
|
changed |= SetIfDifferent(() => volume.isGlobal, value => volume.isGlobal = value, true);
|
|
|
|
|
changed |= SetIfDifferent(() => volume.sharedProfile, value => volume.sharedProfile = value, mainMenuVolumeProfile);
|
|
|
|
|
|
|
|
|
|
var postProcessingLayer = LayerMask.NameToLayer("PostProcessing");
|
|
|
|
|
if (postProcessingLayer >= 0 && volume.gameObject.layer != postProcessingLayer)
|
|
|
|
|
{
|
|
|
|
|
volume.gameObject.layer = postProcessingLayer;
|
|
|
|
|
changed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!changed)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
EditorUtility.SetDirty(volume);
|
|
|
|
|
EditorUtility.SetDirty(volume.gameObject);
|
|
|
|
|
EditorSceneManager.MarkSceneDirty(volume.gameObject.scene);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool SetIfDifferent<T>(Func<T> get, Action<T> set, T value)
|
|
|
|
|
{
|
|
|
|
|
if (Equals(get(), value))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
set(value);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 10:45:48 +01:00
|
|
|
private static void EnsureFolder(string folderPath)
|
|
|
|
|
{
|
|
|
|
|
var parts = folderPath.Split('/');
|
|
|
|
|
var current = parts[0];
|
|
|
|
|
for (var i = 1; i < parts.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var next = current + "/" + parts[i];
|
|
|
|
|
if (!AssetDatabase.IsValidFolder(next))
|
|
|
|
|
{
|
|
|
|
|
AssetDatabase.CreateFolder(current, parts[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current = next;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|