Files
2025-11-30 08:35:03 +02:00

336 lines
14 KiB
C#

using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
namespace Needle
{
public class BetterCubemapEditor : ShaderGUI
{
private static readonly int AutoBakeOnChanges = Shader.PropertyToID("_AutoBakeOnChanges");
private static bool BakedTextureFixIsRunning = false;
[InitializeOnLoadMethod]
private static void RegisterForLightmapChanges()
{
Lightmapping.bakeCompleted += () =>
{
BakedTextureFixIsRunning = false;
var shader = Shader.Find("Skybox/Better Cubemap (Needle)");
if (!RenderSettings.skybox || RenderSettings.skybox.shader != shader)
return;
var mat = RenderSettings.skybox;
if (mat.GetInt(AutoBakeOnChanges) == 0)
return;
var cubemap = ReflectionProbe.defaultTexture;
if (cubemap)
{
// Debug.Log("Found cubemap at " + locationOfReflProbe0, cubemap);
}
else
{
// Debug.LogError("No cubemap after lighting bake! Unity bug? Baking again...");
if (!Lightmapping.isRunning) {
if (Lightmapping.BakeAsync()) {
BakedTextureFixIsRunning = true;
}
}
}
if (cubemap)
Shader.SetGlobalTexture("_Needle_SkyboxPreconvolutedTex", cubemap);
if (cubemap)
{
mat.SetFloat("_CUBEMAP_USAGE", 1);
mat.SetKeyword(new LocalKeyword(shader, "_CUBEMAP_USAGE_BUILTIN"), false);
mat.SetKeyword(new LocalKeyword(shader, "_CUBEMAP_USAGE_EXTRA"), true);
mat.SetKeyword(new LocalKeyword(shader, "_CUBEMAP_USAGE_ORIGINAL"), false);
}
else
{
mat.SetFloat("_CUBEMAP_USAGE", 2);
mat.SetKeyword(new LocalKeyword(shader, "_CUBEMAP_USAGE_BUILTIN"), false);
mat.SetKeyword(new LocalKeyword(shader, "_CUBEMAP_USAGE_EXTRA"), false);
mat.SetKeyword(new LocalKeyword(shader, "_CUBEMAP_USAGE_ORIGINAL"), true);
}
if (RenderSettings.skybox && RenderSettings.skybox.HasProperty("_Tex"))
lastCubemap = RenderSettings.skybox.GetTexture("_Tex");
};
SceneManager.activeSceneChanged += (scene1, scene2) =>
{
if (Application.isPlaying) return;
if (!RenderSettings.skybox) return;
var shader = RenderSettings.skybox.shader;
if (!shader || shader.name != "Skybox/Better Cubemap (Needle)") return;
var mat = RenderSettings.skybox;
if (mat.GetInt(AutoBakeOnChanges) == 0) return;
if (!CanAutoBake) return;
Lightmapping.BakeAsync();
};
EditorApplication.update += ValidateSkybox;
if (RenderSettings.skybox && RenderSettings.skybox.HasProperty("_Tex"))
lastCubemap = RenderSettings.skybox.GetTexture("_Tex");
else
lastCubemap = null;
}
private static long lastValidationFrame = -1;
private static Texture lastCubemap = null;
internal static void ValidateSkybox()
{
if (Time.renderedFrameCount <= lastValidationFrame) return;
lastValidationFrame = Time.renderedFrameCount;
// return if we're in the standalone profiler
if (ModeService.currentId == "profiler") return;
// we want to rebake lighting if the current selected skybox material
// doesn't have the correct data set
if (Lightmapping.isRunning) return;
if (!RenderSettings.skybox) return;
var shader = RenderSettings.skybox.shader;
if (!shader || shader.name != "Skybox/Better Cubemap (Needle)") return;
var mat = RenderSettings.skybox;
if (mat.GetInt(AutoBakeOnChanges) == 0) return;
if (!CanAutoBake) return;
var convolutedCubemap = Shader.GetGlobalTexture("_Needle_SkyboxPreconvolutedTex");
// theoretically, convolutedCubemap should always be the same as ReflectionProbe.defaultTexture
var cubemap = mat.GetTexture("_Tex");
if (!convolutedCubemap || lastCubemap != cubemap)
{
lastCubemap = cubemap;
Lightmapping.BakeAsync();
}
}
public override void AssignNewShaderToMaterial(Material material, Shader oldShader, Shader newShader)
{
base.AssignNewShaderToMaterial(material, oldShader, newShader);
if (!CanAutoBake) return;
if (Lightmapping.isRunning) return;
if (IsUsedByActiveScene(material))
Lightmapping.BakeAsync();
}
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
materialEditor.serializedObject.Update();
// filter out properties:
// _Blurriness
// _LodExposure
// _BakeBlurriness
var blurriness = FindProperty("_BackgroundBlurriness", properties);
var lodExposure = FindProperty("_BackgroundIntensity", properties);
var bakeBlurriness = FindProperty("_BakeBlurriness", properties);
var blurToggle = FindProperty("_Lod", properties);
var filteredProperties = new MaterialProperty[] { blurriness, lodExposure, bakeBlurriness };
var propertiesWithoutBake = new MaterialProperty[] { blurriness, lodExposure };
var filtered = properties
.Except(filteredProperties)
.Except(new MaterialProperty[] { blurToggle })
.ToArray();
materialEditor.SetDefaultGUIWidths();
var needsBake = false;
EditorGUI.BeginChangeCheck();
for (int index = 0; index < filtered.Length; ++index)
{
var prop = filtered[index];
if ((prop.flags & MaterialProperty.PropFlags.HideInInspector) == 0)
materialEditor.ShaderProperty(
EditorGUILayout.GetControlRect(true, materialEditor.GetPropertyHeight(prop, prop.displayName),
EditorStyles.layerMaskField), prop, prop.displayName);
}
needsBake |= EditorGUI.EndChangeCheck();
var mat = materialEditor.target as Material;
if (!mat) return;
var canBake = CanAutoBake;
EditorGUI.BeginDisabledGroup(!canBake);
var state = mat.GetInt(AutoBakeOnChanges);
EditorGUI.BeginChangeCheck();
state = EditorGUILayout.Toggle(
new GUIContent("Update Lighting on Changes",
"When enabled, lighting is regenerated upon changes. This is the same as clicking \"Generate\" in the Lighting Settings."),
state == 1)
? 1
: 0;
if (EditorGUI.EndChangeCheck())
{
mat.SetInt(AutoBakeOnChanges, state);
if (state > 0) needsBake = true;
}
EditorGUI.EndDisabledGroup();
if (!canBake)
{
EditorGUILayout.HelpBox("Can't automatically generate lighting: Baked GI or Realtime GI are enabled. Please generate manually.", MessageType.None);
EditorGUI.BeginDisabledGroup(Lightmapping.isRunning);
if (GUILayout.Button("Generate Lighting"))
{
Lightmapping.BakeAsync();
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.Space();
}
EditorGUI.BeginChangeCheck();
materialEditor.ShaderProperty(blurToggle, blurToggle.displayName);
if (EditorGUI.EndChangeCheck() && bakeBlurriness.floatValue > 0)
needsBake = true;
if (mat.IsKeywordEnabled("_LOD_ON"))
{
EditorGUI.indentLevel++;
for (int index = 0; index < filteredProperties.Length; ++index)
{
var prop = filteredProperties[index];
if (prop == blurriness)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Background Settings", EditorStyles.boldLabel);
EditorGUILayout.LabelField("These settings affect only the background, not reflections.", EditorStyles.miniLabel);
}
else if (prop == bakeBlurriness)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Baking Settings", EditorStyles.boldLabel);
EditorGUILayout.LabelField("These settings affect both reflections and the background.", EditorStyles.miniLabel);
}
if (!propertiesWithoutBake.Contains(prop))
EditorGUI.BeginChangeCheck();
if ((prop.flags & MaterialProperty.PropFlags.HideInInspector) == 0)
materialEditor.ShaderProperty(
EditorGUILayout.GetControlRect(true, materialEditor.GetPropertyHeight(prop, prop.displayName),
EditorStyles.layerMaskField), prop, prop.displayName);
if (!propertiesWithoutBake.Contains(prop))
needsBake |= EditorGUI.EndChangeCheck();
}
EditorGUI.indentLevel--;
}
if (BakedTextureFixIsRunning || Lightmapping.isRunning)
{
EditorGUILayout.Space();
EditorGUILayout.HelpBox("Baking lighting...", MessageType.Info);
}
// No need to draw the Queue...
// var emptyArray = new MaterialProperty[] {};
// materialEditor.PropertiesDefaultGUI(emptyArray);
if (needsBake && state > 0)
{
// check if we can safely kick off a lighting build
// We can only do that if
// - auto generate is off or we're in 2023.2+
// - light mapping is disabled
// - realtime light mapping is disabled
if (canBake && mat == RenderSettings.skybox)
{
// check if this is a mouse up
var e = Event.current;
if (e.type == EventType.MouseUp)
Lightmapping.BakeAsync();
else if (!Lightmapping.isRunning)
Lightmapping.BakeAsync();
}
else
{
// TODO set some internal flag so the skybox knows it needs to be baked
}
}
if (CanAutoBake && mat != RenderSettings.skybox)
{
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUILayout.HelpBox("This material isn't the active skybox and lighting won't be automatically updated. Once you assign this material as skybox, you may need to manually generate lighting.", MessageType.Info);
}
}
private static bool CanAutoBake
{
get
{
var scene = SceneManager.GetActiveScene();
if (!scene.IsValid()) return false;
var settings = Lightmapping.GetLightingSettingsForScene(scene);
if (!settings) return true;
return (settings.bakedGI == false &&
settings.realtimeGI == false
#if !UNITY_2023_2_OR_NEWER
&& settings.autoGenerate == false
#endif
);
}
}
private static bool IsUsedByActiveScene(Material skyboxMaterial)
{
/*
// check if we have the right shader assigned as skybox in any open scene
#if UNITY_2022_2_OR_NEWER
var scenes = SceneManager.loadedSceneCount;
#else
var scenes = EditorSceneManager.loadedSceneCount;
#endif
for (int i = 0; i < scenes; ++i)
{
var scene = SceneManager.GetSceneAt(i);
// get lighting settings for scene
var settings = Lightmapping.GetLightingSettingsForScene(scene);
// var skybox = settings.lightmapper.
RenderS
}
*/
return RenderSettings.skybox == skyboxMaterial;
}
}
// This processor strips away _NEEDLE_EDITOR_ON keywords from shaders.
// For example, our blurred skybox shader has some slow code paths for editor-only blurring,
// that should never end up in a build.
internal class ShaderPreprocessor : IPreprocessShaders
{
public int callbackOrder => 3;
public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
{
var kw = new ShaderKeyword("_NEEDLE_EDITOR_ON");
for (var i = data.Count - 1; i >= 0; --i)
{
if (!data[i].shaderKeywordSet.IsEnabled(kw)) continue;
data.RemoveAt(i);
// Debug.Log("Stripped shader variant containing keyword " + kw + ", now: " + string.Join(" ", data[i].shaderKeywordSet.GetShaderKeywords().Select(x => x.name)));
}
}
}
}