Calculating LOD transitions when using triangle ratio as reduction target

Written by Jesper Tingvall, Product Expert, Simplygon

Disclaimer: The code in this post is written using version 10.1.11000.0 of Simplygon and Unity 2021.3.4f1. 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

This blog covers how to use GetResultDeviation to calculate when to LOD transition. In normal case we strongly recommend to use screen size or max deviation as reduction target, but if you have a specific need which request reduction ratio or exact triangle count this blog will describe how to calculate when to switch to that LOD.

We are going to use this to populate a Unity LODComponent. We are also going cover how to reuse materials from LOD0 automatically.

Prerequisites

This example will use the Simplygon integration in Unity, but the same concepts can be applied to all other integrations of the Simplygon API.

For the Unity specific parts of the blog it is suggested to first read Using Simplygon with Unity LODGroups as we are going to use the same code skeleton and concepts and will not go into great detail.

Problem to solve

We have several assets and want to generate LODs for them. As metric for quality we want to use reduction ratio; where each LOD should have only a percentage of original triangles left. We are then going to use these LODs in our game engine and wonder when it is appropriate to switch to them.

Video games assets; crate, chest, machete, sofa and a turret.

Solution

The solution is done in several steps. First we need to create a chain of cascaded reduction pipelines using triangle ratio as quality metric. We are then going to use the resulting deviation to calculate when to switch to it. Lastly we are going to do some Unity specific post processing where we hook it up to a LODGroup.

Calculate relative triangle ratios from absolute triangle ratios

When creating a LOD chain we strongly suggest to create it via a set of cascaded pipelines. In a cascaded pipeline the previous LOD is used as input to create the LOD next in the chain. The benefits of this is twofold:

  • You work with less and less data, so you have quicker execution time then if you would always start from LOD0.
  • We minimize the visual difference to the LODs closest to us, rather LOD0. It is more likely that we go from, for example, LOD5 to LOD4 then directly from LOD5 to LOD0.

As we use the previous LOD as input we need to rethink how we calculate the triangle ratio used for reduction. We are used to this being in relative to LOD0. Thus we need to recalculate it so it instead is based upon previous results. This function does that.

// Calculate from absolute to relative LOD Ratios.
// In cascaded pipelines we base the next LOD on previous LOD. So we need to take previously processed LOD's ratio into account.
public static float[] CalculateRelativeLODRatiosFromAbsoluteLODRatios(float[] ratios)
{
    float[] new_ratios = new float[ratios.Length];
    for (int i = 0; i < ratios.Length; i++)
    {
        float divider = 1;
        if (i > 1)
            divider = ratios[i - 1];

        new_ratios[i] = ratios[i] / divider;
    }
    return new_ratios;
}

Create and run cascaded reduction pipelines

Once we have our relative reduction ratios suitable for creating cascaded pipelines we can start creating them. First we create a basic function which creates a reduction pipeline for a specific ratio.

// Create reduction pipeline for specified relative reduction ratio.
private static spPipeline CreateReductionPipelineForTriangleRatio(ISimplygon simplygon, float triangleRatio)
{
    var pipeline = simplygon.CreateReductionPipeline();
    spReductionSettings reductionSettings = pipeline.GetReductionSettings();
    reductionSettings.SetReductionTargets(EStopCondition.All, true, false, false, false);
    reductionSettings.SetReductionTargetTriangleRatio(triangleRatio);
    return pipeline;
}

We can then create pipelines for each relative triangle ratio, and create a cascaded chain of them where each previous LOD is used as input for next one.

