Automated Unity prefab optimizer
Written by Jesper Tingvall, Product Expert, Simplygon
Disclaimer: The code in this post is written using version 10.3.5200.0 of Simplygon and Unity 2022.3.13f1. If you encounter this post at a later stage, some of the API calls might have changed. However, the core concepts should still remain valid.
Introduction
In this blog post we'll showcase how to automate LOD creation for Unity prefabs. This allows us to optimize our game with (almost) the click of a button.
Prerequisites
This example will use the Simplygon integration in Unity with HDRP render pipeline, but the same concepts can be applied to all other Unity render pipelines.
Problem to solve
We have a Unity HDRP game that is currently experiencing performance issues. In the game we have lots of prefabs and we do not want to manually optimize each one of them.
To investigate the impact we are going to use this scene which has performance issues.
Solution
We will create a set of Unity scripts which enables us to do the following.
- Perform optimization of all prefabs in the game, including helper functions like clearing LOD data or only rebuilding LODs for those without any.
- Define optimization parameters for a group of assets with
ScriptableObjects
. - Create missing LODs during build phase.
With that system in place we can easily work on fine tuning the game's performance with minimal manual work. We will use two pipelines, reduction to create most of the LOD levels with an optional remeshing step as the last LOD level. We expect to use remeshing on larger kitbashed assets.
Blog structure
The solution has quite many parts, so we will only deep dive into the important functions. Complete scripts can be found last in the blog. The solution is to divided up into several parts.
The first parts covers how to save optimization settings as a ScriptableObject
and connect them to our prefabs.
- LOD Settings: Saving settings as LODQuality
- LOD Settings: LODSettings component for mapping prefabs to LODQuality
Secondary part covers how to create Simplygon pipelines from our settings object.
- Pipelines: Cascaded reduction pipelines with passthrough
- Pipelines: Remeshing pipeline with HDRP material casters
Third part showcase how to work with Unity prefabs.
- Prefab: Loading and saving prefab
- Prefab: Cleaning LOD data and LODGroups
- Prefab: Processing prefab with Simplygon pipelines and creating LODGroup
The fourth part showcase an example on how to trigger generating all LOD data during Player build.
LOD Settings: Saving settings as LODQuality
We will save optimization settings as ScriptableObject
. This makes it easy to create and edit settings for different objects from inside Unity. We can easily create quality settings for different kinds of objects, like all small props, all houses and so on.
[CreateAssetMenu(fileName = "New LOD Quality", menuName = "Simplygon/LOD Quality")]
public class LODQuality : ScriptableObject
{
// LOD quality settings
public float[] LODLevels;
public float Quality = 1;
// Relative screen size for culling away object
public float FadeAwayDistance;
// Settings for hiding LOD transitions with cross fading
public LODFadeMode LODFadeMode = LODFadeMode.CrossFade;
public float FadeTransitionWidth = 0.1f;
We also introduce some helper functions to map Unity's relative screen height metric to Simplygon's screen size target. The blog Using Simplygon with Unity LODGroups covers this in more detail.
/// <summary>
/// Calculate screen size in pixels from relative screen height.
/// </summary>
private uint GetScreenSizeForRelativeScreenHeight(float lodLevel, bool ignoreQuality)
{
lodLevel = Mathf.Clamp01(lodLevel);
var quality = Quality;
if (ignoreQuality)
quality = 1;
var ScreenSize = GAME_RESOLUTION_HEIGHT * lodLevel * quality;
if (ScreenSize < MIN_SCREEN_SIZE)
{
return MIN_SCREEN_SIZE;
}
else
{
return (uint)Mathf.CeilToInt(ScreenSize);
}
}
After adding the script we can easily add new LODQuality
by selecting Assets → Create → Simplygon → LOD Quality.
We get a nice editor UI where we can create different quality settings.
LOD Settings: LODSettings component for mapping prefabs to LODQuality
Prefabs are at the heart of Unity game development. Most objects in our game are saved as prefabs so it make sense this is where we add the ability to generate LOD levels. We add a component that we should attach to every prefab we want to build LODs for. It specified a LODQuality
that contains the settings. This way we can easily share quality settings for many objects.
public class LODSettings : MonoBehaviour
{
public LODQuality Quality;
public string LODPath;
We will also add an Editor
script for this component that allows us to specify apperance in editor.
[CustomEditor(typeof(LODSettings))]
public class LODSettingsEditor : Editor
{
/// <summary>
/// Renders editor UI for LODSettings.
/// </summary>
override public void OnInspectorGUI()
{
var standin = target as LODSettings;
EditorGUILayout.LabelField("LOD Quality:");
var currentQuality = standin.Quality;
var newQuality = EditorGUILayout.ObjectField(standin.Quality, typeof(LODQuality), false) as LODQuality;
if (currentQuality != newQuality)
{
standin.Quality = newQuality;
EditorUtility.SetDirty(standin);
AssetDatabase.SaveAssets();
}
We will add two buttons. One for building LOD data for this prefab.
string assetPath = AssetDatabase.GetAssetPath(standin);
if (assetPath != null && assetPath.Length > 0 && standin.Quality)
{
EditorGUILayout.Separator();
if (GUILayout.Button("Build LODs"))
{
PrefabLODBuilder.BuildLODsForPrefab(standin.gameObject);
}
And a button for destroying LOD data for this prefab.
if (standin.HasBuildLOD)
{
if (GUILayout.Button("Destroy LODs"))
{
GameObject prefab = PrefabUtility.LoadPrefabContents(assetPath);
PrefabLODBuilder.ClearPrefab(prefab);
PrefabUtility.SaveAsPrefabAsset(prefab, assetPath);
PrefabUtility.UnloadPrefabContents(prefab);
}
}
We now have a component we can add to prefabs that connect them to a LODQuality
. We can build and clear LODs for the prefab with the buttons.
Pipelines: Cascaded reduction pipelines with passthrough
We will now create our Simplygon pipelines from our LODQuality object. We will start with creating an PassthroughPipeline
. This pipeline does not do any optimization and just passes through the input to all its cascaded pipelines. This enables us to choose if our pipelines should be cascaded from previously generated LOD, or use LOD0.
Our intention is to create a pipeline hierarcy similar to this. All reduction pipelines are cascaded. If we have specified RemeshLastLOD
then the last LOD level will be a remeshing pipeline based on LOD0. The reason why we do this is that reduction sometimes destroys and creates holes in the model. Normaly that is no problem for LOD far away, but if we want to do a remeshing these kinds of holes can cause a double sided model to be generated.
LOD0
|
Passthrough
|
|___Reduction (LOD1)
| |
| |___Reduction (LOD2)
| |
| |___Reduction (LOD3)
|
|___Remeshing (LOD4)
This script will create that pipeline structure. Notice that reduction pipelines are added as casceded pipelines to pipelines[i]
while the remeshed pipeline is added to pipelines[0]
which refers the passthrough pipeline.
private static List<spPipeline> CreatePipelinesForLODQuality(ISimplygon simplygon, LODQuality quality)
{
List<spPipeline> pipelines = new List<spPipeline>() { simplygon.CreatePassthroughPipeline() };
for (int i = 0; i < quality.NumberOfLODLevels(); i++)
{
spPipeline pipeline = null;
if (i == quality.NumberOfLODLevels() - 1 && quality.RemeshLastLOD)
{
pipeline = CreateRemeshingPipeline(simplygon, quality.GetScreenSizeForIndex(i, true), quality.AutomaticTextureSizeMultiplier);
pipelines[0].AddCascadedPipeline(pipeline);
}
else
{
pipeline = CreateReductionPipelineForScreenHeight(simplygon, quality.GetScreenSizeForIndex(i, false));
pipelines[i].AddCascadedPipeline(pipeline);
}
pipelines.Add(pipeline);
}
return pipelines;
}
Here is how to create a reduction pipeline that uses screen size as reduction target.
private static spPipeline CreateReductionPipelineForScreenHeight(ISimplygon simplygon, uint screenHeight)
{
var pipeline = simplygon.CreateReductionPipeline();
spReductionSettings reductionSettings = pipeline.GetReductionSettings();
reductionSettings.SetReductionTargets(EStopCondition.All, false, false, false, true);
reductionSettings.SetReductionTargetOnScreenSize(screenHeight);
return pipeline;
}
Pipelines: Remeshing pipeline with HDRP material casters
We create a function for creating remeshing pipeline as well. We will use automatic texture size so we also take that as a input parameter and pass it to AutomaticTextureSizeMultiplier
.
private static spPipeline CreateRemeshingPipeline(ISimplygon simplygon, uint screenHeight, float automaticTextureSizeMultiplier)
{
var pipeline = simplygon.CreateRemeshingPipeline();
var settings = pipeline.GetRemeshingSettings();
settings.SetOnScreenSize(screenHeight);
using var simplygonMappingImageSettings = pipeline.GetMappingImageSettings();
simplygonMappingImageSettings.SetGenerateMappingImage(true);
simplygonMappingImageSettings.SetGenerateTexCoords(true);
simplygonMappingImageSettings.SetGenerateTangents(true);
simplygonMappingImageSettings.SetUseFullRetexturing(true);
simplygonMappingImageSettings.SetApplyNewMaterialIds(true);
simplygonMappingImageSettings.SetUseAutomaticTextureSize(true);
simplygonMappingImageSettings.SetAutomaticTextureSizeMultiplier(automaticTextureSizeMultiplier);
Now it is time to add material casters. In Unity we use compute casters for material casting. During export from Unity into Simplygon we setup all neccesary evaluation shaders, attributes and functions for casting. All we have to do for casting is add a compute caster. Then specify which channel and color space.
Our game uses the HDRP render pipeline so the channel names are setup for that one. We can look in Unity Rendering Pipelines to Simplygon table to find correct values.
// Add a caster for the albedo map channel.
using var albedoMapCaster = simplygon.CreateComputeCaster();
using var albedoMapCasterSettings = albedoMapCaster.GetComputeCasterSettings();
albedoMapCasterSettings.SetMaterialChannel("BaseMap");
albedoMapCasterSettings.SetOutputColorSpace(EImageColorSpace.sRGB);
pipeline.AddMaterialCaster(albedoMapCaster, 0);
// Add a caster for the albedo map channel.
using var maskMapCaster = simplygon.CreateComputeCaster();
using var maskMapCasterSettings = maskMapCaster.GetComputeCasterSettings();
maskMapCasterSettings.SetMaterialChannel("MaskMap");
maskMapCasterSettings.SetOutputColorSpace(EImageColorSpace.Linear);
pipeline.AddMaterialCaster(maskMapCaster, 0);
// Add a caster for normals.
using var normalsCaster = simplygon.CreateComputeCaster();
using var normalsCasterSettings = normalsCaster.GetComputeCasterSettings();
normalsCasterSettings.SetMaterialChannel("NormalMap");
normalsCasterSettings.SetOutputColorSpace(EImageColorSpace.Linear);
pipeline.AddMaterialCaster(normalsCaster, 0);
Let's create a LODQuality
and see how well our pipelines work. Here is the settings we'll use.
After optimization with that LOD quality we get following LOD.
Let us also inspect the object visually. We can see that the large jump in triangle count comes once we remesh the last LOD.
Prefab: Loading and saving prefab
To optimize a prefab we first need to load it into memory. To do this we call LoadPrefabContents
, this will create an empty scene containing our prefab. We can then first clean old LOD levels from it, create new LODs with Simplygon, save it with SafeAsPrefabAsset
and lastly unload it with UnloadPrefabContents
.
public static void BuildLODsForPrefab(GameObject prefab)
{
Debug.Log($"Processing prefab: {prefab.name}", prefab);
string assetPath = AssetDatabase.GetAssetPath(prefab);
GameObject contentsRoot = PrefabUtility.LoadPrefabContents(assetPath);
var lodSettings = prefab.GetComponent<LODSettings>();
if (!lodSettings.Quality)
{
Debug.LogWarning($"{prefab.name} is missing LOD quality setting.", prefab);
return;
}
ClearPrefab(contentsRoot);
CreateLOD(contentsRoot);
PrefabUtility.SaveAsPrefabAsset(contentsRoot, assetPath);
PrefabUtility.UnloadPrefabContents(contentsRoot);
}
Prefab: Cleaning LOD data and LODGroups
To have a nice workflow when creating LOD levels we need to be able to clean up old data. It is both intented to clean up LOD levels that might already been on the prefab (for example if we aquired it from an asset pack) and data generated by the prefab LOD creation system in this blog.
We start by deleting all non LOD0 GameObjects
that a LODGroup
refers to.
public static void ClearPrefab(GameObject prefab)
{
// Remove all childs refered by LODGroup that is not LOD0
var lodGroups = prefab.GetComponentsInChildren<LODGroup>();
foreach (var lodGroup in lodGroups)
{
var lods = lodGroup.GetLODs();
for (int i = 1; i < lods.Length; i++)
{
foreach (var renderer in lods[i].renderers)
{
if (renderer && renderer.gameObject)
GameObject.DestroyImmediate(renderer.gameObject);
}
}
GameObject.DestroyImmediate(lodGroup);
}
This can result in empty GameObjects
. To address this we remove any node which is named _LODn
where n !=0.
// Destroy child GameObjects containing _LOD names.
foreach (var transform in prefab.GetComponentsInChildren<Transform>())
{
if (transform && transform.gameObject && transform.gameObject.name.Contains("_LOD") && !transform.gameObject.name.Contains("_LOD0"))
{
GameObject.DestroyImmediate(transform.gameObject);
}
}
Lastly we remove all data in LODPath
, the folder where we saved LOD data for our prefab.
// Remove generated LOD models from asset folder.
var lodSettings = prefab.GetComponent<LODSettings>();
if (lodSettings.LODPath != null && lodSettings.LODPath.Length > 0)
{
AssetDatabase.MoveAssetToTrash(lodSettings.LODPath);
lodSettings.LODPath = "";
}
}
Prefab: Processing prefab with Simplygon pipelines and creating LODGroup
Now it is time to create a function which loads Simplygon and optimize the loaded prefab. First we initialize Simplygon and create Simplygon pipelines from the specified LODQuality
. After that we run all pipelines by calling Run on the root pipeline.
private static void CreateLOD(GameObject prefab)
{
if (!prefab)
return;
var lodQuality = prefab.GetComponent<LODSettings>().Quality;
using (ISimplygon simplygon = global::Simplygon.Loader.InitSimplygon
(out EErrorCodes simplygonErrorCode, out string simplygonErrorMessage))
{
// if Simplygon handle is valid
if (simplygonErrorCode == EErrorCodes.NoError)
{
// Create Simplygon pipelines from LODQuality.
var pipelines = CreatePipelinesForLODQuality(simplygon, lodQuality);
// Run Simplygon processing.
var simplygonProcessing = new SimplygonProcessing(simplygon);
simplygonProcessing.Run(pipelines[0], new List<GameObject>() { prefab }, EPipelineRunMode.RunInThisProcess, true, exportMeshesAsUnique: true);
After creating the LODs we assign the created folder to LODPath so we can clean up the created data. We now create a LODGroup
and assign all created LODs to their correct place. We are also cleaning any data from the LOD levels which is not a renderer.
prefab.GetComponent<LODSettings>().LODPath = simplygonProcessing.PrefabAssetFolder;
var lodGroup = prefab.AddComponent<LODGroup>();
LOD[] lods = new LOD[simplygonProcessing.LODGameObjects.Count + 1];
lods[0] = CreateLODFromChildRenderers(prefab, lodQuality.GetRelativeScreenHeightForIndex(0), lodQuality.FadeTransitionWidth);
for (int i = 0; i < simplygonProcessing.LODGameObjects.Count; i++)
{
var lod = simplygonProcessing.LODGameObjects[i];
// We run this several times as a hacky way to destroy components that requires other components.
ClearComponents(lod);
ClearComponents(lod);
ClearComponents(lod);
lod.transform.parent = prefab.transform;
lods[i + 1] = CreateLODFromChildRenderers(lod, lodQuality.GetRelativeScreenHeightForIndex(i + 1), lodQuality.FadeTransitionWidth);
}
lodGroup.SetLODs(lods);
lodGroup.fadeMode = lodQuality.LODFadeMode;
}
Build missing LODs during Player build
By implimenting the IPreprocessBuildWithReport
interface we can run code while the Player build runs. We will here build all missing LODs.
Careful consideration should be taken before adding this script, as it can make your builds take very long time. If you choose to add it we strongly suggest to add logic to only build them during nightly builds.
public class BuildPrefabsDuringBuild : IPreprocessBuildWithReport
{
public int callbackOrder => 0;
/// <summary>
/// Build all prefabs missing LOD data when building player.
/// </summary>
public void OnPreprocessBuild(BuildReport report)
{
Debug.Log("Build missing LODs");
PrefabLODBuilder.BuildLODs();
}
}
Result
Let us first take a benchmark video of the scene we want to optimize. The average fps for our camera path is ___.
We add a LODSetting
component to all prefabs, then specifying desired quality by picking a LODQuality
.
We can now generate LOD data by opening up our menu and choosing Simplygon → Build missing LODs. Depending on how many assets we have in our project this can take some time. On my machine this takes 2.5 hours, so take a long fika break or run during night.
Small Objects
This quality level is intented for smaller non important objects. For small objects we will only use reduction.
Here is an example of a small object, a lamp. LOD0 is to the left.
LOD level | Triangle count | Transition height % |
---|---|---|
LOD0 | 11k | - |
LOD1 | 5k | 75% |
LOD2 | 2k | 25% |
Medium
For medium sized objects we will use these settings. We will use remeshing as last LOD in our pipeline.
Let's look at an example well asset and how it's been reduced. LOD0 is to the left.
LOD level | Triangle count | Transition height % |
---|---|---|
LOD0 | 72k | - |
LOD1 | 19k | 75% |
LOD2 | 15k | 50% |
LOD3 | 2k | 12% |
Houses and other large objects
For large objects, mostly houses, we will use this pipeline. We have quite a long LOD chain that ends in a remeshing step. We have also set fade away distance to 0 as we expect larger objects to never really be far away enough to be completely removed.
Let's look at an example and compare the different LOD levels. There is very small difference between LOD0 and LOD1, LOD2 so we compate it with larger jumps.
LOD level | Triangle count | Transition height % |
---|---|---|
LOD0 | 95k | - |
LOD1 | 33k | 100% |
LOD2 | 27k | 80% |
LOD3 | 19k | 50% |
LOD4 | 10k | 30% |
LOD5 | 2k | 15% |
Performance
So did all of our hard work improve our performance? Let us benchmark! We can start by looking in our starting location. Here we get a nice FPS increase from around 25 to 50.
In other areas of the level we also get increased FPS.
Adding LODs to our assets clearly improved our rendering performance. We are not the entire way to stable 60 fps though. But that is expected, we just guessed appropriate settings for each asset category. We can now fine tune the performance by changing our LODQuality
assets and rebuilding LODs until we are satisfied. Now is a good time to benchmark and figure out where the bottle neck are, are we bound by triangles, draw calls or something else? Thanks to our automated pipeline we can experiment and test different optimization strategies.
Future work
There is a lot of improvements that can be done to this quite basic LOD system.
- We can introduce more Simplygon optimization pipelines like billboard cloud that can be useful for optimize vegetation assets.
- We can expose more settings for our reduction and remeshing pipeline for better adapt quality to LOD level
- If we have very tessellated assets we could also benefit from optimizing LOD0 as showcased in this blog on LOD0 optimization in Unity. Even if we do not perform triangle reduction, merging materials and removing internal geometry with aggregation pipeline can be really useful. In the tables we can see that lots of our assets have a huge jump in triangle count from LOD0 to LOD1. This indicates that LOD0 is a bit over tesselated.
- Improve logic of when to build LOD levels depending on how build pipeline works. One possible improvement would be to add a button for rebuilding all LODs that uses a specified
LODQuality
.
Complete scripts
LODQuality.cs
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using UnityEngine;
namespace Simplygon.Examples.LODPrefab
{
[CreateAssetMenu(fileName = "New LOD Quality", menuName = "Simplygon/LOD Quality")]
public class LODQuality : ScriptableObject
{
// LOD quality settings
public float[] LODLevels;
public float Quality = 1;
// Relative screen size for culling away object
public float FadeAwayDistance;
// Settings for hiding LOD transitions with cross fading
public LODFadeMode LODFadeMode = LODFadeMode.CrossFade;
public float FadeTransitionWidth = 0.1f;
// Settings for remeshing last LOD level
public bool RemeshLastLOD = false;
public float AutomaticTextureSizeMultiplier = 8;
// Constants
static uint GAME_RESOLUTION_HEIGHT = 1080;
static uint MIN_SCREEN_SIZE = 20;
/// <summary>
/// Returns how many LOD levels we should generate.
/// </summary>
public int NumberOfLODLevels()
{
return LODLevels.Length;
}
/// <summary>
/// Get relative screen heights for LOD index i.
/// </summary>
public float GetRelativeScreenHeightForIndex(int i)
{
if (i >= LODLevels.Length)
return FadeAwayDistance;
return LODLevels[i];
}
/// <summary>
/// Get screen size in pixels for LOD index i.
/// </summary>
public uint GetScreenSizeForIndex(int i, bool ignoreQuality)
{
return GetScreenSizeForRelativeScreenHeight(LODLevels[i], ignoreQuality);
}
/// <summary>
/// Calculate screen size in pixels from relative screen height.
/// </summary>
private uint GetScreenSizeForRelativeScreenHeight(float lodLevel, bool ignoreQuality)
{
lodLevel = Mathf.Clamp01(lodLevel);
var quality = Quality;
if (ignoreQuality)
quality = 1;
var ScreenSize = GAME_RESOLUTION_HEIGHT * lodLevel * quality;
if (ScreenSize < MIN_SCREEN_SIZE)
{
return MIN_SCREEN_SIZE;
}
else
{
return (uint)Mathf.CeilToInt(ScreenSize);
}
}
}
}
LODSettings.cs
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using UnityEngine;
namespace Simplygon.Examples.LODPrefab
{
public class LODSettings : MonoBehaviour
{
public LODQuality Quality;
public string LODPath;
/// <summary>
/// Returns if we have build LODs.
/// </summary>
public bool HasBuildLOD
{
get
{
return LODPath != null && LODPath.Length > 0;
}
}
}
}
PrefabLODBuilder.cs
This script should be placed in an Editor
folder.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using Simplygon.Unity.EditorPlugin;
namespace Simplygon.Examples.LODPrefab
{
public class PrefabLODBuilder
{
/// <summary>
/// Clear all generated LODs.
/// </summary>
[MenuItem("Simplygon/Clear all LODs")]
public static void ClearAllLODs()
{
System.Type componentType = typeof(LODSettings);
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab");
foreach (string prefabGuid in prefabGuids)
{
string prefabPath = AssetDatabase.GUIDToAssetPath(prefabGuid);
GameObject prefab = PrefabUtility.LoadPrefabContents(prefabPath);
if (prefab != null && prefab.GetComponent(componentType) != null && prefab.GetComponent<LODSettings>().LODPath.Length > 0 && !prefabPath.Contains("LOD"))
{
Debug.Log("Removing LOD prefab from " + prefab.GetComponent<LODSettings>().LODPath, prefab);
ClearPrefab(prefab);
PrefabUtility.SaveAsPrefabAsset(prefab, prefabPath);
}
PrefabUtility.UnloadPrefabContents(prefab);
}
AssetDatabase.SaveAssets();
}
/// <summary>
/// Force rebuild every LOD.
/// </summary>
[MenuItem("Simplygon/Rebuild all LODs")]
public static void RebuildLODs()
{
BuildLODs(true);
}
/// <summary>
/// Build LODs for prefabs missing generated LOD data.
/// </summary>
[MenuItem("Simplygon/Build missing LODs")]
public static void BuildLODs()
{
BuildLODs(false);
}
/// <summary>
/// Build LOD data for every prefab with LODSettings component attached.
/// </summary>
static void BuildLODs(bool buildAll)
{
foreach (string prefabGuid in AssetDatabase.FindAssets("t:Prefab"))
{
string prefabPath = AssetDatabase.GUIDToAssetPath(prefabGuid);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
if (prefab)
{
var lodSettings = prefab.GetComponent<LODSettings>();
if (lodSettings != null &&
!prefabPath.Contains("LOD") &&
(!lodSettings.HasBuildLOD || buildAll))
{
BuildLODsForPrefab(prefab);
EditorUtility.SetDirty(prefab);
}
}
}
AssetDatabase.SaveAssets();
}
/// <summary>
/// Load prefab into memory and generate LOD data for it.
/// </summary>
public static void BuildLODsForPrefab(GameObject prefab)
{
Debug.Log($"Processing prefab: {prefab.name}", prefab);
string assetPath = AssetDatabase.GetAssetPath(prefab);
GameObject contentsRoot = PrefabUtility.LoadPrefabContents(assetPath);
var lodSettings = prefab.GetComponent<LODSettings>();
if (!lodSettings.Quality)
{
Debug.LogWarning($"{prefab.name} is missing LOD quality setting.", prefab);
return;
}
ClearPrefab(contentsRoot);
CreateLOD(contentsRoot);
PrefabUtility.SaveAsPrefabAsset(contentsRoot, assetPath);
PrefabUtility.UnloadPrefabContents(contentsRoot);
}
/// <summary>
/// Initiate Simplygon and create LOD for GameObject prefab.
/// </summary>
private static void CreateLOD(GameObject prefab)
{
if (!prefab)
return;
var lodQuality = prefab.GetComponent<LODSettings>().Quality;
using (ISimplygon simplygon = global::Simplygon.Loader.InitSimplygon
(out EErrorCodes simplygonErrorCode, out string simplygonErrorMessage))
{
// if Simplygon handle is valid
if (simplygonErrorCode == EErrorCodes.NoError)
{
// Create Simplygon pipelines from LODQuality.
var pipelines = CreatePipelinesForLODQuality(simplygon, lodQuality);
// Run Simplygon processing.
var simplygonProcessing = new SimplygonProcessing(simplygon);
simplygonProcessing.Run(pipelines[0], new List<GameObject>() { prefab }, EPipelineRunMode.RunInThisProcess, true, exportMeshesAsUnique: true);
prefab.GetComponent<LODSettings>().LODPath = simplygonProcessing.PrefabAssetFolder;
var lodGroup = prefab.AddComponent<LODGroup>();
LOD[] lods = new LOD[simplygonProcessing.LODGameObjects.Count + 1];
lods[0] = CreateLODFromChildRenderers(prefab, lodQuality.GetRelativeScreenHeightForIndex(0), lodQuality.FadeTransitionWidth);
for (int i = 0; i < simplygonProcessing.LODGameObjects.Count; i++)
{
var lod = simplygonProcessing.LODGameObjects[i];
// We run this several times as a hacky way to destroy components that requires other components.
ClearComponents(lod);
ClearComponents(lod);
ClearComponents(lod);
lod.transform.parent = prefab.transform;
lods[i + 1] = CreateLODFromChildRenderers(lod, lodQuality.GetRelativeScreenHeightForIndex(i + 1), lodQuality.FadeTransitionWidth);
}
lodGroup.SetLODs(lods);
lodGroup.fadeMode = lodQuality.LODFadeMode;
}
else
{
Debug.LogError("Simplygon initializing failed!");
}
}
}
/// <summary>
/// Create LOD struct from all child objects.
/// </summary>
public static LOD CreateLODFromChildRenderers(GameObject gameObject, float relativeScreenSize, float fadeTransition)
{
return new LOD(relativeScreenSize, gameObject.GetComponentsInChildren<Renderer>()) { fadeTransitionWidth = fadeTransition };
}
/// <summary>
/// Remove all components from created LOD that is not needed.
/// We only need MeshFilter, MeshRenderer and Transform.
/// </summary>
private static void ClearComponents(GameObject prefab)
{
foreach (var compoment in prefab.GetComponentsInChildren<Component>())
{
var type = compoment.GetType();
if (type != typeof(MeshFilter) && type != typeof(MeshRenderer) && type != typeof(Transform))
{
Component.DestroyImmediate(compoment);
}
}
}
/// <summary>
/// Clear generated data from prefab.
/// </summary>
public static void ClearPrefab(GameObject prefab)
{
// Remove all childs refered by LODGroup that is not LOD0
var lodGroups = prefab.GetComponentsInChildren<LODGroup>();
foreach (var lodGroup in lodGroups)
{
var lods = lodGroup.GetLODs();
for (int i = 1; i < lods.Length; i++)
{
foreach (var renderer in lods[i].renderers)
{
if (renderer && renderer.gameObject)
GameObject.DestroyImmediate(renderer.gameObject);
}
}
GameObject.DestroyImmediate(lodGroup);
}
// Destroy child GameObjects containing _LOD names.
foreach (var transform in prefab.GetComponentsInChildren<Transform>())
{
if (transform && transform.gameObject && transform.gameObject.name.Contains("_LOD") && !transform.gameObject.name.Contains("_LOD0"))
{
GameObject.DestroyImmediate(transform.gameObject);
}
}
// Remove generated LOD models from asset folder.
var lodSettings = prefab.GetComponent<LODSettings>();
if (lodSettings.LODPath != null && lodSettings.LODPath.Length > 0)
{
AssetDatabase.MoveAssetToTrash(lodSettings.LODPath);
lodSettings.LODPath = "";
}
}
/// <summary>
/// Create Simplygon pipelines from LODQuality.
/// </summary>
private static List<spPipeline> CreatePipelinesForLODQuality(ISimplygon simplygon, LODQuality quality)
{
List<spPipeline> pipelines = new List<spPipeline>() { simplygon.CreatePassthroughPipeline() };
for (int i = 0; i < quality.NumberOfLODLevels(); i++)
{
spPipeline pipeline = null;
if (i == quality.NumberOfLODLevels() - 1 && quality.RemeshLastLOD)
{
pipeline = CreateRemeshingPipeline(simplygon, quality.GetScreenSizeForIndex(i, true), quality.AutomaticTextureSizeMultiplier);
pipelines[0].AddCascadedPipeline(pipeline);
}
else
{
pipeline = CreateReductionPipelineForScreenHeight(simplygon, quality.GetScreenSizeForIndex(i, false));
pipelines[i].AddCascadedPipeline(pipeline);
}
pipelines.Add(pipeline);
}
return pipelines;
}
/// <summary>
/// Create remeshing pipeline with HDRP casters for Base, mask and normal maps.
/// </summary>
private static spPipeline CreateRemeshingPipeline(ISimplygon simplygon, uint screenHeight, float automaticTextureSizeMultiplier)
{
var pipeline = simplygon.CreateRemeshingPipeline();
var settings = pipeline.GetRemeshingSettings();
settings.SetOnScreenSize(screenHeight);
using var simplygonMappingImageSettings = pipeline.GetMappingImageSettings();
simplygonMappingImageSettings.SetGenerateMappingImage(true);
simplygonMappingImageSettings.SetGenerateTexCoords(true);
simplygonMappingImageSettings.SetGenerateTangents(true);
simplygonMappingImageSettings.SetUseFullRetexturing(true);
simplygonMappingImageSettings.SetApplyNewMaterialIds(true);
simplygonMappingImageSettings.SetUseAutomaticTextureSize(true);
simplygonMappingImageSettings.SetAutomaticTextureSizeMultiplier(automaticTextureSizeMultiplier);
// Add a caster for the albedo map channel.
using var albedoMapCaster = simplygon.CreateComputeCaster();
using var albedoMapCasterSettings = albedoMapCaster.GetComputeCasterSettings();
albedoMapCasterSettings.SetMaterialChannel("BaseMap");
albedoMapCasterSettings.SetOutputColorSpace(EImageColorSpace.sRGB);
pipeline.AddMaterialCaster(albedoMapCaster, 0);
// Add a caster for the albedo map channel.
using var maskMapCaster = simplygon.CreateComputeCaster();
using var maskMapCasterSettings = maskMapCaster.GetComputeCasterSettings();
maskMapCasterSettings.SetMaterialChannel("MaskMap");
maskMapCasterSettings.SetOutputColorSpace(EImageColorSpace.Linear);
pipeline.AddMaterialCaster(maskMapCaster, 0);
// Add a caster for normals.
using var normalsCaster = simplygon.CreateComputeCaster();
using var normalsCasterSettings = normalsCaster.GetComputeCasterSettings();
normalsCasterSettings.SetMaterialChannel("NormalMap");
normalsCasterSettings.SetOutputColorSpace(EImageColorSpace.Linear);
pipeline.AddMaterialCaster(normalsCaster, 0);
return pipeline;
}
/// <summary>
/// Create reduction pipeline with screen size as reduction target.
/// </summary>
private static spPipeline CreateReductionPipelineForScreenHeight(ISimplygon simplygon, uint screenHeight)
{
var pipeline = simplygon.CreateReductionPipeline();
spReductionSettings reductionSettings = pipeline.GetReductionSettings();
reductionSettings.SetReductionTargets(EStopCondition.All, false, false, false, true);
reductionSettings.SetReductionTargetOnScreenSize(screenHeight);
return pipeline;
}
}
}
BuildPrefabsDuringBuild.cs
This script should be placed in an Editor
folder. Adding this script can make your Player builds take very long time.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;
namespace Simplygon.Examples.LODPrefab
{
public class BuildPrefabsDuringBuild : IPreprocessBuildWithReport
{
public int callbackOrder => 0;
/// <summary>
/// Build all prefabs missing LOD data when building player.
/// </summary>
public void OnPreprocessBuild(BuildReport report)
{
Debug.Log("Build missing LODs");
PrefabLODBuilder.BuildLODs();
}
}
}
LODSettingsEditor.cs
This script should be placed in an Editor
folder.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using UnityEditor;
using UnityEngine;
namespace Simplygon.Examples.LODPrefab
{
[CustomEditor(typeof(LODSettings))]
public class LODSettingsEditor : Editor
{
/// <summary>
/// Renders editor UI for LODSettings.
/// </summary>
override public void OnInspectorGUI()
{
var standin = target as LODSettings;
EditorGUILayout.LabelField("LOD Quality:");
var currentQuality = standin.Quality;
var newQuality = EditorGUILayout.ObjectField(standin.Quality, typeof(LODQuality), false) as LODQuality;
if (currentQuality != newQuality)
{
standin.Quality = newQuality;
EditorUtility.SetDirty(standin);
AssetDatabase.SaveAssets();
}
string assetPath = AssetDatabase.GetAssetPath(standin);
if (assetPath != null && assetPath.Length > 0 && standin.Quality)
{
EditorGUILayout.Separator();
if (GUILayout.Button("Build LODs"))
{
PrefabLODBuilder.BuildLODsForPrefab(standin.gameObject);
}
if (standin.HasBuildLOD)
{
if (GUILayout.Button("Destroy LODs"))
{
GameObject prefab = PrefabUtility.LoadPrefabContents(assetPath);
PrefabLODBuilder.ClearPrefab(prefab);
PrefabUtility.SaveAsPrefabAsset(prefab, assetPath);
PrefabUtility.UnloadPrefabContents(prefab);
}
}
}
}
}
}