├── .gitignore ├── Editor.meta ├── Editor ├── Dependencies.meta ├── Dependencies │ ├── Harmony.meta │ ├── Harmony │ │ ├── 0Harmony_GITweaks.dll │ │ ├── 0Harmony_GITweaks.dll.meta │ │ ├── LICENSE.txt │ │ └── LICENSE.txt.meta │ ├── SHTools.cs │ └── SHTools.cs.meta ├── GITweaksHarmony.cs ├── GITweaksHarmony.cs.meta ├── GITweaksLDAInspector.cs ├── GITweaksLDAInspector.cs.meta ├── GITweaksLightingDataAssetEditor.cs ├── GITweaksLightingDataAssetEditor.cs.meta ├── GITweaksMassSelectionWindow.cs ├── GITweaksMassSelectionWindow.cs.meta ├── GITweaksPostBakeOperations.cs ├── GITweaksPostBakeOperations.cs.meta ├── GITweaksSeamFixer.cs ├── GITweaksSeamFixer.cs.meta ├── GITweaksSettingsWindow.cs ├── GITweaksSettingsWindow.cs.meta ├── GITweaksTexturePacker.cs ├── GITweaksTexturePacker.cs.meta ├── GITweaksUtils.cs ├── GITweaksUtils.cs.meta ├── GITweaksViewModes.cs ├── GITweaksViewModes.cs.meta ├── Resources.meta ├── Resources │ ├── CopyFractional.compute │ ├── CopyFractional.compute.meta │ ├── RenderOver.shader │ ├── RenderOver.shader.meta │ ├── RenderUVMask.mat │ ├── RenderUVMask.mat.meta │ ├── RenderUVMask.shader │ ├── RenderUVMask.shader.meta │ ├── RenderUVMaskConservative.mat │ ├── RenderUVMaskConservative.mat.meta │ ├── RenderUVMaskConservative.shader │ └── RenderUVMaskConservative.shader.meta ├── dev.pema.gitweaks.asmdef └── dev.pema.gitweaks.asmdef.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── GITweaksSeamFix.cs ├── GITweaksSeamFix.cs.meta ├── GITweaksSeamFixVolume.cs ├── GITweaksSeamFixVolume.cs.meta ├── GITweaksSharedLOD.cs ├── GITweaksSharedLOD.cs.meta ├── dev.pema.gitweaks.runtime.asmdef └── dev.pema.gitweaks.runtime.asmdef.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .vs/ -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 89b3ae770be1ed745bf2059242fc8ab6 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Dependencies.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ab60994ed82be334399d0e845a10b119 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Dependencies/Harmony.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b866c795599cb3a45901cf8408075af9 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Dependencies/Harmony/0Harmony_GITweaks.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pema99/GITweaks/9e5caffa6fde541f1461db0deb7a5ab542794f18/Editor/Dependencies/Harmony/0Harmony_GITweaks.dll -------------------------------------------------------------------------------- /Editor/Dependencies/Harmony/0Harmony_GITweaks.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c3c5a0a6bd76c41439a6f4b578fbf7b6 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 1 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | : Any 16 | second: 17 | enabled: 0 18 | settings: 19 | Exclude Android: 1 20 | Exclude Editor: 0 21 | Exclude Linux: 1 22 | Exclude Linux64: 1 23 | Exclude LinuxUniversal: 1 24 | Exclude OSXUniversal: 1 25 | Exclude Win: 1 26 | Exclude Win64: 1 27 | - first: 28 | Android: Android 29 | second: 30 | enabled: 0 31 | settings: 32 | AndroidSharedLibraryType: Executable 33 | CPU: ARMv7 34 | - first: 35 | Any: 36 | second: 37 | enabled: 0 38 | settings: {} 39 | - first: 40 | Editor: Editor 41 | second: 42 | enabled: 1 43 | settings: 44 | CPU: AnyCPU 45 | DefaultValueInitialized: true 46 | OS: AnyOS 47 | - first: 48 | Facebook: Win 49 | second: 50 | enabled: 0 51 | settings: 52 | CPU: AnyCPU 53 | - first: 54 | Facebook: Win64 55 | second: 56 | enabled: 0 57 | settings: 58 | CPU: AnyCPU 59 | - first: 60 | Standalone: Linux 61 | second: 62 | enabled: 0 63 | settings: 64 | CPU: x86 65 | - first: 66 | Standalone: Linux64 67 | second: 68 | enabled: 0 69 | settings: 70 | CPU: AnyCPU 71 | - first: 72 | Standalone: LinuxUniversal 73 | second: 74 | enabled: 0 75 | settings: 76 | CPU: None 77 | - first: 78 | Standalone: OSXUniversal 79 | second: 80 | enabled: 0 81 | settings: 82 | CPU: AnyCPU 83 | - first: 84 | Standalone: Win 85 | second: 86 | enabled: 0 87 | settings: 88 | CPU: AnyCPU 89 | - first: 90 | Standalone: Win64 91 | second: 92 | enabled: 0 93 | settings: 94 | CPU: AnyCPU 95 | - first: 96 | Windows Store Apps: WindowsStoreApps 97 | second: 98 | enabled: 0 99 | settings: 100 | CPU: AnyCPU 101 | userData: 102 | assetBundleName: 103 | assetBundleVariant: 104 | -------------------------------------------------------------------------------- /Editor/Dependencies/Harmony/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andreas Pardeike 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Editor/Dependencies/Harmony/LICENSE.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 631bf63f8f30d2944b67a8846eee3078 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/Dependencies/SHTools.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cf9a1def22613ea4b8e640ece1811918 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GITweaksHarmony.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HarmonyLib; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Reflection.Emit; 8 | using UnityEditor; 9 | using UnityEditor.SceneManagement; 10 | using UnityEngine; 11 | using UnityEngine.Experimental.Rendering; 12 | using UnityEngine.Rendering; 13 | using UnityEngine.SceneManagement; 14 | using Object = UnityEngine.Object; 15 | 16 | namespace GITweaks 17 | { 18 | [InitializeOnLoad] 19 | internal static class GITweaksHarmony 20 | { 21 | public static Harmony Instance = new Harmony("pema.dev.gitweaks"); 22 | 23 | [HarmonyPatch(typeof(MaterialEditor), nameof(MaterialEditor.PropertiesDefaultGUI))] 24 | private class ShowGIFlagsMaterialEditorPatch 25 | { 26 | static void Postfix(MaterialEditor __instance) 27 | { 28 | if (!GITweaksSettingsWindow.IsEnabled(GITweak.LightmapFlagsDropdown)) 29 | return; 30 | 31 | try 32 | { 33 | Rect r = (Rect)AccessTools.Method(typeof(MaterialEditor), "GetControlRectForSingleLine").Invoke(__instance, new object[0]); 34 | 35 | AccessTools.Method(typeof(MaterialEditor), "BeginProperty", new System.Type[] { typeof(Rect), System.Type.GetType("UnityEngine.MaterialSerializedProperty, UnityEngine"), typeof(Object[]) }) 36 | .Invoke(null, new object[] { r, 1 << 1, __instance.targets }); 37 | 38 | EditorGUI.BeginChangeCheck(); 39 | MaterialGlobalIlluminationFlags flags = (MaterialGlobalIlluminationFlags)EditorGUI.EnumPopup( 40 | r, 41 | new GUIContent("Lightmap Flags"), 42 | (__instance.targets[0] as Material).globalIlluminationFlags, 43 | x => Mathf.IsPowerOfTwo((int)((object)x))); 44 | if (EditorGUI.EndChangeCheck()) 45 | { 46 | foreach (Material material in __instance.targets) 47 | material.globalIlluminationFlags = flags; 48 | } 49 | 50 | AccessTools.Method(typeof(MaterialEditor), "EndProperty").Invoke(null, new object[0]); 51 | } 52 | catch 53 | { 54 | // Fail silently, don't want to bother the user 55 | } 56 | } 57 | } 58 | 59 | [HarmonyPatch] 60 | private class BetterDefaultLightingSettingsPatch 61 | { 62 | [HarmonyTargetMethod] 63 | static MethodBase TargetMethod() => AccessTools.Constructor(typeof(LightingSettings)); 64 | 65 | [HarmonyPostfix] 66 | static void Postfix(LightingSettings __instance) 67 | { 68 | try 69 | { 70 | if (!GITweaksSettingsWindow.IsEnabled(GITweak.BetterLightingSettingsDefaults)) 71 | return; 72 | 73 | __instance.lightmapper = LightingSettings.Lightmapper.ProgressiveGPU; 74 | __instance.prioritizeView = false; 75 | } 76 | catch 77 | { 78 | // Fail silently, don't want to bother the user 79 | } 80 | } 81 | } 82 | 83 | [HarmonyPatch] 84 | private class CloneSkyboxMaterialPatch 85 | { 86 | [HarmonyTargetMethod] 87 | static MethodBase TargetMethod() => AccessTools.Method(System.Type.GetType("UnityEditor.LightingEditor, UnityEditor"), "DrawGUI"); 88 | 89 | [HarmonyPostfix] 90 | static void Prefix() 91 | { 92 | if (!GITweaksSettingsWindow.IsEnabled(GITweak.NewSkyboxButton)) 93 | return; 94 | 95 | try 96 | { 97 | EditorGUILayout.BeginHorizontal(); 98 | 99 | if (GUILayout.Button("New skybox material")) 100 | { 101 | Material mat = new Material(Shader.Find("Skybox/Procedural")); 102 | mat.name = "New Skybox Material"; 103 | RenderSettings.skybox = mat; 104 | ProjectWindowUtil.CreateAsset(mat, (mat.name + ".mat")); 105 | } 106 | 107 | using (new EditorGUI.DisabledScope(RenderSettings.skybox == null)) 108 | if (GUILayout.Button("Clone skybox material")) 109 | { 110 | Material mat = new Material(RenderSettings.skybox); 111 | mat.name = string.IsNullOrEmpty(RenderSettings.skybox.name) ? "New Lighting Settings" : RenderSettings.skybox.name; 112 | RenderSettings.skybox = mat; 113 | ProjectWindowUtil.CreateAsset(mat, (mat.name + ".mat")); 114 | } 115 | 116 | EditorGUILayout.EndHorizontal(); 117 | } 118 | catch 119 | { 120 | // Fail silently, don't want to bother the user 121 | } 122 | } 123 | } 124 | 125 | [HarmonyPatch] 126 | private class LightmapPreviewDropdownWindow 127 | { 128 | [HarmonyTargetMethod] 129 | static MethodBase TargetMethod() => AccessTools.Method(System.Type.GetType("UnityEditor.LightmapPreviewWindow, UnityEditor"), "DrawPreviewSettings"); 130 | 131 | [HarmonyPostfix] 132 | static void Prefix(object __instance) 133 | { 134 | if (!GITweaksSettingsWindow.IsEnabled(GITweak.LightmapPreviewDropdown)) 135 | return; 136 | 137 | try 138 | { 139 | System.Type thisType = __instance.GetType(); 140 | int id = (int)AccessTools.Field(thisType, "m_InstanceID").GetValue(__instance); 141 | if (id != -1) 142 | return; 143 | 144 | var lightmapIndexField = AccessTools.Field(thisType, "m_LightmapIndex"); 145 | 146 | int lmCount = LightmapSettings.lightmaps.Length; 147 | if (lmCount == 0) 148 | return; 149 | 150 | var lms = Enumerable.Range(0, lmCount); 151 | EditorGUI.BeginChangeCheck(); 152 | int newLmIndex = EditorGUILayout.IntPopup((int)lightmapIndexField.GetValue(__instance), lms.Select(x => $"Lightmap {x}").ToArray(), lms.ToArray()); 153 | if (EditorGUI.EndChangeCheck()) 154 | { 155 | lightmapIndexField.SetValue(__instance, newLmIndex); 156 | } 157 | 158 | } 159 | catch 160 | { 161 | // Fail silently, don't want to bother the user 162 | } 163 | } 164 | } 165 | 166 | [HarmonyPatch] 167 | private class ClickableUVChartPatch 168 | { 169 | [HarmonyTargetMethod] 170 | static MethodBase TargetMethod() => AccessTools.Method(System.Type.GetType("UnityEditor.LightmapPreviewWindow, UnityEditor"), "DrawPreview"); 171 | 172 | private static Rect ResizeRectToFit(Rect rect, Rect to) 173 | { 174 | float widthScale = to.width / rect.width; 175 | float heightScale = to.height / rect.height; 176 | float scale = Mathf.Min(widthScale, heightScale); 177 | 178 | float width = (int)Mathf.Round((rect.width * scale)); 179 | float height = (int)Mathf.Round((rect.height * scale)); 180 | 181 | return new Rect(rect.x, rect.y, width, height); 182 | } 183 | 184 | private static Rect ScaleRectByZoomableArea(Rect rect, Rect zoomableRect, Rect zoomableShown) 185 | { 186 | float x = -(zoomableShown.x / zoomableShown.width) * (rect.x + zoomableRect.width); 187 | float y = ((zoomableShown.y - (1f - zoomableShown.height)) / zoomableShown.height) * zoomableRect.height; 188 | 189 | float width = rect.width / zoomableShown.width; 190 | float height = rect.height / zoomableShown.height; 191 | 192 | return new Rect(rect.x + x, rect.y + y, width, height); 193 | } 194 | 195 | [HarmonyPostfix] 196 | static void Postfix(object __instance, Rect r) 197 | { 198 | if (!GITweaksSettingsWindow.IsEnabled(GITweak.ClickableLightmapCharts)) 199 | return; 200 | 201 | try 202 | { 203 | System.Type thisType = __instance.GetType(); 204 | object visTex = AccessTools.Field(thisType, "m_CachedTexture").GetValue(__instance); 205 | System.Type visTexType = visTex.GetType(); 206 | Texture2D texture = (Texture2D)AccessTools.Field(visTexType, "texture").GetValue(visTex); 207 | 208 | object zoom = AccessTools.Field(thisType, "m_ZoomablePreview").GetValue(__instance); 209 | System.Type zoomType = zoom.GetType(); 210 | Rect drawableArea = (Rect)AccessTools.Property(zoomType, "drawRect").GetValue(zoom); 211 | Rect shownArea = (Rect)AccessTools.Property(zoomType, "shownArea").GetValue(zoom); 212 | Rect zoomRect = (Rect)AccessTools.Property(zoomType, "rect").GetValue(zoom); 213 | 214 | Rect textureRect = new Rect(r.x, r.y, texture.width, texture.height); 215 | textureRect = ResizeRectToFit(textureRect, drawableArea); 216 | textureRect = ScaleRectByZoomableArea(textureRect, zoomRect, shownArea); 217 | textureRect.x += 5; 218 | textureRect.width -= 5 * 2; 219 | textureRect.height -= 2; 220 | 221 | Event e = Event.current; 222 | if (e.type == EventType.MouseDown && e.button == 0 && textureRect.Contains(e.mousePosition)) 223 | { 224 | Vector2 uv = (e.mousePosition - textureRect.position) / textureRect.size; 225 | uv.y = 1.0f - uv.y; 226 | 227 | // Bakery might create overlapping UVSTs, so we need to find the smallest overlapping one. 228 | GameObject[] cachedTextureObjects = (GameObject[])AccessTools.Field(thisType, "m_CachedTextureObjects").GetValue(__instance); 229 | var candidates = new List<(GameObject go, float size)>(); 230 | foreach (GameObject cachedTextureObject in cachedTextureObjects) 231 | { 232 | MeshRenderer mr = cachedTextureObject.GetComponent(); 233 | Vector4 lightmapScaleOffset; 234 | if (mr != null) 235 | { 236 | lightmapScaleOffset = mr.lightmapScaleOffset; 237 | } 238 | else 239 | { 240 | Terrain tr = cachedTextureObject.GetComponent(); 241 | if (tr == null) 242 | continue; 243 | lightmapScaleOffset = tr.lightmapScaleOffset; 244 | } 245 | 246 | // Get raw ST rect (with padding) 247 | Rect stRect = new Rect(lightmapScaleOffset.z, lightmapScaleOffset.w, lightmapScaleOffset.x, lightmapScaleOffset.y); 248 | 249 | // If we are in the raw rect, we _might_ be in the pixel rect 250 | if (stRect.Contains(uv)) 251 | { 252 | // Scale to uv bounds 253 | if (mr != null) 254 | { 255 | stRect = GITweaksUtils.STRectToPixelRect(mr, stRect); 256 | } 257 | 258 | if (stRect.Contains(uv)) 259 | { 260 | candidates.Add((cachedTextureObject, stRect.width * stRect.height)); 261 | } 262 | } 263 | } 264 | 265 | // Find the best candidate 266 | GameObject bestChart = null; 267 | if (candidates.Count == 1) 268 | { 269 | bestChart = candidates[0].go; 270 | } 271 | else if (candidates.Count > 1) 272 | { 273 | int candidateIndex = -1; 274 | 275 | try 276 | { 277 | // Render a mask of UV charts 278 | Material mat = Resources.Load(SystemInfo.supportsConservativeRaster ? "RenderUVMaskConservative" : "RenderUVMask"); 279 | RenderTexture rt = RenderTexture.GetTemporary(texture.width, texture.height, 0, GraphicsFormat.R32G32B32A32_SFloat); 280 | var prevRT = RenderTexture.active; 281 | RenderTexture.active = rt; 282 | GL.Clear(true, true, Color.black); 283 | for (int i = 0; i < candidates.Count; i++) 284 | { 285 | var go = candidates[i].go; 286 | MeshRenderer mr = go.GetComponent(); 287 | MeshFilter mf = go.GetComponent(); 288 | var mesh = new Mesh(); 289 | if (mr == null || mf == null) 290 | continue; 291 | 292 | if (!GITweaksUtils.GetMeshAndUVChannel(mr, out var uvs, out var uvIndex)) 293 | continue; 294 | mesh.vertices = mf.sharedMesh.vertices; 295 | mesh.uv = uvs; 296 | mesh.triangles = mf.sharedMesh.triangles; 297 | 298 | mat.SetInteger("_CandidateIndex", i + 1); // 1 to avoid writing 0 299 | mat.SetVector("_CandidateST", mr.lightmapScaleOffset); 300 | mat.SetPass(0); 301 | Graphics.DrawMeshNow(mesh, Matrix4x4.identity); 302 | 303 | Object.DestroyImmediate(mesh); 304 | } 305 | Texture2D readback = new Texture2D(rt.width, rt.height, TextureFormat.RGBAFloat, false); 306 | readback.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); 307 | readback.Apply(); 308 | RenderTexture.active = prevRT; 309 | RenderTexture.ReleaseTemporary(rt); 310 | candidateIndex = Mathf.RoundToInt(readback.GetPixel((int)(uv.x * readback.width), (int)(uv.y * readback.height)).r - 1); 311 | Object.DestroyImmediate(readback); 312 | } 313 | catch 314 | { 315 | // Ignore, fallback to cheap method 316 | } 317 | 318 | if (candidateIndex >= 0) 319 | { 320 | bestChart = candidates[candidateIndex].go; 321 | } 322 | else 323 | { 324 | // Fall back to picking the smallest bounding box 325 | bestChart = candidates.OrderBy(x => x.size).FirstOrDefault().go; 326 | } 327 | } 328 | 329 | if (bestChart != null) 330 | { 331 | Selection.activeGameObject = bestChart; 332 | EditorGUIUtility.PingObject(bestChart); 333 | } 334 | } 335 | } 336 | catch 337 | { 338 | // Fail silently, don't want to bother the user 339 | } 340 | } 341 | } 342 | 343 | [HarmonyPatch] 344 | private class ConvertToProbeLitPatch 345 | { 346 | static System.Type MainType = System.Type.GetType("UnityEditor.RendererLightingSettings, UnityEditor"); 347 | 348 | [HarmonyTargetMethod] 349 | static MethodBase TargetMethod() => AccessTools.Method(MainType, "RenderSettings"); 350 | 351 | static MethodInfo InjectedMethodInfo = SymbolExtensions.GetMethodInfo((object self) => InjectedMethod(self)); 352 | 353 | [HarmonyTranspiler] 354 | static IEnumerable Transpiler(IEnumerable instructions) 355 | { 356 | bool first = true; 357 | foreach (var instruction in instructions) 358 | { 359 | yield return instruction; 360 | 361 | if (instruction.operand is MethodInfo info && info.Name == "IntPopup") 362 | { 363 | if (first) 364 | { 365 | yield return new CodeInstruction(OpCodes.Ldarg_0); 366 | yield return new CodeInstruction(OpCodes.Call, InjectedMethodInfo); 367 | 368 | first = false; 369 | } 370 | } 371 | } 372 | } 373 | 374 | static void InjectedMethod(object self) 375 | { 376 | if (GITweaksUtils.IsCurrentSceneBakedWithBakery()) 377 | return; 378 | 379 | if (!GITweaksSettingsWindow.IsEnabled(GITweak.LightmappedToProbeLit)) 380 | return; 381 | 382 | try 383 | { 384 | SerializedObject obj = (SerializedObject)AccessTools.Field(MainType, "m_SerializedObject").GetValue(self); 385 | if (obj.targetObject is MeshRenderer mr) 386 | { 387 | if (mr.receiveGI == ReceiveGI.LightProbes && mr.lightmapIndex < 65534) 388 | { 389 | var rect = EditorGUILayout.GetControlRect(); 390 | rect.x += 14; 391 | rect.width -= 14; 392 | if (GUI.Button(rect, "Convert lightmapped to probe-lit")) 393 | { 394 | var lda = GITweaksLightingDataAssetEditor.GetLDAForScene(mr.gameObject.scene.path); 395 | GITweaksLightingDataAssetEditor.MakeRendererProbeLit(lda, mr); 396 | GITweaksUtils.RefreshLDA(); 397 | } 398 | } 399 | } 400 | } 401 | catch 402 | { 403 | // Fail silently, don't want to bother the user 404 | } 405 | } 406 | } 407 | 408 | // TODO: Move this 409 | static void NewSceneCreated(Scene scene, NewSceneSetup setup, NewSceneMode mode) 410 | { 411 | // Tweak: Embedded lighting settings 412 | if (GITweaksSettingsWindow.IsEnabled(GITweak.AutomaticEmbeddedLightingSettings)) 413 | { 414 | LightingSettings settings = new LightingSettings() { name = "Lighting Settings (Embedded)" }; 415 | Lightmapping.SetLightingSettingsForScene(scene, settings); 416 | } 417 | } 418 | 419 | static GITweaksHarmony() 420 | { 421 | EditorSceneManager.newSceneCreated -= NewSceneCreated; 422 | EditorSceneManager.newSceneCreated += NewSceneCreated; 423 | 424 | EditorApplication.update -= WaitThenPatch; 425 | EditorApplication.update += WaitThenPatch; 426 | } 427 | 428 | // Wait for a few frames before patching, to let static initializers run. 429 | private static int WaitFrames = 0; 430 | private static void WaitThenPatch() 431 | { 432 | WaitFrames++; 433 | if (WaitFrames > 2) 434 | { 435 | EditorApplication.update -= WaitThenPatch; 436 | Instance.PatchAll(); 437 | } 438 | } 439 | } 440 | } -------------------------------------------------------------------------------- /Editor/GITweaksHarmony.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5cde5942ed41917498ce20124c455168 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GITweaksLDAInspector.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace GITweaks 7 | { 8 | [CustomEditor(typeof(LightingDataAsset))] 9 | public class GITweaksLDAInspector : Editor 10 | { 11 | System.Reflection.PropertyInfo inspectorModeSelf = typeof(Editor).GetProperty("inspectorMode", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); 12 | System.Reflection.PropertyInfo inspectorModeObject = typeof(SerializedObject).GetProperty("inspectorMode", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); 13 | 14 | public override void OnInspectorGUI() 15 | { 16 | if (GITweaksSettingsWindow.IsEnabled(GITweak.BetterLDAInspector)) 17 | { 18 | inspectorModeSelf.SetValue(this, InspectorMode.DebugInternal); 19 | inspectorModeObject.SetValue(serializedObject, InspectorMode.DebugInternal); 20 | } 21 | else 22 | { 23 | inspectorModeSelf.SetValue(this, InspectorMode.Normal); 24 | inspectorModeObject.SetValue(serializedObject, InspectorMode.Normal); 25 | } 26 | base.OnInspectorGUI(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Editor/GITweaksLDAInspector.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3b5971ec86008ab429d7b3f47be7c46e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GITweaksLightingDataAssetEditor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using UnityEngine.SceneManagement; 7 | 8 | namespace GITweaks 9 | { 10 | [InitializeOnLoad] 11 | public static class GITweaksLightingDataAssetEditor 12 | { 13 | private static System.Reflection.PropertyInfo inspectorModeObject = 14 | typeof(SerializedObject).GetProperty("inspectorMode", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); 15 | 16 | struct SerializedObjectID : System.IEquatable 17 | { 18 | public long MainLFID; // If prefab, LFID in MeshRenderer in prefab stage, else LFID of object 19 | public long PrefabLFID; // If prefab, LFID of "Prefab instance" object, points to prefab 20 | 21 | public SerializedObjectID(long main, long prefab) 22 | { 23 | MainLFID = main; 24 | PrefabLFID = prefab; 25 | } 26 | 27 | public bool Equals(SerializedObjectID other) => other.MainLFID == MainLFID && other.PrefabLFID == PrefabLFID; 28 | public override bool Equals(object obj) => obj is SerializedObjectID id && Equals(id); 29 | public static bool operator ==(SerializedObjectID a, SerializedObjectID b) => a.Equals(b); 30 | public static bool operator !=(SerializedObjectID a, SerializedObjectID b) => !(a == b); 31 | public override int GetHashCode() => System.HashCode.Combine(MainLFID, PrefabLFID); 32 | } 33 | 34 | private static SerializedObjectID ObjectToSOI(Object obj) 35 | { 36 | using var mainSO = new SerializedObject(obj); 37 | inspectorModeObject.SetValue(mainSO, InspectorMode.DebugInternal); 38 | long lfid = mainSO.FindProperty("m_LocalIdentfierInFile").longValue; 39 | 40 | var prefabInstance = mainSO.FindProperty("m_PrefabInstance"); 41 | if (prefabInstance.objectReferenceValue != null) 42 | { 43 | using var prefabInstanceSO = new SerializedObject(prefabInstance.objectReferenceValue); 44 | inspectorModeObject.SetValue(prefabInstanceSO, InspectorMode.DebugInternal); 45 | 46 | using var correspondingSO = new SerializedObject(mainSO.FindProperty("m_CorrespondingSourceObject").objectReferenceValue); 47 | inspectorModeObject.SetValue(correspondingSO, InspectorMode.DebugInternal); 48 | 49 | long sourceLFID = correspondingSO.FindProperty("m_LocalIdentfierInFile").longValue; 50 | long prefabLFID = prefabInstanceSO.FindProperty("m_LocalIdentfierInFile").longValue; 51 | 52 | return new SerializedObjectID(sourceLFID, prefabLFID); 53 | } 54 | else 55 | { 56 | return new SerializedObjectID(lfid, 0); 57 | } 58 | } 59 | 60 | public static void CopyAtlasSettingsToRenderers(LightingDataAsset lda, MeshRenderer from, MeshRenderer[] to) 61 | { 62 | if (!GITweaksUtils.IsLightmapped(from) && !GITweaksUtils.IsRealtimeLightmapped(from)) 63 | return; 64 | if (lda == null) 65 | return; 66 | 67 | using SerializedObject o = new SerializedObject(lda); 68 | inspectorModeObject.SetValue(o, InspectorMode.DebugInternal); 69 | 70 | // Get LOD0 SOI 71 | var mainSOI = ObjectToSOI(from); 72 | 73 | // Find LOD0 74 | int lm0Index = -1; 75 | using var lmIds = o.FindProperty("m_LightmappedRendererDataIDs"); 76 | for (int i = 0; i < lmIds.arraySize; i++) 77 | { 78 | using var elem = lmIds.GetArrayElementAtIndex(i); 79 | elem.Next(true); 80 | long main = elem.longValue; 81 | elem.Next(false); 82 | long prefab = elem.longValue; 83 | 84 | if (mainSOI == new SerializedObjectID(main, prefab)) 85 | { 86 | lm0Index = i; 87 | break; 88 | } 89 | } 90 | 91 | if (lm0Index < 0) 92 | return; 93 | 94 | // Append LODs 95 | var lmVals = o.FindProperty("m_LightmappedRendererData"); 96 | Debug.Assert(lmIds.arraySize == lmVals.arraySize); 97 | int baseOffset = lmIds.arraySize; 98 | lmIds.arraySize += to.Length; 99 | lmVals.arraySize += to.Length; 100 | for (int i = 0; i < to.Length; i++) 101 | { 102 | MeshRenderer newMr = to[i]; 103 | int lmIndex = baseOffset + i; 104 | 105 | // Set SOI 106 | var newSOI = ObjectToSOI(newMr); 107 | using var soiData = lmIds.GetArrayElementAtIndex(lmIndex); 108 | soiData.Next(true); 109 | soiData.longValue = newSOI.MainLFID; 110 | soiData.Next(false); 111 | soiData.longValue = newSOI.PrefabLFID; 112 | 113 | // Set atlas data 114 | using var fromAtlasData = lmVals.GetArrayElementAtIndex(lm0Index); 115 | using var fromAtlasDataEnd = fromAtlasData.Copy(); 116 | fromAtlasDataEnd.Next(false); 117 | using var toAtlasData = lmVals.GetArrayElementAtIndex(lmIndex); 118 | toAtlasData.Next(true); 119 | fromAtlasData.Next(true); 120 | while (!SerializedProperty.EqualContents(fromAtlasData, fromAtlasDataEnd)) 121 | { 122 | toAtlasData.boxedValue = fromAtlasData.boxedValue; 123 | toAtlasData.Next(false); 124 | fromAtlasData.Next(false); 125 | } 126 | } 127 | 128 | o.ApplyModifiedProperties(); 129 | } 130 | 131 | public static Dictionary GetAtlassing(LightingDataAsset lda) 132 | { 133 | using SerializedObject o = new SerializedObject(lda); 134 | inspectorModeObject.SetValue(o, InspectorMode.DebugInternal); 135 | 136 | // SOI -> Index in m_LightmappedRendererData 137 | Dictionary seralizedIdToIndex = new Dictionary(); 138 | var lmIds = o.FindProperty("m_LightmappedRendererDataIDs"); 139 | for (int i = 0; i < lmIds.arraySize; i++) 140 | { 141 | var elem = lmIds.GetArrayElementAtIndex(i); 142 | elem.Next(true); 143 | long main = elem.longValue; 144 | elem.Next(false); 145 | long prefab = elem.longValue; 146 | seralizedIdToIndex.Add(new SerializedObjectID(main, prefab), i); 147 | } 148 | 149 | var scene = o.FindProperty("m_Scene").objectReferenceValue as SceneAsset; 150 | string scenePath = scene == null ? null : AssetDatabase.GetAssetPath(scene); 151 | 152 | // Renderer -> m_LightmappedRendererData 153 | Component[] mrToAtlasData = new Component[lmIds.arraySize]; 154 | var allMr = Object.FindObjectsByType(FindObjectsSortMode.None).Select(x => x as Component); 155 | var allTerrain = Object.FindObjectsByType(FindObjectsSortMode.None).Select(x => x as Component); 156 | var allRenderer = allMr.Concat(allTerrain).Where(x => x.gameObject.scene.path == scenePath); 157 | foreach (var mr in allRenderer) 158 | { 159 | using var mainSO = new SerializedObject(mr); 160 | inspectorModeObject.SetValue(mainSO, InspectorMode.DebugInternal); 161 | long lfid = mainSO.FindProperty("m_LocalIdentfierInFile").longValue; 162 | 163 | SerializedObjectID id; 164 | 165 | var prefabInstance = mainSO.FindProperty("m_PrefabInstance"); 166 | if (prefabInstance.objectReferenceValue != null) 167 | { 168 | using var prefabInstanceSO = new SerializedObject(prefabInstance.objectReferenceValue); 169 | inspectorModeObject.SetValue(prefabInstanceSO, InspectorMode.DebugInternal); 170 | 171 | using var correspondingSO = new SerializedObject(mainSO.FindProperty("m_CorrespondingSourceObject").objectReferenceValue); 172 | inspectorModeObject.SetValue(correspondingSO, InspectorMode.DebugInternal); 173 | 174 | long sourceLFID = correspondingSO.FindProperty("m_LocalIdentfierInFile").longValue; 175 | long prefabLFID = prefabInstanceSO.FindProperty("m_LocalIdentfierInFile").longValue; 176 | 177 | id = new SerializedObjectID(sourceLFID, prefabLFID); 178 | } 179 | else 180 | { 181 | id = new SerializedObjectID(lfid, 0); 182 | } 183 | 184 | if (seralizedIdToIndex.TryGetValue(id, out int idx)) 185 | { 186 | mrToAtlasData[idx] = mr; 187 | } 188 | } 189 | 190 | var result = new Dictionary(); 191 | 192 | var lmVals = o.FindProperty("m_LightmappedRendererData"); 193 | for (int i = 0; i < mrToAtlasData.Length; i++) 194 | { 195 | var mr = mrToAtlasData[i]; 196 | 197 | var atlasData = lmVals.GetArrayElementAtIndex(i); 198 | var lightmapST = atlasData.FindPropertyRelative("lightmapST"); 199 | var lightmapIndex = atlasData.FindPropertyRelative("lightmapIndex"); 200 | 201 | result[mr] = (lightmapIndex.intValue, lightmapST.vector4Value); 202 | } 203 | 204 | return result; 205 | } 206 | 207 | public static void UpdateAtlassing(LightingDataAsset lda, Dictionary atlassing) 208 | { 209 | using SerializedObject o = new SerializedObject(lda); 210 | inspectorModeObject.SetValue(o, InspectorMode.DebugInternal); 211 | 212 | var soiMap = atlassing.Select(x => (ObjectToSOI(x.Key), x.Value)).ToDictionary(x => x.Item1, x => x.Value); 213 | 214 | var lmIds = o.FindProperty("m_LightmappedRendererDataIDs"); 215 | var lmVals = o.FindProperty("m_LightmappedRendererData"); 216 | 217 | for (int i = 0; i < lmVals.arraySize; i++) 218 | { 219 | var soi = lmIds.GetArrayElementAtIndex(i); 220 | soi.Next(true); 221 | long main = soi.longValue; 222 | soi.Next(false); 223 | long prefab = soi.longValue; 224 | if (soiMap.TryGetValue(new SerializedObjectID(main, prefab), out var newAtlasData)) 225 | { 226 | var atlasData = lmVals.GetArrayElementAtIndex(i); 227 | var lightmapST = atlasData.FindPropertyRelative("lightmapST"); 228 | var lightmapIndex = atlasData.FindPropertyRelative("lightmapIndex"); 229 | lightmapST.vector4Value = newAtlasData.lightmapST; 230 | lightmapIndex.intValue = newAtlasData.lightmapIndex; 231 | } 232 | } 233 | 234 | o.ApplyModifiedProperties(); 235 | } 236 | 237 | public static void UpdateLightmaps(LightingDataAsset lda, LightmapData[] lightmapDatas) 238 | { 239 | using SerializedObject o = new SerializedObject(lda); 240 | inspectorModeObject.SetValue(o, InspectorMode.DebugInternal); 241 | 242 | var lms = o.FindProperty("m_Lightmaps"); 243 | lms.arraySize = lightmapDatas.Length; 244 | for (int i = 0; i < lightmapDatas.Length; i++) 245 | { 246 | var lm = lms.GetArrayElementAtIndex(i); 247 | 248 | if (lightmapDatas[i].lightmapColor != null) 249 | lm.FindPropertyRelative("m_Lightmap").objectReferenceInstanceIDValue = lightmapDatas[i].lightmapColor.GetInstanceID(); 250 | if (lightmapDatas[i].lightmapDir != null) 251 | lm.FindPropertyRelative("m_DirLightmap").objectReferenceInstanceIDValue = lightmapDatas[i].lightmapDir.GetInstanceID(); 252 | if (lightmapDatas[i].shadowMask != null) 253 | lm.FindPropertyRelative("m_ShadowMask").objectReferenceInstanceIDValue = lightmapDatas[i].shadowMask.GetInstanceID(); 254 | } 255 | 256 | o.ApplyModifiedProperties(); 257 | } 258 | 259 | public static LightmapData[] GetLightmaps(LightingDataAsset lda) 260 | { 261 | using SerializedObject o = new SerializedObject(lda); 262 | inspectorModeObject.SetValue(o, InspectorMode.DebugInternal); 263 | 264 | var lms = o.FindProperty("m_Lightmaps"); 265 | var res = new LightmapData[lms.arraySize]; 266 | for (int i = 0; i < lms.arraySize; i++) 267 | { 268 | var lm = lms.GetArrayElementAtIndex(i); 269 | 270 | var lmd = new LightmapData(); 271 | lmd.lightmapColor = lm.FindPropertyRelative("m_Lightmap").objectReferenceValue as Texture2D; 272 | lmd.lightmapDir = lm.FindPropertyRelative("m_DirLightmap").objectReferenceValue as Texture2D; 273 | lmd.shadowMask = lm.FindPropertyRelative("m_ShadowMask").objectReferenceValue as Texture2D; 274 | res[i] = lmd; 275 | } 276 | 277 | return res; 278 | } 279 | 280 | public static void MakeRendererProbeLit(LightingDataAsset lda, MeshRenderer mr) 281 | { 282 | if (lda == null) 283 | return; 284 | 285 | using SerializedObject o = new SerializedObject(lda); 286 | inspectorModeObject.SetValue(o, InspectorMode.DebugInternal); 287 | 288 | var mainSOI = ObjectToSOI(mr); 289 | 290 | // Find mr 291 | int mrIndex = -1; 292 | using var lmIds = o.FindProperty("m_LightmappedRendererDataIDs"); 293 | for (int i = 0; i < lmIds.arraySize; i++) 294 | { 295 | using var elem = lmIds.GetArrayElementAtIndex(i); 296 | elem.Next(true); 297 | long main = elem.longValue; 298 | elem.Next(false); 299 | long prefab = elem.longValue; 300 | 301 | if (mainSOI == new SerializedObjectID(main, prefab)) 302 | { 303 | mrIndex = i; 304 | break; 305 | } 306 | } 307 | 308 | if (mrIndex < 0) 309 | return; 310 | 311 | var lmVals = o.FindProperty("m_LightmappedRendererData"); 312 | var atlasData = lmVals.GetArrayElementAtIndex(mrIndex); 313 | atlasData.FindPropertyRelative("lightmapIndex").intValue = 65534; 314 | 315 | o.ApplyModifiedProperties(); 316 | } 317 | 318 | public static LightingDataAsset GetLDAForScene(string scenePath) 319 | { 320 | var ldas = Resources.FindObjectsOfTypeAll(); 321 | return ldas.FirstOrDefault(x => 322 | { 323 | var so = new SerializedObject(x); 324 | var sceneRef = so.FindProperty("m_Scene").objectReferenceValue as SceneAsset; 325 | if (AssetDatabase.GetAssetPath(sceneRef) == scenePath) 326 | return true; 327 | return false; 328 | }); 329 | } 330 | } 331 | 332 | 333 | } -------------------------------------------------------------------------------- /Editor/GITweaksLightingDataAssetEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cf04642867ef4df49a3e4b33002af2ed 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GITweaksMassSelectionWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using UnityEditor; 6 | using UnityEditor.SceneManagement; 7 | using UnityEngine; 8 | using UnityEngine.Rendering; 9 | using UnityEngine.SceneManagement; 10 | using UnityEngine.UIElements; 11 | 12 | namespace GITweaks 13 | { 14 | public class GITweaksMassSelectionWindow : EditorWindow 15 | { 16 | [MenuItem("Tools/GI Tweaks/Bulk Renderer Selection")] 17 | public static void ShowExample() 18 | { 19 | GITweaksMassSelectionWindow wnd = GetWindow(); 20 | wnd.titleContent = new GUIContent("Bulk Renderer Selection"); 21 | } 22 | 23 | bool filterActiveObjects = false; 24 | bool filterGIContributors = true; 25 | bool filterReceiveGI = false; 26 | ReceiveGI receiveGIFilter = ReceiveGI.LightProbes; 27 | bool filterLightProbesUsage = false; 28 | LightProbeUsage lightProbeUsageFilter = LightProbeUsage.BlendProbes; 29 | bool filterReflectionProbesUsage = false; 30 | ReflectionProbeUsage reflectionProbeUsageFilter = ReflectionProbeUsage.BlendProbes; 31 | bool filterOnlyCurrentScene = false; 32 | bool filterOnlyCurrentSelection = false; 33 | 34 | public void OnGUI() 35 | { 36 | { 37 | filterActiveObjects = GUILayout.Toggle(filterActiveObjects, "Select only active objects"); 38 | filterGIContributors = GUILayout.Toggle(filterGIContributors, "Select only GI contributors"); 39 | filterReceiveGI = GUILayout.Toggle(filterReceiveGI, "Filter by receive GI mode"); 40 | using (new EditorGUI.DisabledScope(!filterReceiveGI)) 41 | { 42 | EditorGUI.indentLevel++; 43 | receiveGIFilter = (ReceiveGI)EditorGUILayout.EnumPopup("Receive GI mode", receiveGIFilter); 44 | EditorGUI.indentLevel--; 45 | } 46 | filterLightProbesUsage = GUILayout.Toggle(filterLightProbesUsage, "Filter by light probe usage"); 47 | using (new EditorGUI.DisabledScope(!filterLightProbesUsage)) 48 | { 49 | EditorGUI.indentLevel++; 50 | lightProbeUsageFilter = (LightProbeUsage)EditorGUILayout.EnumPopup("Light Probe Usage", lightProbeUsageFilter); 51 | EditorGUI.indentLevel--; 52 | } 53 | filterReflectionProbesUsage = GUILayout.Toggle(filterReflectionProbesUsage, "Filter by reflection probe usage"); 54 | using (new EditorGUI.DisabledScope(!filterReflectionProbesUsage)) 55 | { 56 | EditorGUI.indentLevel++; 57 | reflectionProbeUsageFilter = (ReflectionProbeUsage)EditorGUILayout.EnumPopup("Reflection Probe Usage", reflectionProbeUsageFilter); 58 | EditorGUI.indentLevel--; 59 | } 60 | filterOnlyCurrentScene = GUILayout.Toggle(filterOnlyCurrentScene, "Limit to active scene"); 61 | filterOnlyCurrentSelection = GUILayout.Toggle(filterOnlyCurrentSelection, "Limit to current selection"); 62 | 63 | if (GUILayout.Button("Select renderers")) 64 | { 65 | var source = filterOnlyCurrentSelection 66 | ? Selection.gameObjects.Select(x => x.GetComponent()).Where(x => x != null) 67 | : FindObjectsByType(filterActiveObjects ? FindObjectsInactive.Exclude : FindObjectsInactive.Include, FindObjectsSortMode.None); 68 | 69 | if (filterOnlyCurrentScene) 70 | source = source.Where(x => x.gameObject.scene == SceneManager.GetActiveScene()); 71 | 72 | if (filterActiveObjects) 73 | source = source.Where(x => x.enabled && x.gameObject.activeInHierarchy); 74 | 75 | if (filterGIContributors) 76 | source = source.Where(x => GameObjectUtility.AreStaticEditorFlagsSet(x.gameObject, StaticEditorFlags.ContributeGI)); 77 | 78 | if (filterReceiveGI) 79 | source = source.Where(x => x.receiveGI == receiveGIFilter || 80 | (receiveGIFilter == ReceiveGI.LightProbes && !GameObjectUtility.AreStaticEditorFlagsSet(x.gameObject, StaticEditorFlags.ContributeGI))); 81 | 82 | if (filterLightProbesUsage) 83 | source = source.Where(x => x.lightProbeUsage == lightProbeUsageFilter); 84 | 85 | if (filterReflectionProbesUsage) 86 | source = source.Where(x => x.reflectionProbeUsage == reflectionProbeUsageFilter); 87 | 88 | Selection.objects = source.Select(x => x.gameObject).ToArray(); 89 | } 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /Editor/GITweaksMassSelectionWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8f8c1e4136bff4445a696bf2fac3f31f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GITweaksPostBakeOperations.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using UnityEngine.Experimental.Rendering; 8 | using UnityEngine.SceneManagement; 9 | using Atlassing = System.Collections.Generic.Dictionary; 10 | 11 | namespace GITweaks 12 | { 13 | [InitializeOnLoad] 14 | public static class GITweaksPostBakeOperations 15 | { 16 | static GITweaksPostBakeOperations() 17 | { 18 | Lightmapping.bakeCompleted -= BakeFinished; 19 | Lightmapping.bakeCompleted += BakeFinished; 20 | 21 | #if BAKERY_INCLUDED 22 | var bakeryType = System.Type.GetType("ftRenderLightmap, BakeryEditorAssembly"); 23 | if (bakeryType == null) 24 | return; 25 | var evt = bakeryType.GetEvent("OnFinishedFullRender", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); 26 | if (evt == null) 27 | return; 28 | evt.RemoveEventHandler(null, (System.EventHandler)BakeryBakeFinished); 29 | evt.AddEventHandler(null, (System.EventHandler)BakeryBakeFinished); 30 | #endif 31 | } 32 | 33 | #if BAKERY_INCLUDED 34 | private static void BakeryBakeFinished(object sender, System.EventArgs e) => BakeFinished(); 35 | #endif 36 | 37 | private static void BakeFinished() 38 | { 39 | if (!GITweaksUtils.IsCurrentSceneBakedWithBakery()) 40 | { 41 | if (GITweaksSettingsWindow.IsEnabled(GITweak.OptimizeLightmapSizes)) 42 | RepackAtlasses(); 43 | 44 | if (GITweaksSettingsWindow.IsEnabled(GITweak.SharedLODGroupComponents)) 45 | RearrangeLODs(); 46 | 47 | GITweaksUtils.RefreshLDA(); 48 | } 49 | 50 | if (GITweaksSettingsWindow.IsEnabled(GITweak.SeamFixes)) 51 | { 52 | var seamFixes = Object.FindObjectsByType(FindObjectsSortMode.None); 53 | var seamFixVolumes = Object.FindObjectsByType(FindObjectsSortMode.None); 54 | foreach (var seamFix in seamFixes) 55 | { 56 | if (seamFix.RunAfterBaking) 57 | GITweaksSeamFixer.FixSeams(seamFix, true); 58 | } 59 | foreach (var seamFixVolume in seamFixVolumes) 60 | { 61 | if (seamFixVolume.RunAfterBaking) 62 | GITweaksSeamFixer.FixSeams(seamFixVolume, true); 63 | } 64 | } 65 | } 66 | 67 | private static void RearrangeLODs() 68 | { 69 | var sharedLODs = Object.FindObjectsByType(FindObjectsSortMode.None); 70 | foreach (var sharedLOD in sharedLODs) 71 | { 72 | var lods = sharedLOD.GetComponent().GetLODs(); 73 | if (lods.Length == 0) continue; 74 | var lod0 = lods[0].renderers.FirstOrDefault(x => x is MeshRenderer) as MeshRenderer; 75 | if (lod0 == null) continue; 76 | 77 | var mrs = sharedLOD.RenderersToLightmap; 78 | var lda = GITweaksLightingDataAssetEditor.GetLDAForScene(lod0.gameObject.scene.path); 79 | GITweaksLightingDataAssetEditor.CopyAtlasSettingsToRenderers(lda, lod0, mrs); 80 | } 81 | if (GITweaksSettingsWindow.IsEnabled(GITweak.Logging) && sharedLODs.Length > 0) 82 | Debug.Log($"[GITweaks] Finished re-arranging {sharedLODs.Length} LOD groups for sharing."); 83 | } 84 | 85 | class AtlassingCache 86 | { 87 | public Dictionary AtlasIndices; 88 | public List AtlasSizes; 89 | public List> RenderersPerAtlas; 90 | public Dictionary PixelRectsFractional; 91 | public Dictionary PixelRects; 92 | public Dictionary RendererScale; 93 | public Dictionary UVBounds; 94 | 95 | private AtlassingCache() { } 96 | 97 | public AtlassingCache(Atlassing atlassing, List atlasSizes) 98 | { 99 | AtlasIndices = atlassing.ToDictionary(x => x.Key, x => x.Value.lightmapIndex); 100 | AtlasSizes = atlasSizes; 101 | PixelRectsFractional = new Dictionary(); 102 | PixelRects = new Dictionary(); 103 | RendererScale = new Dictionary(); 104 | UVBounds = new Dictionary(); 105 | 106 | RenderersPerAtlas = new List>(); 107 | for (int i = 0; i < atlasSizes.Count; i++) 108 | { 109 | RenderersPerAtlas.Add(new HashSet()); 110 | } 111 | 112 | foreach ((Component c, (int idx, Vector4 st)) in atlassing) 113 | { 114 | var stRect = new Rect(st.z, st.w, st.x, st.y); 115 | var pixelRect = stRect; 116 | 117 | var uvBounds = GITweaksUtils.ComputeUVBounds(c); 118 | pixelRect = GITweaksUtils.STRectToPixelRect(uvBounds, stRect); 119 | 120 | var fractionalPixelRect = new Rect(atlasSizes[idx] * pixelRect.position, atlasSizes[idx] * pixelRect.size); 121 | PixelRectsFractional[c] = fractionalPixelRect; 122 | PixelRects[c] = fractionalPixelRect.ToRectInt(); 123 | RendererScale[c] = Vector2Int.one; 124 | UVBounds[c] = uvBounds; 125 | RenderersPerAtlas[idx].Add(c); 126 | } 127 | } 128 | 129 | public AtlassingCache Copy() 130 | { 131 | var copy = new AtlassingCache(); 132 | copy.AtlasIndices = new Dictionary(AtlasIndices); 133 | copy.AtlasSizes = new List(AtlasSizes); 134 | copy.RenderersPerAtlas = RenderersPerAtlas.Select(x => new HashSet(x)).ToList(); 135 | copy.PixelRectsFractional = new Dictionary(PixelRectsFractional); 136 | copy.PixelRects = new Dictionary(PixelRects); 137 | copy.RendererScale = new Dictionary(RendererScale); 138 | copy.UVBounds = new Dictionary(UVBounds); 139 | return copy; 140 | } 141 | } 142 | 143 | private static float GetCoveragePercentageInRange(AtlassingCache atlassing, int startIndex, int amount) 144 | { 145 | int lightmapArea = 0; 146 | float pixelRectsArea = 0; 147 | 148 | for (int i = 0; i < amount; i++) 149 | { 150 | int lightmapIndex = startIndex + i; 151 | 152 | Vector2Int lightmapSize = atlassing.AtlasSizes[lightmapIndex]; 153 | lightmapArea += lightmapSize.x * lightmapSize.y; 154 | 155 | pixelRectsArea += atlassing.AtlasIndices 156 | .Where(x => x.Value == lightmapIndex) 157 | .Select(x => atlassing.PixelRects[x.Key].size) 158 | .Select(x => x.x * x.y) 159 | .Sum(); 160 | } 161 | 162 | return pixelRectsArea / lightmapArea; 163 | } 164 | 165 | private static float GetCoveragePercentage(AtlassingCache atlassing, int lightmapIndex) 166 | { 167 | return GetCoveragePercentageInRange(atlassing, lightmapIndex, 1); 168 | } 169 | 170 | private static float GetCoveragePercentageInRange(AtlassingCache atlassing) 171 | { 172 | return GetCoveragePercentageInRange(atlassing, 0, atlassing.AtlasSizes.Count); 173 | } 174 | 175 | private static float GetSplitCoveragePercentage(Vector2Int splitAtlasSize, int splitAtlasCount, IEnumerable<(Component key, RectInt rect)> instances) 176 | { 177 | int lightmapArea = splitAtlasSize.x * splitAtlasSize.y * splitAtlasCount; 178 | float pixelRectsArea = 0; 179 | 180 | foreach (var instance in instances) 181 | { 182 | pixelRectsArea += instance.rect.width * instance.rect.height; 183 | } 184 | 185 | return pixelRectsArea / lightmapArea; 186 | } 187 | 188 | private static bool Pack( 189 | int width, 190 | int height, 191 | int padding, 192 | IEnumerable<(K key, RectInt rect)> rects, 193 | out HashSet<(K key, RectInt rect)> packedRects, 194 | out HashSet<(K key, RectInt rect)> remainder) 195 | { 196 | GITweaksTexturePacker packer = new GITweaksTexturePacker(width, height, padding, 0); 197 | packedRects = new HashSet<(K key, RectInt size)>(); 198 | remainder = rects.ToHashSet(); 199 | var sorted = rects.OrderByDescending(x => x.rect.width * x.rect.height); 200 | foreach (var instance in sorted) 201 | { 202 | if (!packer.Pack(instance.rect.width, instance.rect.height, out var frame)) 203 | return false; 204 | 205 | packedRects.Add((instance.key, frame)); 206 | remainder.Remove(instance); 207 | } 208 | return true; 209 | } 210 | 211 | // To include bilinear neighborhood 212 | private static Rect DilateRect(Rect rect) 213 | { 214 | rect.position -= Vector2.one*2; 215 | rect.size += Vector2.one * 2*2; 216 | return rect; 217 | } 218 | 219 | private static Vector2Int DilatePosition(Vector2Int pos) 220 | { 221 | pos -= Vector2Int.one*2; 222 | return pos; 223 | } 224 | 225 | private static void RepackAtlasses() 226 | { 227 | // Find textures used before 228 | var usedBefore = LightmapSettings.lightmaps.SelectMany(x => new [] { x.lightmapColor, x.lightmapDir, x.shadowMask }).ToHashSet(); 229 | 230 | for (int i = 0; i < SceneManager.sceneCount; i++) 231 | { 232 | var scene = SceneManager.GetSceneAt(i); 233 | RepackAtlassesForScene(scene.path); 234 | } 235 | 236 | // Now diff lightmaps, delete unused 237 | GITweaksUtils.RefreshLDA(); 238 | var usedAfter = LightmapSettings.lightmaps.SelectMany(x => new[] { x.lightmapColor, x.lightmapDir, x.shadowMask }).ToHashSet(); 239 | usedBefore.ExceptWith(usedAfter); 240 | foreach (var asset in usedBefore) 241 | { 242 | string assetPath = AssetDatabase.GetAssetPath(asset); 243 | if (!string.IsNullOrEmpty(assetPath)) 244 | AssetDatabase.DeleteAsset(assetPath); 245 | } 246 | } 247 | 248 | private static void RepackAtlassesForScene(string scenePath) 249 | { 250 | var lda = GITweaksLightingDataAssetEditor.GetLDAForScene(scenePath); 251 | if (lda == null) 252 | return; 253 | 254 | var initialLightmaps = GITweaksLightingDataAssetEditor.GetLightmaps(lda); 255 | if (initialLightmaps.Length == 0) 256 | return; 257 | 258 | // Find lightmap index offset in case of multiscene 259 | var firstLightmap = initialLightmaps[0].lightmapColor; 260 | var globalLightmaps = LightmapSettings.lightmaps; 261 | int lightmapIndexBase = 0; 262 | for (int i = 0; i < globalLightmaps.Length; i++) 263 | { 264 | if (globalLightmaps[i].lightmapColor == firstLightmap) 265 | { 266 | lightmapIndexBase = i; 267 | break; 268 | } 269 | } 270 | 271 | bool hasDirectionality = initialLightmaps[0].lightmapDir != null; 272 | bool hasShadowmask = initialLightmaps[0].shadowMask != null; 273 | 274 | // Settings 275 | float minCoveragePercent = GITweaksSettingsWindow.LightmapOptimizationTargetCoverage; 276 | int minLightmapSize = GITweaksSettingsWindow.LightmapOptimizationMinLightmapSize; 277 | int padding = Mathf.Max(3, Lightmapping.lightingSettings.lightmapPadding); 278 | 279 | var initialAtlassing = GITweaksLightingDataAssetEditor.GetAtlassing(lda); 280 | if (initialAtlassing == null || initialAtlassing.Count == 0) 281 | return; 282 | 283 | List atlasSizes = new List(); 284 | for (int i = 0; i < initialLightmaps.Length; i++) 285 | atlasSizes.Add(new Vector2Int(initialLightmaps[i].lightmapColor.width, initialLightmaps[i].lightmapColor.height)); 286 | var initialAtlassingCache = new AtlassingCache(initialAtlassing, atlasSizes); 287 | var atlassingCache = initialAtlassingCache.Copy(); 288 | 289 | bool didRepack = false; 290 | 291 | for (int i = 0; i < atlassingCache.AtlasSizes.Count; i++) 292 | { 293 | // TODO: Halving 294 | 295 | float coverage = GetCoveragePercentage(atlassingCache, i); 296 | if (coverage < minCoveragePercent) 297 | { 298 | // Get quadrant size, check it is big enough 299 | var splitLightmapSize = atlassingCache.AtlasSizes[i] / 2; 300 | if (splitLightmapSize.x < minLightmapSize || splitLightmapSize.y < minLightmapSize) 301 | continue; 302 | 303 | // Get the renderers to repack 304 | var renderers = atlassingCache.RenderersPerAtlas[i]; 305 | var rectsToPack = renderers.Select(x => (x, atlassingCache.PixelRects[x])); 306 | 307 | // Try to repack 1 smaller quadrant. If we can't fit anything, go to next atlas. 308 | Pack(splitLightmapSize.x, splitLightmapSize.y, padding, rectsToPack, out var packedRectsFirst, out var remainder); 309 | if (packedRectsFirst.Count == 0) 310 | continue; 311 | 312 | // Repack into smaller quadrants until we either packed every rect, or until we fail to pack anymore 313 | var packedRects = new List>() { packedRectsFirst }; 314 | while (remainder.Count > 0) 315 | { 316 | Pack(splitLightmapSize.x, splitLightmapSize.y, padding, remainder, out var packedRectsCont, out remainder); 317 | if (packedRectsCont.Count == 0) 318 | break; 319 | packedRects.Add(packedRectsCont); 320 | } 321 | 322 | // If there is still some remainder, this packing isn't valid, so go to next atlas. 323 | if (remainder.Count > 0) 324 | continue; 325 | 326 | // If the coverage is now worse, there is no saving - go to next atlas. 327 | if (GetSplitCoveragePercentage(splitLightmapSize, packedRects.Count, rectsToPack) <= coverage) 328 | continue; 329 | 330 | atlassingCache.AtlasSizes.RemoveAt(i); 331 | atlassingCache.AtlasSizes.InsertRange(i, packedRects.Select(_ => splitLightmapSize)); 332 | 333 | HashSet[] newRenderersPerAtlas = packedRects.Select(rects => rects.Select(x => x.key).ToHashSet()).ToArray(); 334 | atlassingCache.RenderersPerAtlas.RemoveAt(i); 335 | atlassingCache.RenderersPerAtlas.InsertRange(i, newRenderersPerAtlas.Take(packedRects.Count)); 336 | 337 | for (int group = 0; group < packedRects.Count; group++) 338 | { 339 | foreach (var renderer in packedRects[group]) 340 | { 341 | atlassingCache.AtlasIndices[renderer.key] = i + group; 342 | atlassingCache.PixelRects[renderer.key] = renderer.rect; 343 | atlassingCache.PixelRectsFractional[renderer.key] = renderer.rect.ToRect(); 344 | atlassingCache.RendererScale[renderer.key] *= 2; 345 | } 346 | } 347 | 348 | didRepack = true; 349 | 350 | // We just replaced an atlas, now we want to re-visit the results of that 351 | i--; 352 | } 353 | } 354 | 355 | // If we achieved did nothing, just bail 356 | if (!didRepack) 357 | return; 358 | 359 | // Create new lightmap textures to render into 360 | var newLightmapRTs = new (RenderTexture light, RenderTexture dir, RenderTexture shadow)[atlassingCache.AtlasSizes.Count]; 361 | for (int i = 0; i < newLightmapRTs.Length; i++) 362 | { 363 | var size = atlassingCache.AtlasSizes[i]; 364 | var light = new RenderTexture(new RenderTextureDescriptor(size.x, size.y) { graphicsFormat = GraphicsFormat.R16G16B16A16_SFloat, enableRandomWrite = true, }); 365 | var dir = hasDirectionality ? new RenderTexture(new RenderTextureDescriptor(size.x, size.y) { graphicsFormat = GraphicsFormat.R8G8B8A8_UNorm, enableRandomWrite = true, }) : null; 366 | var shadow = hasShadowmask ? new RenderTexture(new RenderTextureDescriptor(size.x, size.y) { graphicsFormat = GraphicsFormat.R8G8B8A8_UNorm, enableRandomWrite = true, }) : null; 367 | newLightmapRTs[i] = (light, dir, shadow); 368 | } 369 | 370 | // Copy renderers over, update their atlassing data 371 | foreach ((var renderer, int lightmapIndex) in atlassingCache.AtlasIndices) 372 | { 373 | var oldAtlasData = initialAtlassing[renderer]; 374 | var newAtlasData = oldAtlasData; 375 | 376 | // Create new atlas data 377 | var lmSize = atlassingCache.AtlasSizes[lightmapIndex]; 378 | var pixelRectScaled = atlassingCache.PixelRects[renderer]; 379 | 380 | Vector2 scale = atlassingCache.RendererScale[renderer]; 381 | newAtlasData.lightmapST = new Vector4( 382 | newAtlasData.lightmapST.x * scale.x, 383 | newAtlasData.lightmapST.y * scale.y, 384 | (float)pixelRectScaled.position.x / lmSize.x, 385 | (float)pixelRectScaled.position.y / lmSize.y); 386 | newAtlasData.lightmapIndex = lightmapIndex; 387 | GITweaksUtils.OffsetLightmapSTByPixelRectOffset(atlassingCache.UVBounds[renderer], ref newAtlasData.lightmapST); 388 | 389 | // Copy renderer 390 | int oldLightmapIndex = oldAtlasData.lightmapIndex; 391 | var oldRect = DilateRect(initialAtlassingCache.PixelRectsFractional[renderer]); 392 | var newPosition = DilatePosition(atlassingCache.PixelRects[renderer].position); 393 | 394 | Texture2D oldLightmap = initialLightmaps[oldLightmapIndex].lightmapColor; 395 | RenderTexture newLightmap = newLightmapRTs[lightmapIndex].light; 396 | GITweaksUtils.CopyFractional(oldLightmap, oldRect, newLightmap, newPosition, PlayerSettings.colorSpace == ColorSpace.Gamma); 397 | 398 | if (hasDirectionality) 399 | { 400 | Texture2D oldDir = initialLightmaps[oldLightmapIndex].lightmapDir; 401 | RenderTexture newDir = newLightmapRTs[lightmapIndex].dir; 402 | GITweaksUtils.CopyFractional(oldDir, oldRect, newDir, newPosition, false); 403 | } 404 | 405 | if (hasShadowmask) 406 | { 407 | Texture2D oldShadowmask = initialLightmaps[oldLightmapIndex].shadowMask; 408 | RenderTexture newShadowmask = newLightmapRTs[lightmapIndex].shadow; 409 | GITweaksUtils.CopyFractional(oldShadowmask, oldRect, newShadowmask, newPosition, false); 410 | } 411 | 412 | initialAtlassing[renderer] = newAtlasData; 413 | } 414 | 415 | // Convert to texture2D and import the new lightmaps 416 | var newLightmaps = new LightmapData[newLightmapRTs.Length]; 417 | for (int i = 0; i < newLightmapRTs.Length; i++) 418 | { 419 | newLightmaps[i] = new LightmapData(); 420 | 421 | var newPair = newLightmapRTs[i]; 422 | 423 | var newColor = GITweaksUtils.RenderTextureToTexture2D(newPair.light); 424 | newPair.light.Release(); 425 | Object.DestroyImmediate(newPair.light); 426 | string lmPath = AssetDatabase.GetAssetPath(initialLightmaps[0].lightmapColor).Replace("Lightmap-", $"Lightmap-{i}-"); 427 | File.WriteAllBytes(lmPath, newColor.EncodeToEXR()); 428 | Object.DestroyImmediate(newColor); 429 | AssetDatabase.ImportAsset(lmPath, ImportAssetOptions.ForceSynchronousImport); 430 | GITweaksUtils.CopyImporterSettingsAndReimport(initialLightmaps[0].lightmapColor, lmPath); 431 | newLightmaps[i].lightmapColor = AssetDatabase.LoadAssetAtPath(lmPath); 432 | 433 | if (hasDirectionality) 434 | { 435 | var newDir = GITweaksUtils.RenderTextureToTexture2D(newPair.dir); 436 | newPair.dir.Release(); 437 | Object.DestroyImmediate(newPair.dir); 438 | string dirPath = AssetDatabase.GetAssetPath(initialLightmaps[0].lightmapDir).Replace("Lightmap-", $"Lightmap-{i}-"); 439 | File.WriteAllBytes(dirPath, newDir.EncodeToPNG()); 440 | Object.DestroyImmediate(newDir); 441 | AssetDatabase.ImportAsset(dirPath, ImportAssetOptions.ForceSynchronousImport); 442 | GITweaksUtils.CopyImporterSettingsAndReimport(initialLightmaps[0].lightmapDir, dirPath); 443 | newLightmaps[i].lightmapDir = AssetDatabase.LoadAssetAtPath(dirPath); 444 | } 445 | 446 | if (hasShadowmask) 447 | { 448 | var newShadow = GITweaksUtils.RenderTextureToTexture2D(newPair.shadow); 449 | newPair.shadow.Release(); 450 | Object.DestroyImmediate(newPair.shadow); 451 | string smPath = AssetDatabase.GetAssetPath(initialLightmaps[0].shadowMask).Replace("Lightmap-", $"Lightmap-{lightmapIndexBase}-{i}-"); 452 | File.WriteAllBytes(smPath, newShadow.EncodeToPNG()); 453 | Object.DestroyImmediate(newShadow); 454 | AssetDatabase.ImportAsset(smPath, ImportAssetOptions.ForceSynchronousImport); 455 | GITweaksUtils.CopyImporterSettingsAndReimport(initialLightmaps[0].shadowMask, smPath); 456 | newLightmaps[i].shadowMask = AssetDatabase.LoadAssetAtPath(smPath); 457 | } 458 | } 459 | 460 | GITweaksLightingDataAssetEditor.UpdateAtlassing(lda, initialAtlassing); 461 | GITweaksLightingDataAssetEditor.UpdateLightmaps(lda, newLightmaps); 462 | 463 | if (GITweaksSettingsWindow.IsEnabled(GITweak.Logging)) 464 | Debug.Log($"[GITweaks] Finished re-packing atlasses for scene \"{scenePath}\". " + 465 | $"New atlas count: {newLightmaps.Length}. " + 466 | $"Old atlas count: {initialLightmaps.Length}. " + 467 | $"New coverage: {GetCoveragePercentageInRange(atlassingCache) * 100}%. " + 468 | $"Old coverage: {GetCoveragePercentageInRange(initialAtlassingCache) * 100}%. "); 469 | } 470 | } 471 | } -------------------------------------------------------------------------------- /Editor/GITweaksPostBakeOperations.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 532137b9d888a4e46b156082ca28b910 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GITweaksSeamFixer.cs: -------------------------------------------------------------------------------- 1 | // Uses ideas from https://www.sebastiansylvan.com/post/LeastSquaresTextureSeams/ 2 | // and https://gist.github.com/ssylvan/18fb6875824c14aa2b8c by Sebastian Sylvan, provided under MIT license. 3 | 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using UnityEditor; 9 | using UnityEngine; 10 | using UnityEngine.Experimental.Rendering; 11 | using UnityEngine.SceneManagement; 12 | 13 | namespace GITweaks 14 | { 15 | [CustomEditor(typeof(GITweaksSeamFix))] 16 | public class GITweaksSeamFixEditor : Editor 17 | { 18 | public override void OnInspectorGUI() 19 | { 20 | var sf = target as GITweaksSeamFix; 21 | if (target == null) 22 | return; 23 | 24 | base.OnInspectorGUI(); // TODO: Better inspector 25 | 26 | serializedObject.Update(); 27 | 28 | //var sp = serializedObject.FindProperty(nameof(GITweaksSeamFix.RenderersToFixSeamsWith)); 29 | //EditorGUILayout.PropertyField(sp); 30 | 31 | EditorGUILayout.BeginHorizontal(); 32 | if (GUILayout.Button("Preview fix")) 33 | { 34 | GITweaksUtils.RefreshLDA(); 35 | GITweaksSeamFixer.FixSeams(sf, false); 36 | } 37 | if (GUILayout.Button("Reset preview")) 38 | { 39 | GITweaksUtils.RefreshLDA(); 40 | } 41 | EditorGUILayout.EndHorizontal(); 42 | 43 | if (GUILayout.Button("Apply fix")) 44 | { 45 | GITweaksUtils.RefreshLDA(); 46 | DelayApplyTarget = sf; 47 | EditorApplication.update += DelayApply; 48 | } 49 | 50 | if (EditorGUILayout.LinkButton("Open documentation")) 51 | Application.OpenURL("https://github.com/pema99/GITweaks/tree/master?tab=readme-ov-file#fix-lightmap-seams-between-objects"); 52 | 53 | serializedObject.ApplyModifiedProperties(); 54 | } 55 | 56 | private static GITweaksSeamFix DelayApplyTarget; 57 | private static void DelayApply() 58 | { 59 | if (DelayApplyTarget != null) 60 | GITweaksSeamFixer.FixSeams(DelayApplyTarget, true); 61 | EditorApplication.update -= DelayApply; 62 | } 63 | } 64 | 65 | [CustomEditor(typeof(GITweaksSeamFixVolume))] 66 | public class GITweaksSeamFixVolumeEditor : Editor 67 | { 68 | [MenuItem("GameObject/Light/GI Tweaks Seam Fix Volume")] 69 | public static void AddVolume() 70 | { 71 | GameObject go = new GameObject("Seam Fix Volume"); 72 | go.AddComponent(); 73 | Selection.activeGameObject = go; 74 | } 75 | 76 | public override void OnInspectorGUI() 77 | { 78 | var sfv = target as GITweaksSeamFixVolume; 79 | if (target == null) 80 | return; 81 | 82 | base.OnInspectorGUI(); // TODO: Better inspector 83 | 84 | serializedObject.Update(); 85 | 86 | EditorGUILayout.BeginHorizontal(); 87 | if (GUILayout.Button("Preview fix")) 88 | { 89 | GITweaksUtils.RefreshLDA(); 90 | GITweaksSeamFixer.FixSeams(sfv, false); 91 | } 92 | if (GUILayout.Button("Reset preview")) 93 | { 94 | GITweaksUtils.RefreshLDA(); 95 | } 96 | EditorGUILayout.EndHorizontal(); 97 | 98 | if (GUILayout.Button("Apply fix")) 99 | { 100 | GITweaksUtils.RefreshLDA(); 101 | DelayApplyTarget = sfv; 102 | EditorApplication.update += DelayApply; 103 | } 104 | 105 | if (EditorGUILayout.LinkButton("Open documentation")) 106 | Application.OpenURL("https://github.com/pema99/GITweaks/tree/master?tab=readme-ov-file#fix-lightmap-seams-between-objects"); 107 | 108 | serializedObject.ApplyModifiedProperties(); 109 | } 110 | 111 | private static GITweaksSeamFixVolume DelayApplyTarget; 112 | private static void DelayApply() 113 | { 114 | if (DelayApplyTarget != null) 115 | GITweaksSeamFixer.FixSeams(DelayApplyTarget, true); 116 | EditorApplication.update -= DelayApply; 117 | } 118 | } 119 | 120 | public static class GITweaksSeamFixer 121 | { 122 | public struct SamplePoint 123 | { 124 | public Vector3 vertex; 125 | public Vector3 normal; 126 | public Vector2 uv; 127 | } 128 | 129 | public struct PixelInfo 130 | { 131 | public Vector2Int position; 132 | public Color color; 133 | public int lightmapIndex; 134 | } 135 | 136 | private static float GetSamplesPerMeter(MeshRenderer mr) 137 | { 138 | float resolution = Lightmapping.lightingSettingsDefaults.lightmapResolution; 139 | if (Lightmapping.TryGetLightingSettings(out var settings)) 140 | resolution = settings.lightmapResolution; 141 | return mr.scaleInLightmap * resolution; 142 | } 143 | 144 | private static List GenerateSamplePoints(MeshRenderer selfMr, Bounds? bounds) 145 | { 146 | var mesh = selfMr.GetComponent().sharedMesh; 147 | 148 | // Get attributes 149 | var verts = mesh.vertices; 150 | var normals = mesh.normals; 151 | var l2w = selfMr.transform.localToWorldMatrix; 152 | for (int i = 0; i < verts.Length; i++) 153 | { 154 | verts[i] = l2w.MultiplyPoint3x4(verts[i]); 155 | normals[i] = l2w.MultiplyVector(normals[i]); 156 | } 157 | var uvs = mesh.uv2; 158 | if (uvs == null || uvs.Length == 0) uvs = mesh.uv; 159 | var indices = mesh.triangles; 160 | 161 | // Find edges 162 | Dictionary<(int indexA, int indexB), int> edgeRefs = new Dictionary<(int indexA, int indexB), int>(); 163 | for (int i = 0; i < indices.Length; i += 3) 164 | { 165 | for (int j = 0; j < 3; j++) 166 | { 167 | (int a, int b) edge = (indices[i + j], indices[i + ((j + 1) % 3)]); 168 | if (edge.a > edge.b) edge = (edge.b, edge.a); 169 | if (!edgeRefs.ContainsKey(edge)) 170 | edgeRefs.Add(edge, 0); 171 | edgeRefs[edge]++; 172 | } 173 | } 174 | var edges = edgeRefs 175 | .Where(x => x.Value == 1) 176 | .Select(x => x.Key) 177 | .ToArray(); 178 | 179 | float samplesPerMeter = GetSamplesPerMeter(selfMr) * 1.5f; // Add a bit of bias, just above sqrt(2) 180 | 181 | // Generate samples along them 182 | List selfSamplePoints = new List(); 183 | for (int i = 0; i < edges.Length; i++) 184 | { 185 | Vector3 vertA = verts[edges[i].indexA]; 186 | Vector3 vertB = verts[edges[i].indexB]; 187 | Vector3 normalA = normals[edges[i].indexA].normalized; 188 | Vector3 normalB = normals[edges[i].indexB].normalized; 189 | Vector2 uvA = uvs[edges[i].indexA]; 190 | Vector2 uvB = uvs[edges[i].indexB]; 191 | 192 | float length = Vector3.Distance(vertA, vertB); 193 | int numSamples = Mathf.Max(3, Mathf.CeilToInt(length * samplesPerMeter)); 194 | for (int j = 0; j < numSamples; j++) 195 | { 196 | float t = (float)j / (float)(numSamples - 1); 197 | selfSamplePoints.Add(new SamplePoint 198 | { 199 | vertex = Vector3.Lerp(vertA, vertB, t), 200 | normal = Vector3.Lerp(normalA, normalB, t).normalized, // TODO: Slerp ? 201 | uv = Vector2.Lerp(uvA, uvB, t) 202 | }); 203 | } 204 | } 205 | 206 | if (bounds != null) 207 | { 208 | selfSamplePoints.RemoveAll(x => !bounds.Value.Contains(x.vertex)); 209 | } 210 | 211 | return selfSamplePoints; 212 | } 213 | 214 | private static Vector2 UVToLightmap(Vector2 uv, Vector4 st, int lightmapWidth, int lightmapHeight) 215 | { 216 | Vector2 instanceUV = uv; 217 | instanceUV *= new Vector2(st.x, st.y); 218 | instanceUV += new Vector2(st.z, st.w); 219 | 220 | Vector2 lightmapUV = new Vector2(instanceUV.x * lightmapWidth, instanceUV.y * lightmapHeight); 221 | lightmapUV -= Vector2.one * 0.5f; 222 | 223 | return lightmapUV; 224 | } 225 | 226 | public static void FixSeams(GITweaksSeamFix sf, bool saveToDisk) 227 | { 228 | foreach (var other in sf.RenderersToFixSeamsWith) 229 | { 230 | FixSeams( 231 | sf.GetComponent(), 232 | other, 233 | saveToDisk, 234 | null, 235 | sf.MaxSurfaceAngle, 236 | sf.MaxSolverIterationCount, 237 | sf.SolverTolerance, 238 | sf.SeamFixStrength); 239 | } 240 | 241 | if (GITweaksSettingsWindow.IsEnabled(GITweak.Logging) && saveToDisk) 242 | Debug.Log($"[GITweaks] Finished applying seam fixes for GameObject \"{sf.gameObject.name}\""); 243 | } 244 | 245 | public static void FixSeams(GITweaksSeamFixVolume sfv, bool saveToDisk) 246 | { 247 | var bounds = new Bounds(sfv.transform.position, sfv.transform.lossyScale); 248 | 249 | var mrs = Object.FindObjectsByType(FindObjectsSortMode.None); 250 | var filtered = new List(); 251 | foreach (var mr in mrs) 252 | { 253 | if (bounds.Intersects(mr.bounds) && !sfv.RenderersToExclude.Contains(mr)) 254 | { 255 | filtered.Add(mr); 256 | } 257 | } 258 | 259 | for (int i = 0; i < filtered.Count; i++) 260 | { 261 | MeshRenderer self = filtered[i]; 262 | for (int j = i + 1; j < filtered.Count; j++) 263 | { 264 | MeshRenderer other = filtered[j]; 265 | FixSeams( 266 | self, 267 | other, 268 | saveToDisk, 269 | bounds, 270 | sfv.MaxSurfaceAngle, 271 | sfv.MaxSolverIterationCount, 272 | sfv.SolverTolerance, 273 | sfv.SeamFixStrength); 274 | } 275 | } 276 | 277 | if (GITweaksSettingsWindow.IsEnabled(GITweak.Logging) && saveToDisk) 278 | Debug.Log($"[GITweaks] Finished applying seam fixes for volume. {filtered.Count} MeshRenderers were taken into account."); 279 | } 280 | 281 | public static List<(SamplePoint self, SamplePoint other)> GenerateSamplePairs( 282 | MeshRenderer selfMr, 283 | MeshRenderer otherMr, 284 | float maxSearchAngle, 285 | Bounds? bounds) 286 | { 287 | var result = new List<(SamplePoint, SamplePoint)>(); 288 | if (!GITweaksUtils.IsLightmapped(selfMr) || !GITweaksUtils.IsLightmapped(otherMr)) 289 | return result; 290 | 291 | var selfSamples = GenerateSamplePoints(selfMr, bounds); 292 | var otherSamples = GenerateSamplePoints(otherMr, bounds); 293 | 294 | float selfSamplesPerMeter = GetSamplesPerMeter(selfMr); 295 | float otherSamplesPerMeter = GetSamplesPerMeter(otherMr); 296 | float maxSamplesPerMeter = Mathf.Min(selfSamplesPerMeter, otherSamplesPerMeter); 297 | float sameDist = (1.0f / maxSamplesPerMeter) * 0.5f; 298 | 299 | var otherHashGrid = new Dictionary>(); 300 | foreach (var otherSample in otherSamples) 301 | { 302 | Vector3Int quantized = Vector3Int.FloorToInt(otherSample.vertex / sameDist); 303 | if (!otherHashGrid.TryGetValue(quantized, out var cell)) 304 | otherHashGrid[quantized] = cell = new List(); 305 | cell.Add(otherSample); 306 | } 307 | 308 | // Find sample pairs 309 | foreach (var selfSample in selfSamples) 310 | { 311 | Vector3Int quantized = Vector3Int.FloorToInt(selfSample.vertex / sameDist); 312 | 313 | for (int x = 0; x < 3; x++) 314 | for (int y = 0; y < 3; y++) 315 | for (int z = 0; z < 3; z++) 316 | { 317 | Vector3Int cellPos = quantized + new Vector3Int(x - 1, y - 1, z - 1); 318 | if (otherHashGrid.TryGetValue(cellPos, out var closeSamples)) 319 | { 320 | foreach (var otherSample in closeSamples) 321 | { 322 | if (Vector3.Distance(selfSample.vertex, otherSample.vertex) <= sameDist && 323 | Vector3.Angle(selfSample.normal, otherSample.normal) < maxSearchAngle) 324 | { 325 | result.Add((selfSample, otherSample)); 326 | } 327 | } 328 | } 329 | } 330 | } 331 | 332 | return result; 333 | } 334 | 335 | public static void FixSeams( 336 | MeshRenderer selfMr, 337 | MeshRenderer otherMr, 338 | bool saveToDisk, 339 | Bounds? bounds, 340 | float maxSearchAngle, 341 | int solveIterations, 342 | float solveTolerance, 343 | float edgeConstraintWeight) 344 | { 345 | if (!GITweaksUtils.IsLightmapped(selfMr) || !GITweaksUtils.IsLightmapped(otherMr) || LightmapSettings.lightmaps.Length == 0) 346 | return; 347 | 348 | // Generate samples 349 | var samplePairs = GenerateSamplePairs(selfMr, otherMr, maxSearchAngle, bounds); 350 | // TODO: Kernel around each sample 351 | 352 | // Get writable lightmaps 353 | Texture2D selfLightmap; 354 | Texture2D otherLightmap; 355 | Color[] selfColors; 356 | Color[] otherColors; 357 | if (selfMr.lightmapIndex == otherMr.lightmapIndex) 358 | { 359 | selfLightmap = otherLightmap = GetRWLightmap(selfMr); 360 | selfColors = otherColors = selfLightmap.GetPixels(); 361 | } 362 | else 363 | { 364 | selfLightmap = GetRWLightmap(selfMr); 365 | otherLightmap = GetRWLightmap(otherMr); 366 | selfColors = selfLightmap.GetPixels(); 367 | otherColors = otherLightmap.GetPixels(); 368 | } 369 | 370 | // Get bilinear neighborhood of each sample 371 | List pixelInfo = new List(); 372 | Dictionary selfPixelToPixelInfoMap = new Dictionary(); 373 | Dictionary otherPixelToPixelInfoMap = new Dictionary(); 374 | foreach (var samplePoint in samplePairs) 375 | { 376 | Vector2 selfUV = samplePoint.self.uv; 377 | Vector2 selfLightmapUV = UVToLightmap(selfUV, selfMr.lightmapScaleOffset, selfLightmap.width, selfLightmap.height); 378 | 379 | Vector2 otherUV = samplePoint.other.uv; 380 | Vector2 otherLightmapUV = UVToLightmap(otherUV, otherMr.lightmapScaleOffset, otherLightmap.width, otherLightmap.height); 381 | 382 | for (int i = 0; i < 4; i++) 383 | { 384 | int xOffset = i & 0b01; 385 | int yOffset = (i & 0b10) >> 1; 386 | 387 | Vector2Int offset = new Vector2Int(xOffset, yOffset); 388 | Vector2Int selfPos = new Vector2Int((int)selfLightmapUV.x, (int)selfLightmapUV.y) + offset; 389 | if (!selfPixelToPixelInfoMap.ContainsKey(selfPos) && selfPos.x < selfLightmap.width && selfPos.y < selfLightmap.height) 390 | { 391 | int selfIndex = selfPos.y * selfLightmap.width + selfPos.x; 392 | //selfColors[selfIndex] = Color.red; 393 | pixelInfo.Add(new PixelInfo { color = selfColors[selfIndex], lightmapIndex = selfMr.lightmapIndex, position = selfPos }); 394 | selfPixelToPixelInfoMap.Add(selfPos, pixelInfo.Count - 1); 395 | } 396 | 397 | Vector2Int otherPos = new Vector2Int((int)otherLightmapUV.x, (int)otherLightmapUV.y) + offset; 398 | if (!otherPixelToPixelInfoMap.ContainsKey(otherPos) && otherPos.x < otherLightmap.width && otherPos.y < otherLightmap.height) 399 | { 400 | int otherIndex = otherPos.y * otherLightmap.width + otherPos.x; 401 | //otherColors[otherIndex] = Color.magenta; 402 | pixelInfo.Add(new PixelInfo { color = otherColors[otherIndex], lightmapIndex = otherMr.lightmapIndex, position = otherPos }); 403 | otherPixelToPixelInfoMap.Add(otherPos, pixelInfo.Count - 1); 404 | } 405 | } 406 | } 407 | 408 | // Setup solver 409 | int totalPixels = pixelInfo.Count; 410 | SparseMat AtA = new SparseMat(totalPixels, totalPixels); 411 | VectorX[] Atbs = { new VectorX(totalPixels), new VectorX(totalPixels), new VectorX(totalPixels) }; 412 | VectorX[] guesses = { new VectorX(totalPixels), new VectorX(totalPixels), new VectorX(totalPixels) }; 413 | SetupLeastSquares( 414 | selfMr.lightmapScaleOffset, 415 | otherMr.lightmapScaleOffset, 416 | selfLightmap.width, 417 | selfLightmap.height, 418 | otherLightmap.width, 419 | otherLightmap.height, 420 | edgeConstraintWeight, 421 | samplePairs, 422 | selfPixelToPixelInfoMap, 423 | otherPixelToPixelInfoMap, 424 | pixelInfo, 425 | AtA, 426 | Atbs, 427 | guesses); 428 | 429 | // Solve 430 | var solutions = new VectorX[3]; 431 | for (int i = 0; i < 3; i++) 432 | { 433 | solutions[i] = ConjugateGradientOptimize(AtA, guesses[i], Atbs[i], solveIterations, solveTolerance); 434 | } 435 | 436 | // Read back pixels 437 | for (int i = 0; i < totalPixels; i++) 438 | { 439 | PixelInfo px = pixelInfo[i]; 440 | Color col = new Color(solutions[0][i], solutions[1][i], solutions[2][i]); 441 | if (px.lightmapIndex == selfMr.lightmapIndex) 442 | selfColors[px.position.y * selfLightmap.width + px.position.x] = col; 443 | else 444 | otherColors[px.position.y * otherLightmap.width + px.position.x] = col; 445 | } 446 | 447 | // Apply gamma curve if needed 448 | if (PlayerSettings.colorSpace == ColorSpace.Gamma && saveToDisk) 449 | { 450 | for (int i = 0; i < selfColors.Length; i++) 451 | selfColors[i] = selfColors[i].linear; 452 | 453 | if (selfMr.lightmapIndex != otherMr.lightmapIndex) // Don't apply it twice 454 | { 455 | for (int i = 0; i < otherColors.Length; i++) 456 | otherColors[i] = otherColors[i].linear; 457 | } 458 | } 459 | 460 | // Apply to lightmaps 461 | selfLightmap.SetPixels(selfColors); 462 | selfLightmap.Apply(); 463 | otherLightmap.SetPixels(otherColors); 464 | otherLightmap.Apply(); 465 | 466 | var initialLightmaps = LightmapSettings.lightmaps; 467 | if (saveToDisk) 468 | { 469 | // Save and apply importer settings 470 | MeshRenderer[] mrs = { selfMr, otherMr }; 471 | Texture2D[] newLMs = { selfLightmap, otherLightmap }; 472 | int numLMs = selfMr.lightmapIndex == otherMr.lightmapIndex ? 1 : 2; 473 | for (int i = 0; i < numLMs; i++) 474 | { 475 | int lmIndex = mrs[i].lightmapIndex; 476 | string lmPath = AssetDatabase.GetAssetPath(initialLightmaps[lmIndex].lightmapColor); 477 | File.WriteAllBytes(lmPath, newLMs[i].EncodeToEXR()); 478 | Object.DestroyImmediate(newLMs[i]); 479 | AssetDatabase.ImportAsset(lmPath, ImportAssetOptions.ForceSynchronousImport); 480 | GITweaksUtils.CopyImporterSettingsAndReimport(initialLightmaps[lmIndex].lightmapColor, lmPath); 481 | } 482 | } 483 | else 484 | { 485 | initialLightmaps[selfMr.lightmapIndex].lightmapColor = selfLightmap; 486 | initialLightmaps[otherMr.lightmapIndex].lightmapColor = otherLightmap; 487 | LightmapSettings.lightmaps = initialLightmaps; 488 | } 489 | } 490 | private static void BilinearSample( 491 | Dictionary pixelToPixelInfoMap, 492 | Vector2 sample, 493 | int width, 494 | int height, 495 | float weight, 496 | int[] outIxs, 497 | float[] outWeights) 498 | { 499 | int truncu = (int)sample.x; 500 | int truncv = (int)sample.y; 501 | 502 | int[] xs = { truncu, truncu + 1, truncu + 1, truncu }; 503 | int[] ys = { truncv, truncv, truncv + 1, truncv + 1 }; 504 | for (int i = 0; i < 4; ++i) 505 | { 506 | int x = Mathf.Clamp(xs[i], 0, width); 507 | int y = Mathf.Clamp(ys[i], 0, height); 508 | outIxs[i] = pixelToPixelInfoMap[new Vector2Int(x, y)]; 509 | } 510 | 511 | float fracX = sample.x - truncu; 512 | float fracY = sample.y - truncv; 513 | outWeights[0] = (1.0f - fracX) * (1.0f - fracY); 514 | outWeights[1] = fracX * (1.0f - fracY); 515 | outWeights[2] = fracX * fracY; 516 | outWeights[3] = (1.0f - fracX) * fracY; 517 | for (int i = 0; i < 4; ++i) 518 | { 519 | outWeights[i] *= weight; 520 | } 521 | } 522 | 523 | private static void SetupLeastSquares( 524 | Vector4 selfSt, 525 | Vector4 otherSt, 526 | int selfWidth, int selfHeight, 527 | int otherWidth, int otherHeight, 528 | float edgeConstraintWeight, 529 | List<(SamplePoint self, SamplePoint other)> samplePairs, 530 | Dictionary selfPixelToPixelInfoMap, 531 | Dictionary otherPixelToPixelInfoMap, 532 | List pixelInfo, 533 | SparseMat AtA, 534 | VectorX[] Atbs, 535 | VectorX[] guesses) 536 | { 537 | int[] selfIxs = new int[4]; 538 | int[] otherIxs = new int[4]; 539 | float[] selfWeights = new float[4]; 540 | float[] otherWeights = new float[4]; 541 | foreach (var samplePair in samplePairs) 542 | { 543 | BilinearSample( 544 | selfPixelToPixelInfoMap, 545 | UVToLightmap(samplePair.self.uv, selfSt, selfWidth, selfHeight), 546 | selfWidth, selfHeight, 547 | edgeConstraintWeight, 548 | selfIxs, selfWeights); 549 | BilinearSample( 550 | otherPixelToPixelInfoMap, 551 | UVToLightmap(samplePair.other.uv, otherSt, otherWidth, otherHeight), 552 | otherWidth, otherHeight, edgeConstraintWeight, 553 | otherIxs, 554 | otherWeights); 555 | 556 | for (int i = 0; i < 4; ++i) 557 | { 558 | for (int j = 0; j < 4; ++j) 559 | { 560 | // + a*a^t 561 | AtA[selfIxs[i], selfIxs[j]] += selfWeights[i] * selfWeights[j]; 562 | // + b*b^t 563 | AtA[otherIxs[i], otherIxs[j]] += otherWeights[i] * otherWeights[j]; 564 | // - a*b^t 565 | AtA[selfIxs[i], otherIxs[j]] -= selfWeights[i] * otherWeights[j]; 566 | // - b*a^t 567 | AtA[otherIxs[i], selfIxs[j]] -= otherWeights[i] * selfWeights[j]; 568 | } 569 | } 570 | } 571 | 572 | for (int i = 0; i < pixelInfo.Count; i++) 573 | { 574 | var pixel = pixelInfo[i]; 575 | 576 | AtA[i, i] += 1.0f; // equality cost 577 | 578 | Atbs[0][i] += pixel.color.r; 579 | Atbs[1][i] += pixel.color.g; 580 | Atbs[2][i] += pixel.color.b; 581 | 582 | guesses[0][i] = pixel.color.r; 583 | guesses[1][i] = pixel.color.g; 584 | guesses[2][i] = pixel.color.b; 585 | } 586 | } 587 | 588 | private static Texture2D GetRWLightmap(MeshRenderer mr) 589 | { 590 | Texture2D lightmap = LightmapSettings.lightmaps[mr.lightmapIndex].lightmapColor; 591 | return GITweaksUtils.GetRWTextureCopy(lightmap, GraphicsFormat.R16G16B16A16_SFloat); 592 | } 593 | 594 | private static VectorX ConjugateGradientOptimize( 595 | SparseMat A, 596 | VectorX guess, 597 | VectorX b, 598 | int numIterations, 599 | float tolerance) 600 | { 601 | int n = guess.Size; 602 | VectorX p = new VectorX(n), r = new VectorX(n), Ap = new VectorX(n), tmp = new VectorX(n); 603 | VectorX x = new VectorX(n); 604 | x.CopyFrom(guess); 605 | 606 | // r = b - A * x; 607 | SparseMat.Mul(tmp, A, x); 608 | VectorX.Sub(ref r, b, tmp); 609 | 610 | p.CopyFrom(r); 611 | float rsq = VectorX.Dot(r, r); 612 | for (int i = 0; i < numIterations; ++i) 613 | { 614 | SparseMat.Mul(Ap, A, p); 615 | float alpha = rsq / VectorX.Dot(p, Ap); 616 | VectorX.MulAdd(x, p, alpha, x); // x = x + alpha * p 617 | VectorX.MulAdd(r, Ap, -alpha, r); // r = r - alpha * Ap 618 | float rsqNew = VectorX.Dot(r, r); 619 | if (Mathf.Abs(rsqNew - rsq) < tolerance * n) 620 | break; 621 | float beta = rsqNew / rsq; 622 | VectorX.MulAdd(p, p, beta, r); // p = r + beta * p 623 | rsq = rsqNew; 624 | } 625 | 626 | return x; 627 | } 628 | 629 | class SparseMat 630 | { 631 | public Row[] Rows; 632 | public int NumRows, NumCols; 633 | 634 | public SparseMat(int numRows, int numCols) 635 | { 636 | this.Rows = new Row[numRows]; 637 | this.NumRows = numRows; 638 | this.NumCols = numCols; 639 | for (int i = 0; i < numRows; i++) 640 | { 641 | Rows[i] = new Row(); 642 | } 643 | } 644 | 645 | public float this[int row, int column] 646 | { 647 | get 648 | { 649 | return Rows[row][column]; 650 | } 651 | set 652 | { 653 | Rows[row][column] = value; 654 | } 655 | } 656 | 657 | public static void Mul(VectorX outVector, SparseMat A, VectorX x) 658 | { 659 | System.Threading.Tasks.Parallel.For(0, A.NumRows, r => 660 | { 661 | outVector[r] = Dot(x, A.Rows[r]); 662 | }); 663 | } 664 | 665 | private static float Dot(VectorX x, Row row) 666 | { 667 | float sum = 0.0f; 668 | for (int i = 0; i < row.Size; i++) 669 | { 670 | sum += x[row.Indices[i]] * row.Coefficients[i]; 671 | } 672 | return sum; 673 | } 674 | 675 | public class Row 676 | { 677 | public int Size = 0; 678 | public int Capacity = 0; 679 | public float[] Coefficients; 680 | public int[] Indices; 681 | 682 | public float this[int column] 683 | { 684 | get 685 | { 686 | int index = GetColumnIndexAndGrowIfNeeded(column); 687 | return Coefficients[index]; 688 | } 689 | set 690 | { 691 | int index = GetColumnIndexAndGrowIfNeeded(column); 692 | Coefficients[index] = value; 693 | } 694 | } 695 | 696 | private void Grow() 697 | { 698 | Capacity = Capacity == 0 ? 16 : Capacity + Capacity / 2; 699 | var newCoeffs = new float[Capacity]; 700 | var newIndices = new int[Capacity]; 701 | 702 | // Copy existing data over 703 | if (Coefficients != null) 704 | { 705 | System.Array.Copy(Coefficients, newCoeffs, Size); 706 | } 707 | if (Indices != null) 708 | { 709 | System.Array.Copy(Indices, newIndices, Size); 710 | } 711 | 712 | Coefficients = newCoeffs; 713 | Indices = newIndices; 714 | } 715 | 716 | private int FindClosestIndex(int columnIndex) 717 | { 718 | for (int i = 0; i < Size; ++i) 719 | { 720 | if (Indices[i] >= columnIndex) 721 | return i; 722 | } 723 | return Size; 724 | } 725 | 726 | private int GetColumnIndexAndGrowIfNeeded(int column) 727 | { 728 | // Find the element 729 | int index = FindClosestIndex(column); 730 | if (Size == 0 || index >= Indices.Length || Indices[index] != column) // Add new element 731 | { 732 | if (Size == Capacity) 733 | { 734 | Grow(); 735 | } 736 | 737 | // Put the new element in the right place, and shift existing elements down by one. 738 | float prevCoeff = 0; 739 | int prevIndex = column; 740 | ++Size; 741 | for (int i = index; i < Size; ++i) 742 | { 743 | float tmpCoeff = Coefficients[i]; 744 | int tmpIndex = Indices[i]; 745 | Coefficients[i] = prevCoeff; 746 | Indices[i] = prevIndex; 747 | prevCoeff = tmpCoeff; 748 | prevIndex = tmpIndex; 749 | } 750 | } 751 | return index; 752 | } 753 | } 754 | } 755 | 756 | public class VectorX 757 | { 758 | private float[] Data; 759 | 760 | public VectorX(int size) 761 | { 762 | Data = new float[size]; 763 | } 764 | 765 | public int Size => Data.Length; 766 | 767 | public float this[int index] 768 | { 769 | get => Data[index]; 770 | set => Data[index] = value; 771 | } 772 | 773 | public void CopyFrom(VectorX other) 774 | { 775 | System.Array.Copy(other.Data, this.Data, this.Size); 776 | } 777 | 778 | public static void Sub(ref VectorX result, VectorX a, VectorX b) 779 | { 780 | for (int i = 0; i < a.Size; i++) 781 | { 782 | result[i] = a[i] - b[i]; 783 | } 784 | } 785 | 786 | public static float Dot(VectorX a, VectorX b) 787 | { 788 | float sum = 0; 789 | for (int i = 0; i < a.Size; i++) 790 | { 791 | sum += a[i] * b[i]; 792 | } 793 | return sum; 794 | } 795 | 796 | public static void MulAdd(VectorX outVec, VectorX v, float a, VectorX b) 797 | { 798 | for (int i = 0; i < v.Size; ++i) 799 | { 800 | outVec[i] = v[i] * a + b[i]; 801 | } 802 | } 803 | } 804 | } 805 | } -------------------------------------------------------------------------------- /Editor/GITweaksSeamFixer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f7f8370e9ea5a964894d4359f325048e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GITweaksSettingsWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using UnityEditor; 6 | using UnityEditor.SceneManagement; 7 | using UnityEngine; 8 | using UnityEngine.Rendering; 9 | using UnityEngine.SceneManagement; 10 | using UnityEngine.UIElements; 11 | 12 | namespace GITweaks 13 | { 14 | public enum GITweak 15 | { 16 | Logging, 17 | ClickableLightmapCharts, 18 | BakedTransmissionViewModes, 19 | BetterLDAInspector, 20 | LightmapFlagsDropdown, 21 | AutomaticEmbeddedLightingSettings, 22 | BetterLightingSettingsDefaults, 23 | NewSkyboxButton, 24 | LightmapPreviewDropdown, 25 | LightmappedToProbeLit, 26 | SharedLODGroupComponents, 27 | OptimizeLightmapSizes, 28 | SeamFixes, 29 | } 30 | 31 | public class GITweaksSettingsWindow : EditorWindow 32 | { 33 | /* TODO: 34 | X Toggles for each tweak 35 | - Lighting settings template override 36 | - Easily switch between lighting settings 37 | - Probe placement projection guide 38 | - Shadow only debug view 39 | X Seam stitching across meshes 40 | X Dropdown for lightmap index in preview window 41 | X Atlassing post bake optim 42 | X Change lightmapped renderer to probe lit without needing rebake 43 | X Create default skybox button 44 | X Transparency view mode 45 | X Click to highlight object in lightmap preview window 46 | X Show lightmap flags in inspector for material 47 | X View all renderers by receive GI mode 48 | X Auto GPU lightmapper selection + no prioritize view 49 | X Auto embedded lighting settings 50 | X Move LODs to overlap on lightmap 51 | X Better LDA inspector 52 | */ 53 | 54 | [MenuItem("Tools/GI Tweaks/Settings")] 55 | public static void ShowExample() 56 | { 57 | GITweaksSettingsWindow wnd = GetWindow(); 58 | wnd.minSize = new Vector2(350, 330); 59 | wnd.titleContent = new GUIContent("GI Tweaks Settings"); 60 | } 61 | 62 | [MenuItem("Tools/GI Tweaks/Bake Lighting")] 63 | public static void BakeLighting() 64 | { 65 | Lightmapping.BakeAsync(); 66 | } 67 | 68 | [MenuItem("Tools/GI Tweaks/Bake Reflection Probes")] 69 | public static void BakeReflectionProbes() 70 | { 71 | typeof(Lightmapping) 72 | .GetMethod("BakeAllReflectionProbesSnapshots", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic) 73 | .Invoke(null, new object[0]); 74 | } 75 | 76 | [MenuItem("Tools/GI Tweaks/Open Lightmap Preview")] 77 | public static void OpenLightmapPreview() 78 | { 79 | var type = Type.GetType("UnityEditor.LightmapPreviewWindow, UnityEditor"); 80 | var window = EditorWindow.CreateInstance(type) as EditorWindow; 81 | type.GetField("m_LightmapIndex", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(window, 0); 82 | window.minSize = new Vector2(360, 390); 83 | window.Show(); 84 | } 85 | 86 | private static readonly Dictionary defaultValues = new Dictionary() 87 | { 88 | { GITweak.Logging, true }, 89 | { GITweak.ClickableLightmapCharts, true }, 90 | { GITweak.BakedTransmissionViewModes, true }, 91 | { GITweak.BetterLDAInspector, true }, 92 | { GITweak.LightmapFlagsDropdown, true }, 93 | { GITweak.AutomaticEmbeddedLightingSettings, true }, 94 | { GITweak.BetterLightingSettingsDefaults, true }, 95 | { GITweak.NewSkyboxButton, true }, 96 | { GITweak.LightmapPreviewDropdown, true }, 97 | { GITweak.LightmappedToProbeLit, true }, 98 | { GITweak.OptimizeLightmapSizes, false }, 99 | { GITweak.SharedLODGroupComponents, true }, 100 | { GITweak.SeamFixes, true }, 101 | }; 102 | 103 | public static PrefFloat LightmapOptimizationTargetCoverage = new PrefFloat("LightmapOptimization.TargetCoverage", 0.85f); 104 | public static PrefInt LightmapOptimizationMinLightmapSize = new PrefInt("LightmapOptimization.MinLightmapSize", 32); 105 | 106 | bool tweakTogglesHeader = true; 107 | bool lightmapOptimizationHeader = true; 108 | Vector2 scrollPosition = Vector2.zero; 109 | 110 | private void ShowTweakToggle(GITweak tweak, string label) 111 | { 112 | string key = $"GITweaks.{Enum.GetName(typeof(GITweak), tweak)}"; 113 | bool val = EditorPrefs.GetBool(key, defaultValues[tweak]); 114 | EditorGUI.BeginChangeCheck(); 115 | val = GUILayout.Toggle(val, label); 116 | if (EditorGUI.EndChangeCheck()) 117 | { 118 | EditorPrefs.SetBool(key, val); 119 | } 120 | 121 | if (val && IsIncompatible(tweak)) 122 | { 123 | EditorGUILayout.HelpBox("This setting is incompatible with Bakery! It will have no effect in scenes that are baked with Bakery.", MessageType.Warning); 124 | } 125 | } 126 | 127 | public void OnGUI() 128 | { 129 | using var _ = new EditorGUILayout.ScrollViewScope(scrollPosition); 130 | 131 | if (EditorGUILayout.LinkButton("Open feature overview")) 132 | Application.OpenURL("https://github.com/pema99/GITweaks/blob/master/README.md#current-features"); 133 | 134 | tweakTogglesHeader = EditorGUILayout.BeginFoldoutHeaderGroup(tweakTogglesHeader, "Tweak toggles"); 135 | if (tweakTogglesHeader) 136 | { 137 | ShowTweakToggle(GITweak.Logging, "Enable console logging"); 138 | ShowTweakToggle(GITweak.ClickableLightmapCharts, "Clickable charts in Lightmap Preview Window"); 139 | ShowTweakToggle(GITweak.LightmapPreviewDropdown, "Show lightmap dropdown in Lightmap Preview Window"); 140 | ShowTweakToggle(GITweak.BetterLDAInspector, "Better Lighting Data asset inspector"); 141 | ShowTweakToggle(GITweak.LightmapFlagsDropdown, "Show \"Lightmap Flags\" dropdown in material inspector"); 142 | ShowTweakToggle(GITweak.AutomaticEmbeddedLightingSettings, "Use embedded Lighting Settings asset for new scenes"); 143 | ShowTweakToggle(GITweak.BetterLightingSettingsDefaults, "Default to GPU lightmapper and no view prioritization"); 144 | ShowTweakToggle(GITweak.NewSkyboxButton, "Show New and Clone buttons for skybox materials"); 145 | 146 | EditorGUI.BeginChangeCheck(); 147 | ShowTweakToggle(GITweak.BakedTransmissionViewModes, "Scene view modes for Baked Transmission"); 148 | if (EditorGUI.EndChangeCheck()) 149 | { 150 | if (IsEnabled(GITweak.BakedTransmissionViewModes)) 151 | GITweaksViewModes.Init(); 152 | else 153 | GITweaksViewModes.Deinit(); 154 | } 155 | 156 | ShowTweakToggle(GITweak.SeamFixes, "Apply seam fixes after baking"); 157 | ShowTweakToggle(GITweak.LightmappedToProbeLit, "Allow converting lightmapped renderers to probe-lit"); 158 | ShowTweakToggle(GITweak.SharedLODGroupComponents, "Apply LOD group lightmap sharing after baking"); 159 | ShowTweakToggle(GITweak.OptimizeLightmapSizes, "Optimize lightmap sizes after baking"); 160 | } 161 | EditorGUILayout.EndFoldoutHeaderGroup(); 162 | 163 | using (new EditorGUI.DisabledScope(!IsEnabled(GITweak.OptimizeLightmapSizes))) 164 | { 165 | lightmapOptimizationHeader = EditorGUILayout.BeginFoldoutHeaderGroup(lightmapOptimizationHeader, "Lightmap size optimization"); 166 | if (lightmapOptimizationHeader) 167 | { 168 | LightmapOptimizationTargetCoverage.value = EditorGUILayout.Slider("Target coverage %", LightmapOptimizationTargetCoverage * 100.0f, 0, 100) / 100.0f; 169 | var sizes = Enumerable.Range(4, 8).Select(x => 2 << x); 170 | 171 | LightmapOptimizationMinLightmapSize.value = EditorGUILayout.IntPopup( 172 | "Minimum Lightmap Size", 173 | LightmapOptimizationMinLightmapSize, 174 | sizes.Select(x => x.ToString()).ToArray(), 175 | sizes.ToArray()); 176 | } 177 | EditorGUILayout.EndFoldoutHeaderGroup(); 178 | } 179 | } 180 | 181 | public static bool IsEnabled(GITweak tweak) 182 | { 183 | string key = $"GITweaks.{Enum.GetName(typeof(GITweak), tweak)}"; 184 | return EditorPrefs.GetBool(key, defaultValues[tweak]); 185 | } 186 | 187 | public static bool ProjectUsesBakery() 188 | { 189 | #if BAKERY_INCLUDED 190 | return true; 191 | #else 192 | return false; 193 | #endif 194 | } 195 | 196 | public static bool IsIncompatible(GITweak tweak) 197 | { 198 | if (!ProjectUsesBakery()) 199 | return false; 200 | 201 | // Bakery has no traditional LDA to edit 202 | if (tweak == GITweak.LightmappedToProbeLit) return true; 203 | 204 | if (tweak == GITweak.OptimizeLightmapSizes) return true; 205 | 206 | if (tweak == GITweak.SharedLODGroupComponents) return true; 207 | 208 | return false; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Editor/GITweaksSettingsWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6f018de2d90568045b71675c99561594 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GITweaksTexturePacker.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | /* 6 | This file is a port of texture_packer by Coeuvre Wong. Original license text: 7 | 8 | The MIT License (MIT) 9 | 10 | Copyright (c) 2014 Coeuvre Wong 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | */ 30 | 31 | namespace GITweaks 32 | { 33 | public class GITweaksTexturePacker 34 | { 35 | // Skyline 36 | struct Skyline 37 | { 38 | public int x; 39 | public int y; 40 | public int w; 41 | 42 | public int left => x; 43 | public int right => x + w - 1; 44 | } 45 | 46 | private RectInt border; 47 | private List skylines; 48 | private int padding; 49 | private int extrusion; 50 | 51 | public GITweaksTexturePacker(int maxWidth, int maxHeight, int padding, int extrusion) 52 | { 53 | border = new RectInt(0, 0, maxWidth, maxHeight); 54 | skylines = new List() 55 | { 56 | new Skyline 57 | { 58 | x = 0, 59 | y = 0, 60 | w = maxWidth 61 | } 62 | }; 63 | this.padding = padding; 64 | this.extrusion = extrusion; 65 | } 66 | 67 | private bool CanPut(int i, int w, int h, out RectInt rect) 68 | { 69 | rect = new RectInt(skylines[i].x, 0, w, h); 70 | int widthLeft = rect.width; 71 | while (true) 72 | { 73 | if (i >= skylines.Count) 74 | return false; 75 | 76 | rect.y = Mathf.Max(rect.y, skylines[i].y); 77 | if (!border.Contains(rect)) 78 | { 79 | return false; 80 | } 81 | if (skylines[i].w >= widthLeft) 82 | { 83 | return true; 84 | } 85 | widthLeft -= skylines[i].w; 86 | i++; 87 | } 88 | } 89 | 90 | private bool FindSkyline(int w, int h, out int resultIndex, out RectInt resultRect) 91 | { 92 | int bottom = int.MaxValue; 93 | int width = int.MaxValue; 94 | int index = -1; 95 | RectInt rect = new RectInt(0, 0, 0, 0); 96 | 97 | // keep the `bottom` and `width` as small as possible 98 | for (int i = 0; i < skylines.Count; i++) 99 | { 100 | if (CanPut(i, w, h, out var r)) 101 | { 102 | if (r.Bottom() < bottom || (r.Bottom() == bottom && skylines[i].w < width)) 103 | { 104 | bottom = r.Bottom(); 105 | width = skylines[i].w; 106 | index = i; 107 | rect = r; 108 | } 109 | } 110 | } 111 | 112 | resultIndex = index; 113 | resultRect = rect; 114 | return index >= 0; 115 | } 116 | 117 | private void Split(int index, RectInt rect) 118 | { 119 | var skyline = new Skyline 120 | { 121 | x = rect.x, 122 | y = rect.Bottom() + 1, 123 | w = rect.width, 124 | }; 125 | 126 | skylines.Insert(index, skyline); 127 | 128 | int i = index + 1; 129 | while (i < skylines.Count) 130 | { 131 | if (skylines[i].left <= skylines[i - 1].right) 132 | { 133 | int shrink = skylines[i - 1].right - skylines[i].left + 1; 134 | if (skylines[i].w <= shrink) 135 | { 136 | skylines.RemoveAt(i); 137 | } 138 | else 139 | { 140 | var s = skylines[i]; 141 | s.x += shrink; 142 | s.w -= shrink; 143 | skylines[i] = s; 144 | break; 145 | } 146 | } 147 | else 148 | { 149 | break; 150 | } 151 | } 152 | } 153 | 154 | private void Merge() 155 | { 156 | int i = 1; 157 | while (i < skylines.Count) 158 | { 159 | if (skylines[i - 1].y == skylines[i].y) 160 | { 161 | var s = skylines[i - 1]; 162 | s.w += skylines[i].w; 163 | skylines[i - 1] = s; 164 | 165 | skylines.RemoveAt(i); 166 | i -= 1; 167 | } 168 | i += 1; 169 | } 170 | } 171 | 172 | public bool Pack(int width, int height, out RectInt frame) 173 | { 174 | width += padding + extrusion * 2; 175 | height += padding + extrusion * 2; 176 | 177 | if (FindSkyline(width, height, out int i, out RectInt rect)) 178 | { 179 | Split(i, rect); 180 | Merge(); 181 | 182 | rect.width -= padding + extrusion * 2; 183 | rect.height -= padding + extrusion * 2; 184 | 185 | frame = rect; 186 | return true; 187 | } 188 | 189 | frame = default; 190 | return false; 191 | } 192 | 193 | public bool CanPack(int width, int height) 194 | { 195 | if (FindSkyline(width + padding + extrusion * 2, height + padding + extrusion * 2, out _, out var rect)) 196 | { 197 | Skyline skyline = new Skyline 198 | { 199 | x = rect.x, 200 | y = rect.yMax + 1, 201 | w = rect.width, 202 | }; 203 | 204 | return skyline.right <= border.yMax && skyline.y <= border.yMin; 205 | } 206 | return false; 207 | } 208 | } 209 | } -------------------------------------------------------------------------------- /Editor/GITweaksTexturePacker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0970ae848032c294ea9ce015b93a4caf 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GITweaksUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using UnityEngine.Experimental.Rendering; 6 | using UnityEngine.Rendering; 7 | 8 | namespace GITweaks 9 | { 10 | public static class GITweaksUtils 11 | { 12 | public static Rect GetSTRect(MeshRenderer mr) 13 | { 14 | return new Rect(mr.lightmapScaleOffset.z, mr.lightmapScaleOffset.w, mr.lightmapScaleOffset.x, mr.lightmapScaleOffset.y); 15 | } 16 | 17 | public static Rect GetSTRect(Terrain tr) 18 | { 19 | return new Rect(tr.lightmapScaleOffset.z, tr.lightmapScaleOffset.w, tr.lightmapScaleOffset.x, tr.lightmapScaleOffset.y); 20 | } 21 | 22 | public static Rect GetSTRect(Component c) 23 | { 24 | if (c is MeshRenderer mr) return GetSTRect(mr); 25 | else return GetSTRect((Terrain)c); 26 | } 27 | 28 | public static bool GetMeshAndUVChannel(MeshRenderer renderer, out Vector2[] uvs, out int uvChannel) 29 | { 30 | uvs = null; 31 | uvChannel = -1; 32 | 33 | if (renderer.additionalVertexStreams != null) 34 | { 35 | if (renderer.additionalVertexStreams.HasVertexAttribute(VertexAttribute.TexCoord1)) 36 | { 37 | uvs = renderer.additionalVertexStreams.uv2; 38 | uvChannel = 1; 39 | } 40 | else if (renderer.additionalVertexStreams.HasVertexAttribute(VertexAttribute.TexCoord0)) 41 | { 42 | uvs = renderer.additionalVertexStreams.uv; 43 | uvChannel = 0; 44 | } 45 | } 46 | 47 | if (uvChannel == -1) 48 | { 49 | var mesh = renderer.GetComponent().sharedMesh; 50 | if (mesh.HasVertexAttribute(VertexAttribute.TexCoord1)) 51 | { 52 | uvs = mesh.uv2; 53 | uvChannel = 1; 54 | } 55 | else if (mesh.HasVertexAttribute(VertexAttribute.TexCoord0)) 56 | { 57 | uvs = mesh.uv; 58 | uvChannel = 0; 59 | } 60 | } 61 | 62 | return uvChannel != -1; 63 | } 64 | 65 | public static Rect ComputeUVBounds(MeshRenderer mr) 66 | { 67 | GetMeshAndUVChannel(mr, out Vector2[] verts, out _); 68 | Vector2 minVert = Vector3.positiveInfinity, maxVert = Vector3.negativeInfinity; 69 | foreach (Vector3 vert in verts) 70 | { 71 | if (vert.x < minVert.x) 72 | minVert.x = vert.x; 73 | if (vert.y < minVert.y) 74 | minVert.y = vert.y; 75 | if (vert.x > maxVert.x) 76 | maxVert.x = vert.x; 77 | if (vert.y > maxVert.y) 78 | maxVert.y = vert.y; 79 | } 80 | Rect uvBounds = new Rect(minVert, maxVert - minVert); 81 | return uvBounds; 82 | } 83 | 84 | public static Rect ComputeUVBounds(Component c) 85 | { 86 | if (c is MeshRenderer mr) 87 | return ComputeUVBounds(mr); 88 | else 89 | return new Rect(0, 0, 1, 1); 90 | } 91 | 92 | public static Rect STRectToPixelRect(MeshRenderer mr, Rect stRect) 93 | { 94 | Rect uvBounds = ComputeUVBounds(mr); 95 | 96 | // Scale ST rect to pixel rect 97 | stRect.x += uvBounds.x * stRect.width; 98 | stRect.y += uvBounds.y * stRect.height; 99 | stRect.width *= uvBounds.width; 100 | stRect.height *= uvBounds.height; 101 | 102 | return stRect; 103 | } 104 | 105 | public static Rect STRectToPixelRect(Rect uvBounds, Rect stRect) 106 | { 107 | // Scale ST rect to pixel rect 108 | stRect.x += uvBounds.x * stRect.width; 109 | stRect.y += uvBounds.y * stRect.height; 110 | stRect.width *= uvBounds.width; 111 | stRect.height *= uvBounds.height; 112 | 113 | return stRect; 114 | } 115 | 116 | public static void OffsetLightmapSTByPixelRectOffset(Rect uvBounds, ref Vector4 lightmapST) 117 | { 118 | lightmapST.z -= uvBounds.x * lightmapST.x; 119 | lightmapST.w -= uvBounds.y * lightmapST.y; 120 | } 121 | 122 | public static void OffsetLightmapSTByPixelRectOffset(MeshRenderer mr, ref Vector4 lightmapST) 123 | { 124 | // Scale to uv bounds 125 | if (mr != null) 126 | { 127 | GetMeshAndUVChannel(mr, out Vector2[] verts, out _); 128 | Vector2 minVert = Vector3.positiveInfinity; 129 | foreach (Vector3 vert in verts) 130 | { 131 | if (vert.x < minVert.x) 132 | minVert.x = vert.x; 133 | if (vert.y < minVert.y) 134 | minVert.y = vert.y; 135 | } 136 | // Scale ST rect to pixel rect 137 | lightmapST.z -= minVert.x * lightmapST.x; 138 | lightmapST.w -= minVert.y * lightmapST.y; 139 | } 140 | } 141 | 142 | public static void GetSTAndPixelRect(Component c, out Rect stRect, out Rect pixelRect) 143 | { 144 | if (c is MeshRenderer mr) 145 | { 146 | stRect = GetSTRect(mr); 147 | pixelRect = STRectToPixelRect(mr, stRect); 148 | } 149 | else 150 | { 151 | stRect = GetSTRect((Terrain)c); 152 | pixelRect = stRect; 153 | } 154 | } 155 | 156 | public static int Top(this RectInt r) => r.y; 157 | public static int Bottom(this RectInt r) => r.y + r.height - 1; 158 | public static int Left(this RectInt r) => r.x; 159 | public static int Right(this RectInt r) => r.x + r.width - 1; 160 | public static bool Contains(this RectInt self, RectInt other) 161 | { 162 | return self.Left() <= other.Left() 163 | && self.Right() >= other.Right() 164 | && self.Top() <= other.Top() 165 | && self.Bottom() >= other.Bottom(); 166 | } 167 | public static Rect ToRect(this RectInt self) 168 | { 169 | return new Rect(self.position, self.size); 170 | } 171 | public static RectInt ToRectInt(this Rect self) 172 | { 173 | return new RectInt(Vector2Int.CeilToInt(self.position), Vector2Int.CeilToInt(self.size)); 174 | } 175 | 176 | private static int DivUp(int x, int y) => (x + y - 1) / y; 177 | 178 | private static ComputeShader copyFractionalShader = null; 179 | private static int copyFractionalShaderKernel = -1; 180 | public static void CopyFractional(Texture2D from, Rect fromRect, RenderTexture to, Vector2Int toPosition, bool gammaToLinear) 181 | { 182 | if (copyFractionalShader == null) 183 | copyFractionalShader = Resources.Load("CopyFractional"); 184 | if (copyFractionalShaderKernel < 0) 185 | copyFractionalShaderKernel = copyFractionalShader.FindKernel("CopyFractional"); 186 | 187 | copyFractionalShader.SetTexture(copyFractionalShaderKernel, "_Input", from); 188 | copyFractionalShader.SetTexture(copyFractionalShaderKernel, "_Output", to); 189 | copyFractionalShader.SetVector("_SrcRect", new Vector4(fromRect.width, fromRect.height, fromRect.x, fromRect.y)); 190 | copyFractionalShader.SetInt("_DstX", toPosition.x); 191 | copyFractionalShader.SetInt("_DstY", toPosition.y); 192 | copyFractionalShader.SetInt("_GammaToLinear", gammaToLinear ? 1 : 0); 193 | 194 | copyFractionalShader.GetKernelThreadGroupSizes(copyFractionalShaderKernel, out uint kx, out uint ky, out _); 195 | copyFractionalShader.Dispatch( 196 | copyFractionalShaderKernel, 197 | DivUp(Mathf.CeilToInt(fromRect.width), (int)kx), 198 | DivUp(Mathf.CeilToInt(fromRect.height), (int)ky), 199 | 1); 200 | } 201 | 202 | public static Texture2D GetRWTextureCopy(Texture2D texture, GraphicsFormat format) 203 | { 204 | RenderTexture temp = RenderTexture.GetTemporary(texture.width, texture.height, 0, format); 205 | Graphics.Blit(texture, temp); 206 | 207 | Texture2D textureCopy = new Texture2D(texture.width, texture.height, format, TextureCreationFlags.None) { name = $"Copy of {texture.name}"}; 208 | textureCopy.wrapMode = TextureWrapMode.Clamp; 209 | var prevRT = RenderTexture.active; 210 | RenderTexture.active = temp; 211 | textureCopy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); 212 | RenderTexture.active = prevRT; 213 | RenderTexture.ReleaseTemporary(temp); 214 | 215 | return textureCopy; 216 | } 217 | 218 | public static void RenderTextureToTexture2D(RenderTexture src, Texture2D dst) 219 | { 220 | var prevRT = RenderTexture.active; 221 | RenderTexture.active = src; 222 | dst.ReadPixels(new Rect(0, 0, src.width, src.height), 0, 0); 223 | RenderTexture.active = prevRT; 224 | } 225 | 226 | public static Texture2D RenderTextureToTexture2D(RenderTexture src) 227 | { 228 | Texture2D result = new Texture2D(src.width, src.height, src.graphicsFormat, TextureCreationFlags.None) { name = $"Conversion of {src.name}" }; 229 | var prevRT = RenderTexture.active; 230 | RenderTexture.active = src; 231 | result.ReadPixels(new Rect(0, 0, src.width, src.height), 0, 0); 232 | RenderTexture.active = prevRT; 233 | return result; 234 | } 235 | 236 | public static void Texture2DToRenderTexture(Texture2D src, RenderTexture dst) 237 | { 238 | Graphics.Blit(src, dst); 239 | } 240 | 241 | public static void CopyImporterSettingsAndReimport(Texture2D template, string dstPath) 242 | { 243 | var srcImporter = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(template)); 244 | var dstImporter = AssetImporter.GetAtPath(dstPath); 245 | 246 | var srcImporterObj = new SerializedObject(srcImporter); 247 | var dstImporterObj = new SerializedObject(dstImporter); 248 | 249 | var srcIter = srcImporterObj.GetIterator(); 250 | 251 | while (srcIter.Next(true)) 252 | { 253 | dstImporterObj.CopyFromSerializedProperty(srcIter); 254 | } 255 | 256 | dstImporterObj.ApplyModifiedProperties(); 257 | dstImporter.SaveAndReimport(); 258 | } 259 | 260 | public static bool IsLightmapped(MeshRenderer mr) 261 | { 262 | return mr.lightmapIndex >= 0 && mr.lightmapIndex < 65534; 263 | } 264 | 265 | public static bool IsRealtimeLightmapped(MeshRenderer mr) 266 | { 267 | return mr.realtimeLightmapIndex >= 0 && mr.realtimeLightmapIndex < 65534; 268 | } 269 | 270 | public static List GetBakeryLightmapStorages() 271 | { 272 | var ty = System.Type.GetType("ftLightmapsStorage, BakeryRuntimeAssembly"); 273 | if (ty == null) 274 | return new List(); 275 | 276 | var field = ty.GetField("lastBakeTime"); 277 | if (field == null) 278 | return new List(); 279 | 280 | var objs = Object.FindObjectsByType(ty, FindObjectsSortMode.None); 281 | List result = new List(); 282 | foreach (var obj in objs) 283 | { 284 | int lastBakeTime = (int)field.GetValue(obj); 285 | if (lastBakeTime != 0 && obj) 286 | result.Add(obj); 287 | } 288 | return result; 289 | } 290 | 291 | public static bool IsCurrentSceneBakedWithBakery() 292 | { 293 | return GetBakeryLightmapStorages().Count != 0; 294 | } 295 | 296 | public static void RefreshLDA() 297 | { 298 | #if BAKERY_INCLUDED 299 | var lms = GetBakeryLightmapStorages(); 300 | if (lms.Count == 0) 301 | { 302 | Lightmapping.lightingDataAsset = Lightmapping.lightingDataAsset; 303 | return; 304 | } 305 | 306 | LightmapSettings.lightmaps = new LightmapData[0]; 307 | 308 | var ty = System.Type.GetType("ftLightmapsStorage, BakeryRuntimeAssembly"); 309 | var awake = ty.GetMethod("Awake", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); 310 | 311 | foreach (var lm in lms) 312 | { 313 | awake.Invoke(lm, new object[0]); 314 | } 315 | #else 316 | Lightmapping.lightingDataAsset = Lightmapping.lightingDataAsset; 317 | #endif 318 | } 319 | } 320 | 321 | public class PrefInt 322 | { 323 | int Value; 324 | string Name; 325 | bool Loaded; 326 | 327 | public PrefInt(string name, int value) 328 | { 329 | Name = $"GITweaks.{name}"; 330 | Loaded = false; 331 | Value = value; 332 | } 333 | 334 | private void Load() 335 | { 336 | if (Loaded) 337 | return; 338 | 339 | Loaded = true; 340 | Value = EditorPrefs.GetInt(Name, Value); 341 | } 342 | 343 | public int value 344 | { 345 | get { Load(); return Value; } 346 | set 347 | { 348 | Load(); 349 | if (Value == value) 350 | return; 351 | Value = value; 352 | EditorPrefs.SetInt(Name, value); 353 | } 354 | } 355 | 356 | public static implicit operator int(PrefInt s) 357 | { 358 | return s.value; 359 | } 360 | } 361 | 362 | public class PrefFloat 363 | { 364 | float Value; 365 | string Name; 366 | bool Loaded; 367 | 368 | public PrefFloat(string name, float value) 369 | { 370 | Name = $"GITweaks.{name}"; 371 | Loaded = false; 372 | Value = value; 373 | } 374 | 375 | private void Load() 376 | { 377 | if (Loaded) 378 | return; 379 | 380 | Loaded = true; 381 | Value = EditorPrefs.GetFloat(Name, Value); 382 | } 383 | 384 | public float value 385 | { 386 | get { Load(); return Value; } 387 | set 388 | { 389 | Load(); 390 | if (Value == value) 391 | return; 392 | Value = value; 393 | EditorPrefs.SetFloat(Name, value); 394 | } 395 | } 396 | 397 | public static implicit operator float(PrefFloat s) 398 | { 399 | return s.value; 400 | } 401 | } 402 | 403 | public class PrefBool 404 | { 405 | bool Value; 406 | string Name; 407 | bool Loaded; 408 | 409 | public PrefBool(string name, bool value) 410 | { 411 | Name = $"GITweaks.{name}"; 412 | Loaded = false; 413 | Value = value; 414 | } 415 | 416 | private void Load() 417 | { 418 | if (Loaded) 419 | return; 420 | 421 | Loaded = true; 422 | Value = EditorPrefs.GetBool(Name, Value); 423 | } 424 | 425 | public bool value 426 | { 427 | get { Load(); return Value; } 428 | set 429 | { 430 | Load(); 431 | if (Value == value) 432 | return; 433 | Value = value; 434 | EditorPrefs.SetBool(Name, value); 435 | } 436 | } 437 | 438 | public static implicit operator bool(PrefBool s) 439 | { 440 | return s.value; 441 | } 442 | } 443 | 444 | public class PrefEnum 445 | where TEnum : System.Enum 446 | { 447 | TEnum Value; 448 | string Name; 449 | bool Loaded; 450 | 451 | public PrefEnum(string name, TEnum value) 452 | { 453 | Name = $"GITweaks.{name}"; 454 | Loaded = false; 455 | Value = value; 456 | } 457 | 458 | private void Load() 459 | { 460 | if (Loaded) 461 | return; 462 | 463 | Loaded = true; 464 | Value = (TEnum)(object)EditorPrefs.GetInt(Name, (int)(object)Value); 465 | } 466 | 467 | public TEnum value 468 | { 469 | get { Load(); return Value; } 470 | set 471 | { 472 | Load(); 473 | if (EqualityComparer.Default.Equals(Value, value)) 474 | return; 475 | Value = value; 476 | EditorPrefs.SetInt(Name, (int)(object)value); 477 | } 478 | } 479 | 480 | public static implicit operator TEnum(PrefEnum s) 481 | { 482 | return s.value; 483 | } 484 | } 485 | } -------------------------------------------------------------------------------- /Editor/GITweaksUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: db0243be490bfc74d965b3ce7862fccf 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GITweaksViewModes.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using UnityEngine.Rendering; 6 | using UnityEngine.UIElements; 7 | using System.Linq; 8 | using UnityEditor.Overlays; 9 | 10 | namespace GITweaks 11 | { 12 | [InitializeOnLoad] 13 | public static class GITweaksViewModes 14 | { 15 | [Overlay(typeof(SceneView), "Baked Transmission Legend", false)] 16 | public class MyToolButtonOverlay : Overlay, ITransientOverlay 17 | { 18 | private static VisualElement CreateColorSwatch(string label, Color color) 19 | { 20 | var row = new VisualElement() { style = { flexDirection = FlexDirection.Row, marginLeft = 2 } }; 21 | row.AddToClassList("unity-base-field"); 22 | 23 | var swatchContainer = new VisualElement(); 24 | swatchContainer.AddToClassList("unity-base-field__label"); 25 | swatchContainer.AddToClassList("unity-pbr-validation-color-swatch"); 26 | 27 | var colorContent = new VisualElement() { name = "color-content" }; 28 | colorContent.style.backgroundColor = new StyleColor(color); 29 | 30 | swatchContainer.Add(colorContent); 31 | row.Add(swatchContainer); 32 | 33 | var colorLabel = new Label(label) { name = "color-label" }; 34 | colorLabel.AddToClassList("unity-base-field__label"); 35 | row.Add(colorLabel); 36 | return row; 37 | } 38 | 39 | public override VisualElement CreatePanelContent() 40 | { 41 | var root = new VisualElement() { name = "Root" }; 42 | root.Add(CreateColorSwatch("Full RGB transmission", Color.red)); 43 | root.Add(CreateColorSwatch("Alpha/Fade transmission", Color.green)); 44 | root.Add(CreateColorSwatch("Cutout transmission", Color.blue)); 45 | root.Add(CreateColorSwatch("Opaque (Missing _MainTex)", Color.magenta)); 46 | root.Add(CreateColorSwatch("Opaque", Color.white)); 47 | return root; 48 | 49 | } 50 | 51 | public bool visible => SceneView.lastActiveSceneView.cameraMode.name == BakedTransmissionModes; 52 | } 53 | 54 | const string BakedTransmissionModes = "Baked Transmission Modes"; 55 | const string BakedTransmissionTextures = "Baked Transmission Data"; 56 | 57 | static GITweaksViewModes() 58 | { 59 | Init(); 60 | } 61 | 62 | public static void Init() 63 | { 64 | if (!GITweaksSettingsWindow.IsEnabled(GITweak.BakedTransmissionViewModes)) 65 | return; 66 | 67 | var modes = (List)typeof(SceneView) 68 | .GetProperty("userDefinedModes", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static) 69 | .GetValue(null, new object[0]); 70 | 71 | if (!modes.Any(x => x.name == BakedTransmissionModes)) 72 | { 73 | SceneView.AddCameraMode(BakedTransmissionModes, "GI Tweaks"); 74 | } 75 | if (!modes.Any(x => x.name == BakedTransmissionTextures)) 76 | { 77 | SceneView.AddCameraMode(BakedTransmissionTextures, "GI Tweaks"); 78 | } 79 | 80 | SceneView.beforeSceneGui -= RenderCustom; 81 | SceneView.beforeSceneGui += RenderCustom; 82 | } 83 | 84 | public static void Deinit() 85 | { 86 | foreach (SceneView sv in SceneView.sceneViews) 87 | { 88 | sv.cameraMode = SceneView.GetBuiltinCameraMode(DrawCameraMode.Textured); 89 | } 90 | 91 | SceneView.ClearUserDefinedCameraModes(); 92 | } 93 | 94 | enum TransparencyMode 95 | { 96 | RGB, 97 | Alpha, 98 | Cutout, 99 | MissingMainTex, 100 | Opaque, 101 | } 102 | 103 | private static TransparencyMode GetTransparencyMode(Material m, out Texture tex, out float alpha) 104 | { 105 | alpha = m.color.a; 106 | 107 | if (m.HasProperty("_TransparencyLM")) 108 | { 109 | alpha = 1; 110 | tex = m.GetTexture("_TransparencyLM"); 111 | return TransparencyMode.RGB; 112 | } 113 | 114 | // Transparent type 115 | bool cutout = m.renderQueue >= (int)RenderQueue.AlphaTest && m.renderQueue < (int)RenderQueue.Transparent; 116 | bool transparent = m.renderQueue >= (int)RenderQueue.Transparent && m.renderQueue < (int)RenderQueue.Overlay; 117 | 118 | if (!transparent) 119 | { 120 | string renderType = m.GetTag("RenderType", false); 121 | if (m.name.Contains("Transparent") || (m.name.Contains("Tree") && (m.name.Contains("Leaves") || m.name.Contains("Leaf")))) 122 | { 123 | transparent = true; 124 | } 125 | else if (renderType == "GrassBillboard" || renderType == "Transparent" || renderType == "Grass" || renderType == "TreeLeaf") 126 | { 127 | transparent = true; 128 | } 129 | } 130 | 131 | if (!cutout) 132 | { 133 | string renderType = m.GetTag("RenderType", false); 134 | if (renderType == "TransparentCutout" || renderType == "TreeTransparentCutout") 135 | { 136 | cutout = true; 137 | } 138 | else if (m.IsKeywordEnabled("GEOM_TYPE_FROND") || m.IsKeywordEnabled("GEOM_TYPE_LEAF")) 139 | { 140 | cutout = true; 141 | } 142 | } 143 | 144 | if (cutout || transparent) 145 | { 146 | tex = m.mainTexture; 147 | 148 | bool hasMainTexture = m.mainTexture != null; 149 | if (!hasMainTexture) 150 | { 151 | if (m.HasProperty("_MainTex")) 152 | { 153 | hasMainTexture = true; 154 | } 155 | else 156 | { 157 | var shader = m.shader; 158 | int propertyCount = shader.GetPropertyCount(); 159 | for (int i = 0; i < propertyCount; i++) 160 | { 161 | if (shader.GetPropertyType(i) != ShaderPropertyType.Texture) 162 | continue; 163 | 164 | if ((shader.GetPropertyFlags(i) & ShaderPropertyFlags.MainTexture) != 0) 165 | { 166 | hasMainTexture = true; 167 | break; 168 | } 169 | } 170 | } 171 | } 172 | 173 | if (!hasMainTexture) 174 | return TransparencyMode.MissingMainTex; 175 | 176 | if (cutout && (m.HasProperty("_Cutoff") || m.HasProperty("_AlphaTestRef"))) 177 | return TransparencyMode.Cutout; 178 | 179 | if (transparent) 180 | return TransparencyMode.Alpha; 181 | } 182 | 183 | tex = Texture2D.blackTexture; 184 | return TransparencyMode.Opaque; 185 | } 186 | 187 | private static Material viewMat = null; 188 | 189 | private static void RenderCustom(SceneView sceneView) 190 | { 191 | if (sceneView.cameraMode.name != BakedTransmissionModes && sceneView.cameraMode.name != BakedTransmissionTextures) 192 | return; 193 | 194 | bool showTextures = sceneView.cameraMode.name == BakedTransmissionTextures; 195 | 196 | if (Event.current.type != EventType.Repaint) 197 | return; 198 | 199 | sceneView.SetSceneViewShaderReplace(null, ""); 200 | 201 | if (viewMat == null) 202 | viewMat = new Material(Shader.Find("Hidden/pema99/Overlay")); 203 | 204 | var planes = GeometryUtility.CalculateFrustumPlanes(sceneView.camera); 205 | var mrs = Object.FindObjectsByType(FindObjectsSortMode.None); 206 | foreach (var mr in mrs) 207 | { 208 | if (!GeometryUtility.TestPlanesAABB(planes, mr.bounds)) 209 | continue; 210 | 211 | var mode = GetTransparencyMode(mr.sharedMaterial, out var tex, out var alpha); 212 | switch (mode) 213 | { 214 | case TransparencyMode.RGB: viewMat.color = Color.red; break; 215 | case TransparencyMode.Alpha: viewMat.color = Color.green; break; 216 | case TransparencyMode.Cutout: viewMat.color = Color.blue; break; 217 | case TransparencyMode.MissingMainTex: viewMat.color = Color.magenta; break; 218 | case TransparencyMode.Opaque: viewMat.color = Color.white; break; 219 | } 220 | viewMat.mainTexture = tex; 221 | viewMat.SetFloat("_Alpha", alpha); 222 | viewMat.SetInt("_Mode", showTextures ? (mode == TransparencyMode.RGB ? 2 : 1) : 0); 223 | viewMat.SetPass(0); 224 | 225 | Graphics.DrawMeshNow(mr.GetComponent().sharedMesh, mr.localToWorldMatrix); 226 | } 227 | } 228 | } 229 | } -------------------------------------------------------------------------------- /Editor/GITweaksViewModes.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 897438c06ad1d514daa741cb0dfe974c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Resources.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8fea5840a869e174fa8220cfa4ed65cc 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Resources/CopyFractional.compute: -------------------------------------------------------------------------------- 1 | #pragma kernel CopyFractional 2 | 3 | SamplerState sampler_Input; 4 | Texture2D _Input; 5 | 6 | RWTexture2D _Output; 7 | 8 | float4 _SrcRect; 9 | int _DstX; 10 | int _DstY; 11 | 12 | bool _GammaToLinear; 13 | 14 | inline float GammaToLinearSpaceExact(float value) 15 | { 16 | if (value <= 0.04045F) 17 | return value / 12.92F; 18 | else if (value < 1.0F) 19 | return pow((value + 0.055F) / 1.055F, 2.4F); 20 | else 21 | return pow(value, 2.2F); 22 | } 23 | 24 | [numthreads(8,8,1)] 25 | void CopyFractional(uint3 id : SV_DispatchThreadID) 26 | { 27 | int copyWidth = ceil(_SrcRect.x); 28 | int copyHeight = ceil(_SrcRect.y); 29 | 30 | if (id.x >= copyWidth || id.y >= copyHeight) 31 | return; 32 | 33 | // Read input texel at fractional location 34 | int inputWidth, inputHeight; 35 | _Input.GetDimensions(inputWidth, inputHeight); 36 | float2 samplePos = (_SrcRect.zw + id.xy + 0.5) / float2(inputWidth, inputHeight); 37 | float4 sample = _Input.SampleLevel(sampler_Input, samplePos, 0); 38 | 39 | // Gamma correct 40 | if (_GammaToLinear) 41 | { 42 | sample.r = GammaToLinearSpaceExact(sample.r); 43 | sample.g = GammaToLinearSpaceExact(sample.g); 44 | sample.b = GammaToLinearSpaceExact(sample.b); 45 | sample.a = GammaToLinearSpaceExact(sample.a); 46 | } 47 | 48 | // Write to output and integer location (ie. center of pixel) 49 | _Output[uint2(_DstX, _DstY) + id.xy] = sample; 50 | } 51 | -------------------------------------------------------------------------------- /Editor/Resources/CopyFractional.compute.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 169e9b8bcd9e6af42b2f5f54d7c97d62 3 | ComputeShaderImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/Resources/RenderOver.shader: -------------------------------------------------------------------------------- 1 | Shader "Hidden/pema99/Overlay" 2 | { 3 | Properties 4 | { 5 | [MainColor] _Color("Color", Color) = (0,0,0,1) 6 | [MainTexture] _MainTex("MainTex", 2D) = "white" {} 7 | } 8 | SubShader 9 | { 10 | Tags { "Queue" = "Overlay" } 11 | 12 | Pass 13 | { 14 | Offset -1, -1 15 | 16 | CGPROGRAM 17 | #pragma vertex vert 18 | #pragma fragment frag 19 | // make fog work 20 | #pragma multi_compile_fog 21 | 22 | #include "UnityCG.cginc" 23 | 24 | struct appdata 25 | { 26 | float4 vertex : POSITION; 27 | float2 uv : TEXCOORD0; 28 | }; 29 | 30 | struct v2f 31 | { 32 | float2 uv : TEXCOORD0; 33 | float4 vertex : SV_POSITION; 34 | }; 35 | 36 | float4 _Color; 37 | sampler2D _MainTex; 38 | float _Alpha; 39 | int _Mode; 40 | 41 | v2f vert (appdata v) 42 | { 43 | v2f o; 44 | o.vertex = UnityObjectToClipPos(v.vertex); 45 | o.uv = v.uv; 46 | return o; 47 | } 48 | 49 | float4 frag(v2f i) : SV_Target 50 | { 51 | if (_Mode == 2) 52 | { 53 | return float4(tex2D(_MainTex, i.uv).rgb, 1); 54 | } 55 | else if (_Mode == 1) 56 | { 57 | return tex2D(_MainTex, i.uv).a * _Alpha; 58 | } 59 | else 60 | { 61 | return _Color; 62 | } 63 | } 64 | ENDCG 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Editor/Resources/RenderOver.shader.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 77770626711d16945bfbd078931c5450 3 | ShaderImporter: 4 | externalObjects: {} 5 | defaultTextures: [] 6 | nonModifiableTextures: [] 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Resources/RenderUVMask.mat: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!21 &2100000 4 | Material: 5 | serializedVersion: 8 6 | m_ObjectHideFlags: 0 7 | m_CorrespondingSourceObject: {fileID: 0} 8 | m_PrefabInstance: {fileID: 0} 9 | m_PrefabAsset: {fileID: 0} 10 | m_Name: RenderUVMask 11 | m_Shader: {fileID: 4800000, guid: 56748c5a399cf8f4dbeb09c1d86a9e6d, type: 3} 12 | m_Parent: {fileID: 0} 13 | m_ModifiedSerializedProperties: 0 14 | m_ValidKeywords: 15 | - USE_UV1 16 | m_InvalidKeywords: [] 17 | m_LightmapFlags: 4 18 | m_EnableInstancingVariants: 0 19 | m_DoubleSidedGI: 0 20 | m_CustomRenderQueue: -1 21 | stringTagMap: {} 22 | disabledShaderPasses: [] 23 | m_LockedProperties: 24 | m_SavedProperties: 25 | serializedVersion: 3 26 | m_TexEnvs: 27 | - _BumpMap: 28 | m_Texture: {fileID: 0} 29 | m_Scale: {x: 1, y: 1} 30 | m_Offset: {x: 0, y: 0} 31 | - _DetailAlbedoMap: 32 | m_Texture: {fileID: 0} 33 | m_Scale: {x: 1, y: 1} 34 | m_Offset: {x: 0, y: 0} 35 | - _DetailMask: 36 | m_Texture: {fileID: 0} 37 | m_Scale: {x: 1, y: 1} 38 | m_Offset: {x: 0, y: 0} 39 | - _DetailNormalMap: 40 | m_Texture: {fileID: 0} 41 | m_Scale: {x: 1, y: 1} 42 | m_Offset: {x: 0, y: 0} 43 | - _EmissionMap: 44 | m_Texture: {fileID: 0} 45 | m_Scale: {x: 1, y: 1} 46 | m_Offset: {x: 0, y: 0} 47 | - _MainTex: 48 | m_Texture: {fileID: 0} 49 | m_Scale: {x: 1, y: 1} 50 | m_Offset: {x: 0, y: 0} 51 | - _MetallicGlossMap: 52 | m_Texture: {fileID: 0} 53 | m_Scale: {x: 1, y: 1} 54 | m_Offset: {x: 0, y: 0} 55 | - _OcclusionMap: 56 | m_Texture: {fileID: 0} 57 | m_Scale: {x: 1, y: 1} 58 | m_Offset: {x: 0, y: 0} 59 | - _ParallaxMap: 60 | m_Texture: {fileID: 0} 61 | m_Scale: {x: 1, y: 1} 62 | m_Offset: {x: 0, y: 0} 63 | m_Ints: [] 64 | m_Floats: 65 | - _BumpScale: 1 66 | - _Cutoff: 0.5 67 | - _DetailNormalMapScale: 1 68 | - _DstBlend: 0 69 | - _GlossMapScale: 1 70 | - _Glossiness: 0.5 71 | - _GlossyReflections: 1 72 | - _Metallic: 0 73 | - _Mode: 0 74 | - _OcclusionStrength: 1 75 | - _Parallax: 0.02 76 | - _SmoothnessTextureChannel: 0 77 | - _SpecularHighlights: 1 78 | - _SrcBlend: 1 79 | - _UVSec: 0 80 | - _ZWrite: 1 81 | m_Colors: 82 | - _Color: {r: 1, g: 1, b: 1, a: 1} 83 | - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} 84 | m_BuildTextureStacks: [] 85 | -------------------------------------------------------------------------------- /Editor/Resources/RenderUVMask.mat.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: affc6ccf13ed9b64d9779f172a40496e 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 2100000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Resources/RenderUVMask.shader: -------------------------------------------------------------------------------- 1 | Shader "Unlit/RenderUVMask" 2 | { 3 | Properties 4 | { 5 | } 6 | SubShader 7 | { 8 | Pass 9 | { 10 | CGPROGRAM 11 | #pragma vertex vert 12 | #pragma fragment frag 13 | #pragma multi_compile _ USE_UV1 14 | 15 | struct appdata 16 | { 17 | float2 uv : TEXCOORD0; 18 | }; 19 | 20 | struct v2f 21 | { 22 | float4 vertex : SV_POSITION; 23 | }; 24 | 25 | float4 _CandidateST; 26 | int _CandidateIndex; 27 | 28 | v2f vert (appdata v) 29 | { 30 | v2f o; 31 | float2 uv = v.uv * _CandidateST.xy + _CandidateST.zw; 32 | o.vertex = float4(float2(1,-1)*(uv*2-1),0,1); 33 | return o; 34 | } 35 | 36 | float4 frag (v2f i) : SV_Target 37 | { 38 | return _CandidateIndex; 39 | } 40 | ENDCG 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Editor/Resources/RenderUVMask.shader.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 56748c5a399cf8f4dbeb09c1d86a9e6d 3 | ShaderImporter: 4 | externalObjects: {} 5 | defaultTextures: [] 6 | nonModifiableTextures: [] 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Resources/RenderUVMaskConservative.mat: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!21 &2100000 4 | Material: 5 | serializedVersion: 8 6 | m_ObjectHideFlags: 0 7 | m_CorrespondingSourceObject: {fileID: 0} 8 | m_PrefabInstance: {fileID: 0} 9 | m_PrefabAsset: {fileID: 0} 10 | m_Name: RenderUVMaskConservative 11 | m_Shader: {fileID: 4800000, guid: 09e099655fcb1bc46be2c17cb7a9ca93, type: 3} 12 | m_Parent: {fileID: 0} 13 | m_ModifiedSerializedProperties: 0 14 | m_ValidKeywords: 15 | - USE_UV1 16 | m_InvalidKeywords: [] 17 | m_LightmapFlags: 4 18 | m_EnableInstancingVariants: 0 19 | m_DoubleSidedGI: 0 20 | m_CustomRenderQueue: -1 21 | stringTagMap: {} 22 | disabledShaderPasses: [] 23 | m_LockedProperties: 24 | m_SavedProperties: 25 | serializedVersion: 3 26 | m_TexEnvs: 27 | - _BumpMap: 28 | m_Texture: {fileID: 0} 29 | m_Scale: {x: 1, y: 1} 30 | m_Offset: {x: 0, y: 0} 31 | - _DetailAlbedoMap: 32 | m_Texture: {fileID: 0} 33 | m_Scale: {x: 1, y: 1} 34 | m_Offset: {x: 0, y: 0} 35 | - _DetailMask: 36 | m_Texture: {fileID: 0} 37 | m_Scale: {x: 1, y: 1} 38 | m_Offset: {x: 0, y: 0} 39 | - _DetailNormalMap: 40 | m_Texture: {fileID: 0} 41 | m_Scale: {x: 1, y: 1} 42 | m_Offset: {x: 0, y: 0} 43 | - _EmissionMap: 44 | m_Texture: {fileID: 0} 45 | m_Scale: {x: 1, y: 1} 46 | m_Offset: {x: 0, y: 0} 47 | - _MainTex: 48 | m_Texture: {fileID: 0} 49 | m_Scale: {x: 1, y: 1} 50 | m_Offset: {x: 0, y: 0} 51 | - _MetallicGlossMap: 52 | m_Texture: {fileID: 0} 53 | m_Scale: {x: 1, y: 1} 54 | m_Offset: {x: 0, y: 0} 55 | - _OcclusionMap: 56 | m_Texture: {fileID: 0} 57 | m_Scale: {x: 1, y: 1} 58 | m_Offset: {x: 0, y: 0} 59 | - _ParallaxMap: 60 | m_Texture: {fileID: 0} 61 | m_Scale: {x: 1, y: 1} 62 | m_Offset: {x: 0, y: 0} 63 | m_Ints: [] 64 | m_Floats: 65 | - _BumpScale: 1 66 | - _Cutoff: 0.5 67 | - _DetailNormalMapScale: 1 68 | - _DstBlend: 0 69 | - _GlossMapScale: 1 70 | - _Glossiness: 0.5 71 | - _GlossyReflections: 1 72 | - _Metallic: 0 73 | - _Mode: 0 74 | - _OcclusionStrength: 1 75 | - _Parallax: 0.02 76 | - _SmoothnessTextureChannel: 0 77 | - _SpecularHighlights: 1 78 | - _SrcBlend: 1 79 | - _UVSec: 0 80 | - _ZWrite: 1 81 | m_Colors: 82 | - _Color: {r: 1, g: 1, b: 1, a: 1} 83 | - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} 84 | m_BuildTextureStacks: [] 85 | -------------------------------------------------------------------------------- /Editor/Resources/RenderUVMaskConservative.mat.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6d7a57123b0fa974586680450e8ef111 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 2100000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Resources/RenderUVMaskConservative.shader: -------------------------------------------------------------------------------- 1 | Shader "Unlit/RenderUVMask" 2 | { 3 | Properties 4 | { 5 | } 6 | SubShader 7 | { 8 | Pass 9 | { 10 | Conservative True 11 | 12 | CGPROGRAM 13 | #pragma vertex vert 14 | #pragma fragment frag 15 | #pragma multi_compile _ USE_UV1 16 | 17 | struct appdata 18 | { 19 | float2 uv : TEXCOORD0; 20 | }; 21 | 22 | struct v2f 23 | { 24 | float4 vertex : SV_POSITION; 25 | }; 26 | 27 | float4 _CandidateST; 28 | int _CandidateIndex; 29 | 30 | v2f vert (appdata v) 31 | { 32 | v2f o; 33 | float2 uv = v.uv * _CandidateST.xy + _CandidateST.zw; 34 | o.vertex = float4(float2(1,-1)*(uv*2-1),0,1); 35 | return o; 36 | } 37 | 38 | float4 frag (v2f i) : SV_Target 39 | { 40 | return _CandidateIndex; 41 | } 42 | ENDCG 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Editor/Resources/RenderUVMaskConservative.shader.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 09e099655fcb1bc46be2c17cb7a9ca93 3 | ShaderImporter: 4 | externalObjects: {} 5 | defaultTextures: [] 6 | nonModifiableTextures: [] 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/dev.pema.gitweaks.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GITweaks", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:8973f39a683868b42a349f2e1dfb47f4" 6 | ], 7 | "includePlatforms": [ 8 | "Editor" 9 | ], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": true, 13 | "precompiledReferences": [ 14 | "0Harmony_GITweaks.dll" 15 | ], 16 | "autoReferenced": true, 17 | "defineConstraints": [], 18 | "versionDefines": [], 19 | "noEngineReferences": false 20 | } -------------------------------------------------------------------------------- /Editor/dev.pema.gitweaks.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fafe0d6630fe7b74abc46759f8210ce5 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GI Tweaks 2 | For a detailed overview of the features provided, [click here](#current-features). 3 | 4 | This package contains various tools and tweaks for working with global illumination in Unity. Both Unity's builtin progressive lightmapper and the third-party Bakery Lightmapper are supported, though not all tweaks work with Bakery. [Harmony](https://github.com/pardeike/Harmony) is used for making patches. Tested with Unity 2022.3 on the Builtin Render Pipeline. 5 | 6 | All features can be toggled via the settings window in "Tools > GI Tweaks > Settings". 7 | 8 | ## How to install 9 | 10 | #### Option 1 - Unity Package Manager 11 | 12 | 1. Open the package manager (Window > Package Manager). 13 | 2. Press the big plus icon, select "Install package from git URL". 14 | 3. Paste the URL of the repo `https://github.com/pema99/GITweaks.git` (note the `.git` ending) and click install. 15 | 16 | ![image](https://github.com/pema99/GITweaks/assets/11212115/133bdd9c-7f87-4714-8b1f-ed5eece77c95) 17 | 18 | #### Option 2 - VRChat Creator Companion 19 | 20 | For VRChat users, the package can be installed via the VRChat Creator companion: 21 | - [Click here](https://pema99.github.io/vpm/redirect.html). 22 | - When prompted, open the redirect with VCC. 23 | - Add the repository. You can now add GITweaks to your projects as normal. 24 | 25 | #### Option 3 - Via releases 26 | 27 | 1. Download the [latest release](https://github.com/pema99/GITweaks/releases) zip. 28 | 2. Open the package manager (Window > Package Manager). 29 | 3. Press the big plus icon, select "Install package from disk". 30 | 4. Select the downloaded zip file. 31 | 32 | ## Current features 33 | 34 | - [Share lightmap space across all LODs](#share-lightmap-space-across-all-lods) 35 | - [Clickable Lightmap Preview charts](#clickable-lightmap-preview-charts) 36 | - [Lightmap index dropdown in Lightmap Preview window](#lightmap-index-dropdown-in-lightmap-preview-window) 37 | - [Optimize lightmap sizes after baking](#optimize-lightmap-sizes-after-baking) 38 | - [Fix lightmap seams between objects](#fix-lightmap-seams-between-objects) 39 | - [Bulk select renderers](#bulk-select-renderers) 40 | - [Baked Transmission view modes](#baked-transmission-view-modes) 41 | - [Better Lighting Data asset inspector](#better-lighting-data-asset-inspector) 42 | - [Show lightmap flags in default material inspector](#show-lightmap-flags-in-default-material-inspector) 43 | - [Automatic embedded Lighting Settings](#automatic-embedded-lighting-settings) 44 | - [Better default Lighting Settings](#better-default-lighting-settings) 45 | - [Convert lightmapped renderer to probe-lit without rebaking](#convert-lightmapped-renderer-to-probe-lit-without-rebaking) 46 | - [New and Clone buttons for Skybox material](#new-and-clone-buttons-for-skybox-material) 47 | 48 | ### Share lightmap space across all LODs 49 | When using LOD groups, if you use lightmapping for several LOD levels, each LOD level will take up its own space in the lightmap. This tweak adds a script "GI Tweaks Shared LOD" which lets you reuse the same lightmap space for several LOD levels. Simply attach the script to the GameObject that has the LOD group and bake. Unlike some of the other solutions that exist for this, the script will edit the LightingDataAsset stored on disk, meaning you don't need to manually fiddle with lightmap indices, scales and offsets at runtime. 50 | 51 | ![1zWgISsTpp](https://github.com/pema99/GITweaks/assets/11212115/df0ce872-845d-488e-974a-f158ef57ce3d) 52 | 53 | For reference, the same scene baked with the script disabled, and using lightmaps for each LOD level: 54 | 55 | ![image](https://github.com/pema99/GITweaks/assets/11212115/edcfd2e8-f18c-4166-a3c3-97089e749774) 56 | 57 | ### Clickable Lightmap Preview charts 58 | The Lightmap Preview window can highlight the UV chart of the currently selected object. However, you cannot inversely click on a chart to select the corresponding object. This tweak adds that functionality. 59 | 60 | ![ZFvbglRVTT](https://github.com/pema99/GITweaks/assets/11212115/ec36ed87-5bdf-489d-b94d-cbe8c5595bd4) 61 | 62 | ### Lightmap index dropdown in Lightmap Preview window 63 | This tweak adds a dropdown to the Lightmap Preview window that lets you switch between viewing different lightmaps. A convenient shortcut for opening the window has been added under "Tools > GI Tweaks > Open Lightmap Preview". 64 | 65 | Before: 66 | 67 | ![image](https://github.com/pema99/GITweaks/assets/11212115/7bb1490b-7001-463e-9590-6d3499d24ec2) 68 | 69 | After: 70 | 71 | ![image](https://github.com/pema99/GITweaks/assets/11212115/d3f8f39e-9588-4ea2-a5cf-4fa3100910a8) 72 | 73 | ### Optimize lightmap sizes after baking 74 | > Note: This feature does not work with the Bakery lightmapper. 75 | 76 | > Note: This tweak is _not_ enabled by default, and must be enabled in "Tools > GI Tweaks > Settings". 77 | 78 | Unity's builtin Lightmapper has a tendency to produce poorly packed lightmaps in some cases, which leads to wasting VRAM on empty texture space. An example is shown below. 79 | 80 | ![image](https://github.com/pema99/GITweaks/assets/11212115/dec8c317-2360-437a-b76d-e8bbbfae7f0a) 81 | 82 | When a bake is finished with this tweak enabled, the lightmap packing will be re-done, producing a new set of lightmaps, each of which is packed more tightly. These new lightmaps will often be smaller than the original lightmaps, and may be different sizes. Instances in each lightmap will never be resized, so there shouldn't be any noticeable quality difference. Below is the result of using the feature on the lightmap shown above. Before optimization, the scene had a single 512x512 lightmap. After optimization, the scene uses two 256x256 lightmaps - a 2x reduction in VRAM usage: 83 | 84 | ![image](https://github.com/pema99/GITweaks/assets/11212115/157cb1c6-4fac-4d9c-9538-35bd19761ce6) 85 | 86 | The tweak is configurable via two additional settings: 87 | - **Target coverage %** is a threshold determining when lightmap size optimization will be enabled. If less than the specified percentage of lightmaps texels are covered, optimization will be done. This should usually be set pretty high. 88 | - **Minimum Lightmap Size** determines the minimum allowed lightmap size after optimization. If you want to avoid many small lightmaps, increase this value. If you set it too high, no optimization will be done. 89 | 90 | ![image](https://github.com/pema99/GITweaks/assets/11212115/9e73b53d-d806-4340-a2ca-0d86fc2cfd66) 91 | 92 | ### Fix lightmap seams between objects 93 | When baking scenes containing surfaces built of multiple modular pieces, you will often get seams where the pieces meet, due to differences in bilinear sampling. Unity has a solution for [fixing seams](https://docs.unity3d.com/Manual/Lightmapping-SeamStitching.html) on a single renderer, but nothing to fix seams between different renderers, which is typically worked around by manually merging meshes. This tweak provides some tools for mitigating seams between different renderers. 94 | 95 | It can be used in 2 primary ways: As a volume, and as a targeted component. The gif below shows an example of using the "GI Tweaks Seam Fix Volume" variant to fix a seam. Volume components can be quickly created with the right click context menu / GameObject menu. 96 | 97 | ![s2o3BE4np4](https://github.com/pema99/GITweaks/assets/11212115/c0a9e89e-2693-40ae-a0d5-4284f0358e8a) 98 | 99 | ![image](https://github.com/pema99/GITweaks/assets/11212115/73b7bce1-09d5-40ce-a6f2-5797b0c5d79f) 100 | 101 | The volume component will only attempt to fix seams occuring within the volume. Alternatively, you can use the targeted "GI Tweaks Seam Fix" component, shown below. This component is applied directly onto the object exhibiting seams, and should be provided a list of other renderers contributing to the seams. In this example, the component is applied to a cube, and pointed to another cube, in effect fixing the seam between the 2 cubes. 102 | 103 | ![l42h87mouz](https://github.com/pema99/GITweaks/assets/11212115/440f165f-2256-44b4-bd8a-08afc3d7cc17) 104 | 105 | The tweak is configurable via some additional settings: 106 | - **Run After Baking** controls whether the seam fix should be applied automatically when baking. 107 | - **Max Surface Angle** is the maximum allowed angle in degrees between 2 surfaces for them to be considered "the same". This is used to prevent fixing intentional seams, such as the corners of a cube. 108 | - **Seam Fix Strength** controls how aggresively the seam fixing algorithm blurs the seam. 109 | - **Max Solver Iteration Count** controls how many iterations the algorithm takes at maximum. Higher numbers may give better quality, but will be slower. 110 | - **Solver Tolerance** is an error threshold which, when reached, will cause the seam fixing algorithm to stop early. 111 | - (Volume variant only) **Renderers To Exclude** is a list of renderers to ignore, even if they overlap the volume. 112 | - (Targeted variant only) **Renderers To Fix Seams With** is a list of renderers to run the seam fixing algorithm for. 113 | 114 | The "Preview fix" and "Reset preview" button can be used to non-destructively preview the result of applying the seam fix. "Apply fix" will permanently modify the lightmap texture on disk. 115 | 116 | > Note: This tweak **only** fixes hard seams due to differences in bilinear filtering. If the lightmaps have perceptually different colors at the seam, this tweak will not fix that. 117 | 118 | > Note: Seam fixing can be expensive, especially with the volume component. Try not to make huge volumes - instead, keep the small and use only where seams occur. 119 | 120 | ### Bulk select renderers 121 | Making bulk lighting-related changes to renderers in a large scene is tedious. This tool provides a simple way to mass-select renderers based on some configurable filters, for the purpose of multi-editing them. Accesible via "Tools > GI Tweaks > Bulk Renderer Selection". 122 | 123 | ![image](https://github.com/pema99/GITweaks/assets/11212115/95754281-4f98-4d1a-a480-542b3a2f7523) 124 | 125 | ### Baked Transmission view modes 126 | The rules for what is considered transmissive/transparent by the builtin lightmapper are somewhat opaque. These added scene view modes allow for easy identification and debugging of transparents. There are 2 modes, both accessible from the scene view toolbar: 127 | 128 | ![image](https://github.com/pema99/GITweaks/assets/11212115/ddd63e87-da58-4183-a756-ef1b47aab180) 129 | 130 | "Baked Transmission Modes" displays what the baker sees as transmissive: 131 | 132 | ![Unity_c35PO3McfI](https://github.com/pema99/GITweaks/assets/11212115/5e7eed73-ac73-4a8a-907e-1d65b4d8ae8a) 133 | 134 | "Baked Transmission Data" displays the actual transmission textures fed to the baker: 135 | 136 | ![uwJ51JYeAA](https://github.com/pema99/GITweaks/assets/11212115/783bedb2-0e4e-46dd-a9b0-826f8c2b6e62) 137 | 138 | ### Better Lighting Data asset inspector 139 | The output of the a bake - the Lighting Data asset - is a black box. The asset's inspector doesn't show any information about the contents. This tweak changes the default inspector to display all the contained data. Warning: Modifying this data can screw up your bake. Any issues should be resolved by simply rebaking, though. 140 | 141 | Before: 142 | 143 | ![image](https://github.com/pema99/GITweaks/assets/11212115/b8d52401-bfb7-4e46-bbbe-e0b1677b8da7) 144 | 145 | After: 146 | 147 | ![image](https://github.com/pema99/GITweaks/assets/11212115/24644bdc-78a5-4b4f-837a-95d13508b562) 148 | 149 | ### Show lightmap flags in default material inspector 150 | Unity has a hidden `MaterialGlobalIlluminationFlags` on each material, which must be set in order for baked emission to work. It is in't shown in the inspector by default. This tweak shows it. 151 | 152 | Before: 153 | 154 | ![zDSlKh9F6x](https://github.com/pema99/GITweaks/assets/11212115/7506060e-6132-46d8-9af8-add9fc2aca3c) 155 | 156 | After: 157 | 158 | ![Om0RfPY6M5](https://github.com/pema99/GITweaks/assets/11212115/e61383fc-b1ea-493f-af58-1be6884d16b6) 159 | 160 | ### Automatic embedded Lighting Settings 161 | When you create a new scene in Unity, it will by default have no Lighting Settings asset assigned, and you won't be able to edit any settings without creating one. This tweak instead assigns a new embedded Lighting Settings asset which is serialized directly into the scene, letting you immediately modify settings without having to create an additional asset. 162 | 163 | Before: 164 | 165 | ![fJ3RSEK0VC](https://github.com/pema99/GITweaks/assets/11212115/3fe8e37d-1826-4b93-b67c-4b8702df0c41) 166 | 167 | After: 168 | 169 | ![N9cgB5O7BE](https://github.com/pema99/GITweaks/assets/11212115/bd2e34ba-4b42-4d61-920d-512e4a0dcc5b) 170 | 171 | ### Better default Lighting Settings 172 | When you create a new scene or Lighting Settings asset, the CPU lightmapper is the default choice of baking backend. The CPU lightmap is very slow and shouldn't be used if you have a GPU. This tweak changes the default to be the GPU lightmapper. Additionally, it disables the "Progressive Updates" checkbox by default, which can slow down bakes heavily. 173 | 174 | Before: 175 | 176 | ![D909MxI0nx](https://github.com/pema99/GITweaks/assets/11212115/5cab3c26-48a7-4173-b836-8609a02f47a4) 177 | 178 | After: 179 | 180 | ![Unity_Vdixu05Zlp](https://github.com/pema99/GITweaks/assets/11212115/713c5598-3857-4e33-8c60-160cc35ceded) 181 | 182 | ### Convert lightmapped renderer to probe-lit without rebaking 183 | When you have a lightmapped renderer, and you want to change it to be lit by probes, changing the setting in the inspector won't immediately make the change. For the setting to apply, you must bake again! This tweak adds a button to the inspector that only appears when you have changed a previously lightmapped renderer to be probe-lit, and allows you to immediately apply the change to the Lighting Data asset without having to re-bake. 184 | 185 | ![6C1yre8Mth](https://github.com/pema99/GITweaks/assets/11212115/1cceef7a-e976-4283-b9db-a5ade9cd09cb) 186 | 187 | ### New and Clone buttons for Skybox material 188 | This tweak adds some buttons for quickly creating new Skybox Materials and assigning them in the Environment tab of the Lighting Window. 189 | 190 | ![image](https://github.com/pema99/GITweaks/assets/11212115/43c6cc79-96d1-4302-b310-de252b08d6c5) 191 | 192 | 193 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c22f91f3b316a314d96c7c808a46b806 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: add4b1def8745134b8b9c4f44b12498e 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/GITweaksSeamFix.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using UnityEngine; 3 | 4 | [RequireComponent(typeof(MeshRenderer))] 5 | public class GITweaksSeamFix : MonoBehaviour 6 | { 7 | public bool RunAfterBaking = true; 8 | public MeshRenderer[] RenderersToFixSeamsWith = new MeshRenderer[0]; 9 | 10 | [Range(0, 180)] public float MaxSurfaceAngle = 15; 11 | [Min(0.001f)] public float SeamFixStrength = 5.0f; 12 | 13 | [Min(1)] public int MaxSolverIterationCount = 100; 14 | [Min(1e-13f)] public float SolverTolerance = 0.001f; 15 | } 16 | 17 | #else 18 | public class GITweaksSeamFix : MonoBehaviour {} 19 | #endif -------------------------------------------------------------------------------- /Runtime/GITweaksSeamFix.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c231453a7f34dd444a2b8431f08974e8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/GITweaksSeamFixVolume.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using UnityEngine; 3 | 4 | public class GITweaksSeamFixVolume : MonoBehaviour 5 | { 6 | public bool RunAfterBaking = true; 7 | public MeshRenderer[] RenderersToExclude = new MeshRenderer[0]; 8 | 9 | [Range(0, 180)] public float MaxSurfaceAngle = 15; 10 | [Min(0.001f)] public float SeamFixStrength = 5.0f; 11 | 12 | [Min(1)] public int MaxSolverIterationCount = 100; 13 | [Min(1e-13f)] public float SolverTolerance = 0.001f; 14 | 15 | public void OnDrawGizmosSelected() 16 | { 17 | var oldMatrix = Gizmos.matrix; 18 | Gizmos.matrix = Matrix4x4.TRS(transform.position, Quaternion.identity, transform.lossyScale); 19 | Gizmos.DrawWireCube(Vector3.zero, Vector3.one); 20 | Gizmos.matrix = oldMatrix; 21 | } 22 | } 23 | #else 24 | public class GITweaksSeamFixVolume : MonoBehaviour {} 25 | #endif -------------------------------------------------------------------------------- /Runtime/GITweaksSeamFixVolume.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 16f1a2c1324592148b7e18a317467c20 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/GITweaksSharedLOD.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | [RequireComponent(typeof(LODGroup))] 7 | public class GITweaksSharedLOD : MonoBehaviour 8 | { 9 | public MeshRenderer[] RenderersToLightmap; 10 | 11 | public void Reset() 12 | { 13 | var lods = GetComponent().GetLODs(); 14 | if (lods.Length > 1) 15 | { 16 | RenderersToLightmap = lods 17 | .Skip(1) 18 | .SelectMany(x => x.renderers) 19 | .Select(x => x as MeshRenderer) 20 | .Where(x => x != null) 21 | .ToArray(); 22 | } 23 | } 24 | } 25 | 26 | [CustomEditor(typeof(GITweaksSharedLOD))] 27 | public class GITweaksSharedLODEditor : Editor 28 | { 29 | public override void OnInspectorGUI() 30 | { 31 | if (target is not GITweaksSharedLOD lod) 32 | return; 33 | 34 | serializedObject.Update(); 35 | 36 | if (GUILayout.Button("Reset selection to all LODs")) 37 | { 38 | lod.Reset(); 39 | } 40 | 41 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(GITweaksSharedLOD.RenderersToLightmap))); 42 | 43 | var lods = lod.GetComponent().GetLODs(); 44 | var lod0renderers = lods.Length > 0 ? lods[0].renderers : System.Array.Empty(); 45 | var contributorAlready = lod.RenderersToLightmap 46 | .Where(x => GameObjectUtility.AreStaticEditorFlagsSet(x.gameObject, StaticEditorFlags.ContributeGI)) 47 | .Where(x => !lod0renderers.Contains(x)); 48 | if (contributorAlready.Any()) 49 | { 50 | string renderers = string.Join("\n", contributorAlready.Select(x => $"- {x.name}")); 51 | EditorGUILayout.HelpBox( 52 | $"Some LOD's are marked as GI contributors. For this script to function properly, only LOD0 should be a contributor. The problematic MeshRenderers are:\n{renderers}", 53 | MessageType.Warning); 54 | if (GUILayout.Button("Fix issue")) 55 | { 56 | foreach (var renderer in contributorAlready) 57 | { 58 | var flags = GameObjectUtility.GetStaticEditorFlags(renderer.gameObject); 59 | flags &= ~StaticEditorFlags.ContributeGI; 60 | GameObjectUtility.SetStaticEditorFlags(renderer.gameObject, flags); 61 | } 62 | } 63 | } 64 | 65 | var firstMR = lod0renderers.FirstOrDefault(x => x is MeshRenderer); 66 | if (firstMR != null && lod0renderers.Length > 1) 67 | { 68 | EditorGUILayout.HelpBox( 69 | $"LOD0 contains multiple renderers. Only the first MeshRenderer, {firstMR}, will have its lightmap data copied to other LODs.", 70 | MessageType.Warning); 71 | } 72 | 73 | #if BAKERY_INCLUDED 74 | EditorGUILayout.HelpBox("This component is incompatible with Bakery. It will have no effect when baking using Bakery.", MessageType.Warning); 75 | #endif 76 | 77 | serializedObject.ApplyModifiedProperties(); 78 | } 79 | } 80 | 81 | #else 82 | public class GITweaksSharedLOD : MonoBehaviour {} 83 | #endif -------------------------------------------------------------------------------- /Runtime/GITweaksSharedLOD.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 303db03de3f1b2d418a5c988a2da85d1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/dev.pema.gitweaks.runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GITweaks.Runtime", 3 | "rootNamespace": "", 4 | "references": [], 5 | "includePlatforms": [], 6 | "excludePlatforms": [ 7 | "Android", 8 | "GameCoreScarlett", 9 | "GameCoreXboxOne", 10 | "iOS", 11 | "LinuxStandalone64", 12 | "CloudRendering", 13 | "macOSStandalone", 14 | "PS4", 15 | "PS5", 16 | "Stadia", 17 | "Switch", 18 | "tvOS", 19 | "WSA", 20 | "WebGL", 21 | "WindowsStandalone32", 22 | "WindowsStandalone64", 23 | "XboxOne" 24 | ], 25 | "allowUnsafeCode": false, 26 | "overrideReferences": false, 27 | "precompiledReferences": [], 28 | "autoReferenced": false, 29 | "defineConstraints": [], 30 | "versionDefines": [], 31 | "noEngineReferences": false 32 | } -------------------------------------------------------------------------------- /Runtime/dev.pema.gitweaks.runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8973f39a683868b42a349f2e1dfb47f4 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev.pema.gitweaks", 3 | "displayName": "GI Tweaks", 4 | "version": "0.1.3", 5 | "unity": "2022.3", 6 | "description": "Various fixes, tweaks and tools for working with global illumination.", 7 | "author": { 8 | "name": "Pema Malling", 9 | "email": "pemamalling@gmail.com" 10 | }, 11 | "hideInEditor": false 12 | } 13 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 78747aa2cfd6de94d86267503426564f 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------