diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f64f7..f42f2a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 0.2.0 - 2026-03-11 +### Added +- LOD fade mode support (None, CrossFade, SpeedTree) to reduce popping artifacts during LOD transitions +- Animate cross-fading toggle for smooth automated LOD blending +- `LODGroupHelper.InvalidateCache()` method for manual cache refresh after external LODGroup changes +- Fade mode settings exposed in Preferences UI, per-model LODData overrides, and LODData inspector + +### Fixed +- Consistent `ForceLOD(0)/ForceLOD(-1)` initialization pattern in `ModelImporterLODGenerator.CreateLODGroup` +- Null renderer filtering when building LOD arrays for LODGroup setup in `ModelImporterLODGenerator` +- Null safety for renderer arrays in `Extensions.HasLODChain()` +- Null safety for LOD arrays and renderer arrays in `LODGroupExtensions.SetRenderersEnabled` + ## 0.1.2 - 2024-05-23 - Adds option to use same material for all LODs - More Instalod integration fixes diff --git a/Editor/AutoLOD.cs b/Editor/AutoLOD.cs index 96e3601..bacee00 100644 --- a/Editor/AutoLOD.cs +++ b/Editor/AutoLOD.cs @@ -114,6 +114,8 @@ static public void GenerateLODs(GameObject go) lodGroup.ForceLOD(0); lodGroup.SetLODs(lods.ToArray()); lodGroup.RecalculateBounds(); + lodGroup.fadeMode = autoLODSettingsData.FadeMode; + lodGroup.animateCrossFading = autoLODSettingsData.AnimateCrossFading; lodGroup.ForceLOD(-1); var prefab = PrefabUtility.GetCorrespondingObjectFromSource(go); diff --git a/Editor/AutoLODSettings.cs b/Editor/AutoLODSettings.cs index 09cacec..7976021 100644 --- a/Editor/AutoLODSettings.cs +++ b/Editor/AutoLODSettings.cs @@ -34,6 +34,7 @@ static void DisplayPreferencesGUI() GenerateLODsOnImportGUI(); SaveAssetsGUI(); SameMaterialLODsGUI(); + FadeModeGUI(); UseSceneLODGUI(); HierarchyTypeGUI(); ParentNameGUI(); @@ -180,6 +181,30 @@ static void SameMaterialLODsGUI() autoLODSettingsData.UseSameMaterialForLODs = useSameMaterial; } + static void FadeModeGUI() + { + var label = new GUIContent("LOD Fade Mode", "Controls how LOD levels transition. " + + "None uses hard cuts between LODs. CrossFade enables blending between LOD levels " + + "to reduce popping artifacts. SpeedTree is optimized for SpeedTree assets."); + + EditorGUI.BeginChangeCheck(); + var fadeModeValue = (LODFadeMode)EditorGUILayout.EnumPopup(label, autoLODSettingsData.FadeMode); + if (EditorGUI.EndChangeCheck()) + autoLODSettingsData.FadeMode = fadeModeValue; + + if (autoLODSettingsData.FadeMode != LODFadeMode.None) + { + EditorGUI.indentLevel++; + var animateLabel = new GUIContent("Animate Cross-Fading", "When enabled, Unity will automatically " + + "animate the transition between LOD levels over time for smoother visual results."); + EditorGUI.BeginChangeCheck(); + var animate = EditorGUILayout.Toggle(animateLabel, autoLODSettingsData.AnimateCrossFading); + if (EditorGUI.EndChangeCheck()) + autoLODSettingsData.AnimateCrossFading = animate; + EditorGUI.indentLevel--; + } + } + static void UseSceneLODGUI() { var label = new GUIContent("Scene LOD", "Enable Hierarchical LOD (HLOD) support for scenes, " diff --git a/Editor/Extensions.cs b/Editor/Extensions.cs index 595c89d..cb496ec 100644 --- a/Editor/Extensions.cs +++ b/Editor/Extensions.cs @@ -14,7 +14,7 @@ public static bool HasLODChain(this LODGroup lodGroup) for (var l = 1; l < lods.Length; l++) { var lod = lods[l]; - if (lod.renderers.Length > 0) + if (lod.renderers != null && lod.renderers.Length > 0) { return true; } diff --git a/Editor/LODDataEditor.cs b/Editor/LODDataEditor.cs index 2f5967d..58b2a26 100644 --- a/Editor/LODDataEditor.cs +++ b/Editor/LODDataEditor.cs @@ -41,6 +41,8 @@ public override void OnInspectorGUI() m_ImportSettings.FindPropertyRelative("initialLODMaxPolyCount").intValue = autoLODSettingsData.InitialLODMaxPolyCount; m_ImportSettings.FindPropertyRelative("hierarchyType").enumValueIndex = (int)autoLODSettingsData.HierarchyType; m_ImportSettings.FindPropertyRelative("parentName").stringValue = autoLODSettingsData.ParentName; + m_ImportSettings.FindPropertyRelative("fadeMode").enumValueIndex = (int)autoLODSettingsData.FadeMode; + m_ImportSettings.FindPropertyRelative("animateCrossFading").boolValue = autoLODSettingsData.AnimateCrossFading; } if (settingsOverridden) diff --git a/Editor/ModelImporterLODGenerator.cs b/Editor/ModelImporterLODGenerator.cs index ad9d701..a34a7ce 100644 --- a/Editor/ModelImporterLODGenerator.cs +++ b/Editor/ModelImporterLODGenerator.cs @@ -464,12 +464,20 @@ void CreateLODGroup(GameObject go, LODData lodData) for (int i = 0; i <= maxLODFound; i++) { - var lod = new LOD { renderers = lodData[i], screenRelativeTransitionHeight = GetScreenPercentage(i, maxLODFound, importerLODLevels) }; + var renderers = lodData[i]; + if (renderers != null) + renderers = renderers.Where(r => r != null).ToArray(); + + var lod = new LOD { renderers = renderers ?? Array.Empty(), screenRelativeTransitionHeight = GetScreenPercentage(i, maxLODFound, importerLODLevels) }; lods.Add(lod); } + lodGroup.ForceLOD(0); lodGroup.SetLODs(lods.ToArray()); lodGroup.RecalculateBounds(); + lodGroup.fadeMode = autoLODSettingsData.FadeMode; + lodGroup.animateCrossFading = autoLODSettingsData.AnimateCrossFading; + lodGroup.ForceLOD(-1); SyncImporterLODLevels(importerRef, importerLODLevels, lods); @@ -610,6 +618,8 @@ internal static LODData GetLODData(string assetPath) importSettings.initialLODMaxPolyCount = autoLODSettingsData.InitialLODMaxPolyCount; importSettings.hierarchyType = autoLODSettingsData.HierarchyType; importSettings.parentName = autoLODSettingsData.ParentName; + importSettings.fadeMode = autoLODSettingsData.FadeMode; + importSettings.animateCrossFading = autoLODSettingsData.AnimateCrossFading; } return lodData; diff --git a/Runtime/AutoLODSettingsData.cs b/Runtime/AutoLODSettingsData.cs index 8fa0684..24599f7 100644 --- a/Runtime/AutoLODSettingsData.cs +++ b/Runtime/AutoLODSettingsData.cs @@ -32,6 +32,8 @@ public class AutoLODSettingsData : ScriptableObject [SerializeField] private bool showVolumeBounds = false; [SerializeField] private int maxLOD = AutoLODConst.k_DefaultMaxLOD; [SerializeField] private bool useSameMaterialForLODs = false; + [SerializeField] private LODFadeMode fadeMode = LODFadeMode.None; + [SerializeField] private bool animateCrossFading = false; [SerializeField] private List meshSimplifiers; [SerializeField] private List batchers; @@ -369,6 +371,26 @@ public bool UseSameMaterialForLODs } } + public LODFadeMode FadeMode + { + get => fadeMode; + set + { + fadeMode = value; + OnSettingsUpdated?.Invoke(); + } + } + + public bool AnimateCrossFading + { + get => animateCrossFading; + set + { + animateCrossFading = value; + OnSettingsUpdated?.Invoke(); + } + } + public IPreferences SimplifierPreferences { get => simplifierPreferences; diff --git a/Runtime/Extensions/LODGroupExtensions.cs b/Runtime/Extensions/LODGroupExtensions.cs index 8569102..8116aa5 100644 --- a/Runtime/Extensions/LODGroupExtensions.cs +++ b/Runtime/Extensions/LODGroupExtensions.cs @@ -32,11 +32,17 @@ public static void SetEnabled(this LODGroupHelper lodGroupHelper, bool enabled) static void SetRenderersEnabled(LOD[] lods, bool enabled) { + if (lods == null) + return; + for (var i = 0; i < lods.Length; i++) { var lod = lods[i]; var renderers = lod.renderers; + if (renderers == null) + continue; + foreach (var r in renderers) { if (r) diff --git a/Runtime/LODGroupHelper.cs b/Runtime/LODGroupHelper.cs index 045172c..e9a0b1a 100644 --- a/Runtime/LODGroupHelper.cs +++ b/Runtime/LODGroupHelper.cs @@ -66,6 +66,18 @@ public int maxLOD } } + /// + /// Invalidates all cached LODGroup data, forcing it to be re-fetched on next access. + /// Call this after modifying the LODGroup's LODs, transform, or size externally. + /// + public void InvalidateCache() + { + m_LODs = null; + m_ReferencePoint = null; + m_WorldSpaceSize = null; + m_MaxLOD = null; + } + [SerializeField] LODGroup m_LODGroup; LOD[] m_LODs; diff --git a/Runtime/LODImportSettings.cs b/Runtime/LODImportSettings.cs index f7e0538..ed3091f 100644 --- a/Runtime/LODImportSettings.cs +++ b/Runtime/LODImportSettings.cs @@ -1,4 +1,5 @@ using System; +using UnityEngine; using UnityEngine.Serialization; namespace Unity.AutoLOD @@ -13,5 +14,7 @@ public class LODImportSettings public int initialLODMaxPolyCount = Int32.MaxValue; public LODHierarchyType hierarchyType = LODHierarchyType.ChildOfSource; public string parentName = String.Empty; + public LODFadeMode fadeMode = LODFadeMode.None; + public bool animateCrossFading = false; } } diff --git a/Runtime/LODVolume_Editor.cs b/Runtime/LODVolume_Editor.cs index f087a28..67ecd32 100644 --- a/Runtime/LODVolume_Editor.cs +++ b/Runtime/LODVolume_Editor.cs @@ -193,6 +193,8 @@ public IEnumerator GenerateHLOD(bool propagateUpwards = true) lod.renderers = hlodRoot.GetComponentsInChildren(false); lodGroup.SetLODs(new LOD[] { lod }); + lodGroup.fadeMode = AutoLODSettingsData.Instance.FadeMode; + lodGroup.animateCrossFading = AutoLODSettingsData.Instance.AnimateCrossFading; if (propagateUpwards) { @@ -270,6 +272,8 @@ void GenerateLODs() lodGroup.ForceLOD(0); lodGroup.SetLODs(lods.ToArray()); lodGroup.RecalculateBounds(); + lodGroup.fadeMode = AutoLODSettingsData.Instance.FadeMode; + lodGroup.animateCrossFading = AutoLODSettingsData.Instance.AnimateCrossFading; lodGroup.ForceLOD(-1); var prefab = PrefabUtility.GetCorrespondingObjectFromSource(go);