public static List<GameObject> SimplygonProcess(GameObject selectedGameObject, IEnumerable<float> triangleRatios, out List<float> screenSizes)
{
...

    // Create reduction pipelines.
    for (int i = 0; i < triangleRatios.Count(); i++)
    {
        var pipeline = CreateReductionPipelineForTriangleRatio(simplygon, triangleRatios.ElementAt(i));
        pipelines.Add(pipeline);
    }

    // Add each pipeline as cascaded to last one.
    for (int i = 0; i < pipelines.Count - 1; i++)
    {
        pipelines[i].AddCascadedPipeline(pipelines[i + 1]);
    }

    // Process LOD.
    pipelines[0].RunScene(sgScene, EPipelineRunMode.RunInThisProcess);

    // Calculate resulted screen sizes.
    screenSizes = CalculateResultingScreenSizes(pipelines);

Calculate resulting screen size from result deviation

As you see we lastly calculated the resulting screen sizes for each step in our pipeline. This can be done via reduction pipeline's GetResultDeviation. This returns the introduced error of the last collapse it performed. This takes not only geometrical errors into account, but also things like texture stretches, changes in normals, skinning and more.

To calculate how many pixels the resulting LOD should be able to be rendered at, size on screen, with less then one pixels of error we can use the formula ScreenSize = SceneDiameter / ResultDeviation. All values needed to calculate this can be fetched from the processed scene.

// Calculate resulting screen size.
private static List<float> CalculateResultingScreenSizes(List<spPipeline> pipelines)
{
    var calculatedScreenSizes = new float[pipelines.Count];

    for (int i = 0; i < pipelines.Count; i++)
    {
        var deviation = ((spReductionPipeline)pipelines[i]).GetResultDeviation();
        var sceneDiameter = 2 * pipelines[i].GetProcessedScene().GetRadius();
        var screenSize = sceneDiameter / deviation;
        calculatedScreenSizes[i] = screenSize;
    }
    return calculatedScreenSizes.ToList();
}

Use generated LODs in Unity's LODGroup

How to connect screen size to Unity's LODGroup's screenRelativeTransitionHeight is covered in detail in our blog about using Simplygon with Unity LODGroups, so we will not go into great detail here.

The calculated screen after performing our reductions can be larger then game's max render height.

Due to the way Unity handles LOD transitions there is no way to handle transitions where object is covering more then entire screen height. This could be the case for when we view large detailed objects we view up close. Detailed castle walls would be a good example. In those cases Unity currently only offer a solution to start transition when we move the camera so far away from the wall as it is no longer covering entire screen height.

To handle this better one would need to slice up that asset into smaller chunks. This is not a bad idea as it would also allow to better frustum and occlusion culling. This is outside the scope of this blog post and we will focus on prop assets where we do not expect the player to view them at larger then full screen height most of the time.

If we set relative transition level that is higher than one Unity will reject it. Thus we need to handle larger then max screen height values. There is no real "correct" way of doing this and another approach would be to throw all LODs that are to be displayed above max height pixels away since we have no reason to keep them in the LOD chain. I we have a lot of values that are outside our max screen resolution then it is an indication of that perhaps our LOD0 asset would benefit from some reduction before being used in game.

What we are going to do instead of throwing them away is remapping the values larger then max screen height. We are going to fit them into the space between max height and the LODs levels that is below max height.

This introduces sometimes very odd LOD transitions where transitions are happening very close to each other, so in a practical scenario consider optimizing the original asset or throw some LOD levels away.

// Remap values outside of largest screen size to fit underneat it.
// This does not completely make sense in terms of calculated deviation, but it is best we can do as we otherwise will get error on adding it to LODGroup.
// If this function is needed indicates perhaps that our original asset was to high poly... or that we should use other metric then ratio for optimization :).
public static List<float> RemapOutsideValues(List<float> screenSizes, float maxScreenSize)
{
    List<float> aboveMaxScreenSize = new List<float>();
    List<float> underMaxScreenSize = new List<float>();

    // Divide up into the values which fit under max screen size and those that are above
    foreach (var value in screenSizes)
    {
        if (value >= maxScreenSize)
        {
            aboveMaxScreenSize.Add(value);
        }
        else
        {
            underMaxScreenSize.Add(value);
        }
    }

    // No need for remapping
    if (aboveMaxScreenSize.Count == 0)
        return underMaxScreenSize;

    float fitScreenSizeToStart = 0;
    if (underMaxScreenSize.Count > 0)
        fitScreenSizeToStart = underMaxScreenSize.Max();

    float scaleFromMaxValue = aboveMaxScreenSize.Max();

    var scaleDelta = scaleFromMaxValue - fitScreenSizeToStart;

    // For each value in aboveMaxScreenSize scale so it fits in space between 1 and the first screen size in under max screen size list.
    aboveMaxScreenSize.Reverse();
    foreach (var value in aboveMaxScreenSize)
    {
        var newScreenSize = (value - fitScreenSizeToStart) / scaleDelta * (maxScreenSize - fitScreenSizeToStart) + fitScreenSizeToStart;
        underMaxScreenSize.Insert(0, newScreenSize);
    }

    return underMaxScreenSize;
}

Once we have a remap helper function we can attaches all generated LODs to a LODGroup and transition to them at appropriate relative screen sizes.

// Attach LODs to LODGroup.
public static void AttachLODsToLODGroup(LODGroup lodGroup, List<GameObject> lodGameObjects, List<float> screenSizes)
{
    if (lodGameObjects.Count != screenSizes.Count())
    {
        Debug.LogError("Incorrect number of LODs and screen sizes Lod group", lodGroup);
        return;
    }

    // Warn if we have calculated screen sizes larger then game's max resolution.
    // This is normally not a problem as some objects in the game can be expected to be rendered very zoomed in. 
    // What this could also indicate is that we should use a lower poly model as LOD0.
    for (int i = 0; i < screenSizes.Count; i++)
    {
        var size = screenSizes[i];
        if (size >= SimplygonLODProcessor.GAME_RESOLUTION_HEIGHT)
        {
            Debug.LogWarning("Calculated screen size for LOD" + (i + 1) + ": " + size + " pixels is higher then game's max resolution. Screen size will be remapped.");
        }
    }

    // Remap outside screen sizes. This is needed so SetLODs does not crash.
    var remappedScreenSizes = RemapOutsideValues(screenSizes, SimplygonLODProcessor.GAME_RESOLUTION_HEIGHT);

    var lods = new LOD[lodGameObjects.Count];
    for (int i = 0; i < lods.Length; i++)
    {
        lods[i] = new LOD();
        var relativeScreenSize = GetRelativeScreenSize(remappedScreenSizes.ElementAt(i), SimplygonLODProcessor.GAME_RESOLUTION_HEIGHT);
        lods[i] = CreateLODFromChildRenderers(lodGameObjects[i], relativeScreenSize);
        Debug.Log("LODGroup: LOD" + (i + 1) + " | " + remappedScreenSizes.ElementAt(i) + " | " + relativeScreenSize);
    }
    lodGroup.SetLODs(lods);
}

The above code snippet nearly sets up our LODComponent with all required data.

LODGroup with generated LODs at correct transition.

Reuse original materials in Unity

We use USD as intermediary file format between Simplygon and Unity. One drawback of this is that it creates copies of the material, even if we are only using reduction and could share material. We are thus introducing a clean up function which allows a LOD to reuse LOD0s material. This code should be useful for anyone scripting with our Unity integration.

We are going to start by introducing a helper function which sets a LOD's renderer's shaderMaterials to same as LOD0.

// Reuse materials from LOD0 to LOD renderer.
public static void ReuseOriginalMaterials(Renderer lod0Renderer, Renderer lodRenderer)
{
    if ((bool)(lod0Renderer) != (bool)(lodRenderer)) // Only one of objects has renderer
    {
        Debug.LogError("Original asset and current LOD has not same renderers", lodRenderer);
        return;
    }
    else if (!lod0Renderer && !lodRenderer) // No renderer
    {
        return;
    }
    else if (lod0Renderer.sharedMaterials.Length != lodRenderer.sharedMaterials.Length)
    {
        Debug.LogError("Original asset and current LOD has not same amount of renderers", lodRenderer);
        return;
    }

    lodRenderer.sharedMaterials = lod0Renderer.sharedMaterials;
}

With that helper function in place we can introduce a function which takes the generated LOD GameObject and original LOD0 GameObject and then recursively traverse it transform per transform and sets all renderers' material to same as LOD0. One thing to look out for here is that Simplygon can not handle all characters Unity can in the hierarchy, all of those characters will be replaced with "_" characters. So if this function fails it is likely that you need to look over naming of the game objects, spaces for instance are not supported.

// Recursively reuse materials from LOD0 to created LOD.
public static void ResuseOriginalMaterials(GameObject currentLod, GameObject lod0)
{
    // Recursively go through each objects hierarcy
    for (int i = 0; i < lod0.transform.childCount; i++)
    {
        var lod0Child = lod0.transform.GetChild(i);
        var currentLodChild = currentLod.transform.Find(lod0Child.name);
        if (!currentLodChild)
        {
            Debug.LogError("Original asset and current LOD has not same hierarcy.", currentLod);
            return;
        }
        else
        {
            ResuseOriginalMaterials(currentLodChild.gameObject, lod0Child.gameObject);
        }
    }

    // Reuse materials
    var lod0Renderers = lod0.GetComponent<Renderer>();
    var lodRenderer = currentLod.GetComponent<Renderer>();
    ReuseOriginalMaterials(lod0Renderers, lodRenderer);
}

Putting it all together

Once all puzzle pieces are in place we can put our script together. We start by converting from absolute to relative LOD ratios.

 private static void ProcessObject(GameObject lod0, float[] lodRatios)
        {
            Debug.Log($"Generating LODs for {lod0.name}", lod0);

            // Create LODGroup component.
            var lodGroup = lod0.GetComponent<LODGroup>();
            if (!lodGroup)
                lodGroup = lod0.AddComponent<LODGroup>();

            if (SimplygonLODProcessor.ABSOLUTE_LOD_RATIOS.Count() < 1)
            {
                Debug.LogError($"No LODS to process for {lod0.name}", lod0);
                return;
            }

            // Convert from absolute to relative LOD ratios
            var relativeLodRatios = SimplygonLODProcessor.CalculateRelativeLODRatiosFromAbsoluteLODRatios(lodRatios);

After that we can process our LODs and will get back the calculated screen size.

            // Process LODs
            var Lods = SimplygonLODProcessor.SimplygonProcess(lod0, relativeLodRatios, out var screenSizes);

            // Post processing of LODs
            if (Lods != null)
            {

We will clean up the assets after USD import.

                // Cleanup USD components and reuse original material
                foreach (var lod in Lods)
                {
                    SimplygonUSDHelper.CleanUpUSDComponents(lod);
                    SimplygonUSDHelper.ResuseOriginalMaterials(lod, lod0);
                }

After that we can assign all LODs, including LOD0, to our LODGroup.

                // Assign all renderers to LOD group (including LOD0).
                Lods.Insert(0, lod0);
                screenSizes.Add(SimplygonLODGroupHelper.GetRelativeScreenSize(SimplygonLODProcessor.CULL_SCREEN_SIZE, SimplygonLODProcessor.GAME_RESOLUTION_HEIGHT));
                SimplygonLODGroupHelper.AttachLODsToLODGroup(lodGroup, Lods, screenSizes);
                Lods.Remove(lod0);

Lastly we also parent the created LODs to our LOD0 object.

                // Set LODs as childs to processed game object.
                foreach (var currentLod in Lods)
                {
                    currentLod.transform.parent = lod0.transform;
                }
            }
            else
            {
                Debug.LogError($"Failed creating LODs for {lod0.name}", lod0);
            }
        }

Result

To investigate our script we process 5 different assets with the following settings.

LOD level Triangle ratio from LOD0
LOD1 75%
LOD2 50%
LOD3 25%

Chest

Four LODs of chest with wireframe showing

LOD level Triangle count Deviation from LOD0 Calculated screen size Remapped screen size LODGroup screenRelativeTransitionHeight
LOD0 103 k - - - -
LOD1 77 k 0.5 mm 2 253 pixels 1 080 pixels 100%
LOD2 39 k 2.1 mm 572 pixels 572 pixels 53%
LOD3 19 k 4.5 mm 266 pixels 266 pixels 25%

Turret

Four LODs of turret with wireframe showing

LOD level Triangle count Deviation from LOD0 Calculated screen size Remapped screen size LODGroup screenRelativeTransitionHeight
LOD0 206 k - - - -
LOD1 154 k 0.05 mm 3 8025 pixels 1 080 pixels 100%
LOD2 77 k 1.4 mm 1 659 pixels 487 pixels 45%
LOD3 39 k 4.8 mm 467 pixels 467 pixels 43%

Sofa

Four LODs of sofa with wireframe showing

LOD level Triangle count Deviation from LOD0 Calculated screen size Remapped screen size LODGroup screenRelativeTransitionHeight
LOD0 52 k - - - -
LOD1 39 k 0.3 mm 11 980 pixels 1 080 pixels 100%
LOD2 19 k 1.8 mm 1 904 pixels 684 pixels 63%
LOD3 10 k 5.4 mm 634 pixels 634 pixels 59%

Machete

Four LODs of machete with wireframe showing

LOD level Triangle count Deviation from LOD0 Calculated screen size Remapped screen size LODGroup screenRelativeTransitionHeight
LOD0 3 032 - - - -
LOD1 2 273 0.5 mm 1 535 pixels 1 080 pixels 100%
LOD2 1 136 1.4 mm 535 pixels 535 pixels 50%
LOD3 567 3.7 mm 206 pixels 206 pixels 19%

Crate

Four LODs of crate with wireframe showing

LOD level Triangle count Deviation from LOD0 Calculated screen size Remapped screen size LODGroup screenRelativeTransitionHeight
LOD0 7 k - - - -
LOD1 5 k 2.8 mm 303 pixels 303 pixels 28%
LOD2 2 k 7.7 mm 114 pixels 114 pixels 11%
LOD3 1 k 22.7 mm 38 pixels 38 pixels 4%

There are a couple of things we can notice from the result table.

  • First is to notice that the resulting deviation, the error we have introduced in the model, varies a lot depending on model. When removing triangles different models behaves differently, some keep their shape better then others.
  • The Sofa LOD2-LOD3 and Turret LOD2-LOD3 are very close to each other. In our case it is a artifact introduced form our screen size remapping. What would be more ideal in this case would perhaps to throw one of the LODs away. It is however important to notice that this artifact can appear without screen size remapping as we have no guarantee that the error will increase somewhat linear per triangle removed. This is one of the reasons why we
  • We can see that it behaves differently for high poly and low poly assets where high poly assets tend to have very little deviation in LOD1 & LOD2, where as low poly assets almost instantly starts to deviate from LOD0. While our game probably has a well defined art style that is either high poly or low poly we probably have variance in the poly density between different assets. This can particularly be true if we have outsourced production, or are using lots of asset packs.

When to transition between LOD levels?

The core of our blog is to decide when to transition between the different LOD levels. Can we formulate a rule applicable for all assets; like swap down to 50% triangles at 50% screen size? Let us investigate!

LOD2 Here we will just compare the LOD2s who's calculated screen size is not remapped. As we can see for 50% reduction our LOD transition falls in 53% - 11% screen coverage depending on asset.

LOD level Calculated screen size LODGroup screenRelativeTransitionHeight
Chest LOD2 572 pixels 53%
Machete LOD2 535 pixels 50%
Crate LOD2 114 pixels 11%

LOD3 The spread of when to transition to LOD3 is even larger. We get everything from 59% down to 4%!

LOD level Calculated screen size LODGroup screenRelativeTransitionHeight
Chest LOD3 266 pixels 25%
Turret LOD3 467 pixels 43%
Sofa LOD3 634 pixels 59%
Machete LOD3 206 pixels 19%
Crate LOD3 38 pixels 4%

We can conclude that is not possible to decide a transition distance that would work for all assets when we work with triangle ratio.

Another way of saying this is: It is possible to either decide the number of triangles a LOD should have, but we can not know when to transition to it. Or we can decide when to switch to the LOD, but not know the exact number of triangles in that LOD. We recommend thinking about in the second way, decide when to perform LOD transition and then give that LOD level as many triangles as it needs to look decent.

Complete scripts

SimplygonLODGroupMenu.cs

// Copyright (c) Microsoft Corporation. 
// Licensed under the MIT license. 

using UnityEditor;
using UnityEngine;
using System.Linq;

namespace Simplygon.Examples.UnityLODGroupResultingDeviation
{
    public class SimplygonLODGroupMenu
    {
        [MenuItem("Simplygon/LODGroup/Create LODS Prefabs from triangle ratio")]
        static void CreateLODSForPrefabs()
        {
            if (Selection.objects.Length > 0)
            {
                foreach (var selectedObject in Selection.objects)
                {
                    var selectedGameObject = selectedObject as GameObject;
                    if (selectedGameObject)
                    {
                        ProcessObject(selectedGameObject, SimplygonLODProcessor.ABSOLUTE_LOD_RATIOS);
                    }
                    else
                    {
                        Debug.LogWarning($"{selectedObject.name} is not a GameObject and will be ignored,");
                    }
                }
            }
            else
            {
                Debug.LogWarning("No objects selected");
            }
        }

        private static void ProcessObject(GameObject lod0, float[] lodRatios)
        {
            Debug.Log($"Generating LODs for {lod0.name}", lod0);

            // Create LODGroup component.
            var lodGroup = lod0.GetComponent<LODGroup>();
            if (!lodGroup)
                lodGroup = lod0.AddComponent<LODGroup>();

            if (SimplygonLODProcessor.ABSOLUTE_LOD_RATIOS.Count() < 1)
            {
                Debug.LogError($"No LODS to process for {lod0.name}", lod0);
                return;
            }

            // Convert from absolute to relative LOD ratios
            var relativeLodRatios = SimplygonLODProcessor.CalculateRelativeLODRatiosFromAbsoluteLODRatios(lodRatios);

            // Process LODs
            var Lods = SimplygonLODProcessor.SimplygonProcess(lod0, relativeLodRatios, out var screenSizes);

            // Post processing of LODs
            if (Lods != null)
            {
                // Cleanup USD components and reuse original material
                foreach (var lod in Lods)
                {
                    SimplygonUSDHelper.CleanUpUSDComponents(lod);
                    SimplygonUSDHelper.ResuseOriginalMaterials(lod, lod0);
                }

                // Assign all renderers to LOD group (including LOD0).
                Lods.Insert(0, lod0);
                screenSizes.Add(SimplygonLODGroupHelper.GetRelativeScreenSize(SimplygonLODProcessor.CULL_SCREEN_SIZE, SimplygonLODProcessor.GAME_RESOLUTION_HEIGHT));
                SimplygonLODGroupHelper.AttachLODsToLODGroup(lodGroup, Lods, screenSizes);
                Lods.Remove(lod0);

                // Set LODs as childs to processed game object.
                foreach (var currentLod in Lods)
                {
                    currentLod.transform.parent = lod0.transform;
                }
            }
            else
            {
                Debug.LogError($"Failed creating LODs for {lod0.name}", lod0);
            }
        }
    }
}

SimplygonLODProcessor.cs

// Copyright (c) Microsoft Corporation. 
// Licensed under the MIT license. 

using Simplygon.Unity.EditorPlugin;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

namespace Simplygon.Examples.UnityLODGroupResultingDeviation
{
    public class SimplygonLODProcessor
    {
        // Settings
        public static uint GAME_RESOLUTION_HEIGHT = 1080;
        public static float LOD_MAX_PIXEL_DIFFERENCE = 1;
        public static float CULL_SCREEN_SIZE = 20;

        // Absolute LOD ratios.
        // LOD1 will be 0.75 of LOD0's triangle count
        // LOD2 will be 0.50 of LOD0's triangle count
        // LOD3 will be 0.25 of LOD0's triangle count
        public static float[] ABSOLUTE_LOD_RATIOS = new float[] { 0.75f, 0.5f, 0.25f };


        // Calculate from absolute to relative LOD Ratios.
        // In cascaded pipelines we base the next LOD on previous LOD. So we need to take previously processed LOD's ratio into account.
        public static float[] CalculateRelativeLODRatiosFromAbsoluteLODRatios(float[] ratios)
        {
            float[] new_ratios = new float[ratios.Length];
            for (int i = 0; i < ratios.Length; i++)
            {
                float divider = 1;
                if (i > 1)
                    divider = ratios[i - 1];

                new_ratios[i] = ratios[i] / divider;
            }
            return new_ratios;
        }


        // Create reduction pipeline for specified relative reduction ratio.
        private static spPipeline CreateReductionPipelineForTriangleRatio(ISimplygon simplygon, float triangleRatio)
        {
            var pipeline = simplygon.CreateReductionPipeline();
            spReductionSettings reductionSettings = pipeline.GetReductionSettings();
            reductionSettings.SetReductionTargets(EStopCondition.All, true, false, false, false);
            reductionSettings.SetReductionTargetTriangleRatio(triangleRatio);
            return pipeline;
        }


        // Create folders for our LODs.
        private static string CreateLODFolder()
        {
            string baseFolder = "Assets/LODs";
            if (!AssetDatabase.IsValidFolder(baseFolder))
            {
                AssetDatabase.CreateFolder("Assets", "LODs");
            }

            return baseFolder;
        }


        // Got folder to place our LODs into.
        private static string GetAssetFolder(GameObject selectedGameObject)
        {
            string baseFolder = CreateLODFolder();
            string assetFolderGuid = AssetDatabase.CreateFolder(baseFolder, selectedGameObject.name);
            string assetFolderPath = AssetDatabase.GUIDToAssetPath(assetFolderGuid);
            return assetFolderPath;
        }


        // Process LODs for game object
        public static List<GameObject> SimplygonProcess(GameObject selectedGameObject, IEnumerable<float> triangleRatios, out List<float> screenSizes)
        {
            // We do not have anything to process
            if (triangleRatios.Count() < 1)
            {
                screenSizes = null;
                return new List<GameObject>();
            }

            using (ISimplygon simplygon = Loader.InitSimplygon(out EErrorCodes simplygonErrorCode, out string simplygonErrorMessage))
            {
                if (simplygonErrorCode == EErrorCodes.NoError)
                {
                    var exportTempDirectory = SimplygonUtils.GetNewTempDirectory();
                    var selectedGameObjects = new List<GameObject>() { selectedGameObject };

                    List<spPipeline> pipelines = new List<spPipeline>();
                    List<GameObject> lods = new List<GameObject>();

                    using spScene sgScene = SimplygonExporter.Export(simplygon, exportTempDirectory, selectedGameObjects);
                    try
                    {
                        // Create reduction pipelines.
                        for (int i = 0; i < triangleRatios.Count(); i++)
                        {
                            var pipeline = CreateReductionPipelineForTriangleRatio(simplygon, triangleRatios.ElementAt(i));
                            pipelines.Add(pipeline);
                        }

                        // Add each pipeline as cascaded to last one.
                        for (int i = 0; i < pipelines.Count - 1; i++)
                        {
                            pipelines[i].AddCascadedPipeline(pipelines[i + 1]);
                        }

                        // Process LOD.
                        pipelines[0].RunScene(sgScene, EPipelineRunMode.RunInThisProcess);

                        // Calculate resulted screen sizes.
                        screenSizes = CalculateResultingScreenSizes(pipelines);

                        // Import created LODs into Unity.
                        string assetFolderPath = GetAssetFolder(selectedGameObject);
                        int startingLodIndex = 1;
                        SimplygonImporter.Import(simplygon, pipelines[0], ref startingLodIndex,
                            assetFolderPath, selectedGameObject.name, lods);
                    }
                    finally
                    {
                        foreach (var pipeline in pipelines)
                            pipeline?.Dispose();
                    }

                    return lods;
                }
                else
                {
                    Debug.LogError($"Simplygon initializing failed ({simplygonErrorCode}): " + simplygonErrorMessage);
                    screenSizes = null;
                    return null;
                }
            }
        }


        // Calculate resulting screen size.
        private static List<float> CalculateResultingScreenSizes(List<spPipeline> pipelines)
        {
            var calculatedScreenSizes = new float[pipelines.Count];

            for (int i = 0; i < pipelines.Count; i++)
            {
                var deviation = ((spReductionPipeline)pipelines[i]).GetResultDeviation();
                var sceneDiameter = 2 * pipelines[i].GetProcessedScene().GetRadius();
                var screenSize = sceneDiameter / deviation;
                calculatedScreenSizes[i] = screenSize;
            }
            return calculatedScreenSizes.ToList();
        }
    }
}

SimplygonLODGroupHelper.cs

// Copyright (c) Microsoft Corporation. 
// Licensed under the MIT license. 

using System.Collections.Generic;
using UnityEngine;
using System.Linq;

namespace Simplygon.Examples.UnityLODGroupResultingDeviation
{
    public class SimplygonLODGroupHelper
    {
        // Create LOD for specific relative screen size for game object.
        public static LOD CreateLODFromChildRenderers(GameObject gameObject, float relativeScreenSize)
        {
            return new LOD(relativeScreenSize, gameObject.GetComponentsInChildren<Renderer>());
        }


        // Change from absolute screen size to relative screen size. In Unity this is measured against height.
        public static float GetRelativeScreenSize(float renderScreenSize, float maxScreenSize)
        {
            if (renderScreenSize > maxScreenSize)
            {
                throw new System.ArgumentOutOfRangeException("Calculated screen resolution (" + renderScreenSize + ") is higher then max game resolution height");
            }
            return Mathf.Clamp01(SimplygonLODProcessor.LOD_MAX_PIXEL_DIFFERENCE * renderScreenSize / maxScreenSize);
        }


        // Remap values outside of largest screen size to fit underneat it.
        // This does not completely make sense in terms of calculated deviation, but it is best we can do as we otherwise will get error on adding it to LODGroup.
        // If this function is needed indicates perhaps that our original asset was to high poly... or that we should use other metric then ratio for optimization :).
        public static List<float> RemapOutsideValues(List<float> screenSizes, float maxScreenSize)
        {
            List<float> aboveMaxScreenSize = new List<float>();
            List<float> underMaxScreenSize = new List<float>();

            // Divide up into the values which fit under max screen size and those that are above
            foreach (var value in screenSizes)
            {
                if (value >= maxScreenSize)
                {
                    aboveMaxScreenSize.Add(value);
                }
                else
                {
                    underMaxScreenSize.Add(value);
                }
            }

            // No need for remapping
            if (aboveMaxScreenSize.Count == 0)
                return underMaxScreenSize;

            float fitScreenSizeToStart = 0;
            if (underMaxScreenSize.Count > 0)
                fitScreenSizeToStart = underMaxScreenSize.Max();

            float scaleFromMaxValue = aboveMaxScreenSize.Max();

            var scaleDelta = scaleFromMaxValue - fitScreenSizeToStart;

            // For each value in aboveMaxScreenSize scale so it fits in space between 1 and the first screen size in under max screen size list.
            aboveMaxScreenSize.Reverse();
            foreach (var value in aboveMaxScreenSize)
            {
                var newScreenSize = (value - fitScreenSizeToStart) / scaleDelta * (maxScreenSize - fitScreenSizeToStart) + fitScreenSizeToStart;
                underMaxScreenSize.Insert(0, newScreenSize);
            }

            return underMaxScreenSize;
        }


        // Attach LODs to LODGroup.
        public static void AttachLODsToLODGroup(LODGroup lodGroup, List<GameObject> lodGameObjects, List<float> screenSizes)
        {
            if (lodGameObjects.Count != screenSizes.Count())
            {
                Debug.LogError("Incorrect number of LODs and screen sizes Lod group", lodGroup);
                return;
            }

            // Warn if we have calculated screen sizes larger then game's max resolution.
            // This is normally not a problem as some objects in the game can be expected to be rendered very zoomed in. 
            // What this could also indicate is that we should use a lower poly model as LOD0.
            for (int i = 0; i < screenSizes.Count; i++)
            {
                var size = screenSizes[i];
                if (size >= SimplygonLODProcessor.GAME_RESOLUTION_HEIGHT)
                {
                    Debug.LogWarning("Calculated screen size for LOD" + (i + 1) + ": " + size + " pixels is higher then game's max resolution. Screen size will be remapped.");
                }
            }

            // Remap outside screen sizes. This is needed so SetLODs does not crash.
            var remappedScreenSizes = RemapOutsideValues(screenSizes, SimplygonLODProcessor.GAME_RESOLUTION_HEIGHT);

            var lods = new LOD[lodGameObjects.Count];
            for (int i = 0; i < lods.Length; i++)
            {
                lods[i] = new LOD();
                var relativeScreenSize = GetRelativeScreenSize(remappedScreenSizes.ElementAt(i), SimplygonLODProcessor.GAME_RESOLUTION_HEIGHT);
                lods[i] = CreateLODFromChildRenderers(lodGameObjects[i], relativeScreenSize);
            }
            lodGroup.SetLODs(lods);
        }
    }
}

SimplygonUSDHelper.cs

// Copyright (c) Microsoft Corporation. 
// Licensed under the MIT license. 

using UnityEngine;
using Unity.Formats.USD;

namespace Simplygon.Examples.UnityLODGroupResultingDeviation
{
    public class SimplygonUSDHelper
    {
        // Remove game objects and components created during USD import.
        public static void CleanUpUSDComponents(GameObject objectToClean)
        {
            // Remove UsdPrimSources components
            foreach (var primSource in objectToClean.GetComponentsInChildren<UsdPrimSource>())
            {
                GameObject.DestroyImmediate(primSource);
            }

            // Remove Materials GameObject
            var materialsChild = objectToClean.transform.Find("Materials");
            if (materialsChild)
            {
                GameObject.DestroyImmediate(materialsChild.gameObject);
            }

            // Remove UsdAsset
            var usdAsset = objectToClean.GetComponent<UsdAsset>();
            if (usdAsset)
                GameObject.DestroyImmediate(usdAsset);
        }

        // Reuse materials from LOD0 to LOD renderer.
        public static void ReuseOriginalMaterials(Renderer lod0Renderer, Renderer lodRenderer)
        {
            if ((bool)(lod0Renderer) != (bool)(lodRenderer)) // Only one of objects has renderer
            {
                Debug.LogError("Original asset and current LOD has not same renderers", lodRenderer);
                return;
            }
            else if (!lod0Renderer && !lodRenderer) // No renderer
            {
                return;
            }
            else if (lod0Renderer.sharedMaterials.Length != lodRenderer.sharedMaterials.Length)
            {
                Debug.LogError("Original asset and current LOD has not same amount of renderers", lodRenderer);
                return;
            }

            lodRenderer.sharedMaterials = lod0Renderer.sharedMaterials;
        }

        // Recursively reuse materials from LOD0 to created LOD.
        public static void ResuseOriginalMaterials(GameObject currentLod, GameObject lod0)
        {
            // Recursively go through each objects hierarcy
            for (int i = 0; i < lod0.transform.childCount; i++)
            {
                var lod0Child = lod0.transform.GetChild(i);
                var currentLodChild = currentLod.transform.Find(lod0Child.name);
                if (!currentLodChild)
                {
                    Debug.LogError("Original asset and current LOD has not same hierarcy.", currentLod);
                    return;
                }
                else
                {
                    ResuseOriginalMaterials(currentLodChild.gameObject, lod0Child.gameObject);
                }
            }

            // Reuse materials
            var lod0Renderers = lod0.GetComponent<Renderer>();
            var lodRenderer = currentLod.GetComponent<Renderer>();
            ReuseOriginalMaterials(lod0Renderers, lodRenderer);
        }
    }
}
⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*