Optimize Unity character prefabs

Written by Jesper Tingvall, Product Expert, Simplygon

Disclaimer: The code in this post is written using version 10.4.148.0 of Simplygon and Unity 2022.3.37f1. 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 we will create a system for automatic character prefab automation. We will cover baking textures during reduction for URP and rebinding LODs to the original skeleton.

This blog is a combination of Automated Unity prefab optimizer and Reuse skeletons for Unity LODGroup. We will only cover the new parts so a reading of both blogs is recommended to give a full understanding.

Prerequisites

This example will use the Simplygon integration in Unity with URP render pipeline, but the same concepts can be applied to all other Unity render pipelines.

Problem to solve

We are making an Unity game with the URP render pipeline. We have several characters in the game that we want to optimize. We are suffering both from issues related to small triangles (or triangle count) and draw calls so want to address both.

3 characters, one alien, one human and one robot.

Our game features one humanoid protagonist (in the middle) that we want to give higher quality to then the rest of the characters.

Solution

We are going to extend the script introduced in Automated Unity prefab optimizer with two new functionalities:

  • We will add a reduction with material merging pipeline.
  • Instead of LODs having their own hierarchy in the resulting prefabs the will be parented to the original structure. Any SkinnedMeshRenderer will use original skeleton.

Bake material settings

To bake materials we will first add settings to our LODQuality settings object. We will start by creating an enum for each URP material channel we want to expose for baking.

[System.Serializable]
public enum URPMaterialChannel
{
    BaseMap,
    NormalMap,
    EmissionMap,
    MetallicMap,
    OcclusionMap
}

We also need to know which color space it should be baked into. That can be found in the SRGB Channel support table. We create a helper function that returns the correct color space.

static public EImageColorSpace GetColorSpace(URPMaterialChannel channel)
{
    if (channel == URPMaterialChannel.BaseMap || channel == URPMaterialChannel.EmissionMap)
        return EImageColorSpace.sRGB;
    return EImageColorSpace.Linear;
}

We can now add merge textures settings to LODQuality. MergeTexturesLODIndex indicates at what LOD level we will merge the textures. Any LOD level below will use the merged texture.

public class LODQuality : ScriptableObject
{
    ...

    // Merge texture settings
    public bool MergeTextures;
    public URPMaterialChannel[] MaterialChannels;
    public int MergeTexturesLODIndex;
    public uint TextureSize;
    ...

    /// <summary>
    /// Should we merge textures when processing this LOD level.
    /// </summary>
    public bool ShouldMergeTextures(int index)
    {
        return MergeTextures && index == MergeTexturesLODIndex;
    }

Here is our new setting object with our texture baking settings exposed.

LODQuality with material merging settings

Surface area chart aggregation mode

Before moving on we will deep dive into one setting that will give us better usage of UVs. Let us start by performing a simple reduction with material merging via Simplygon's UI. Our result to the right looks very good and we can't really see any issues with it from this angle.

Original and optimized aliens with settings showing.

If we instead look at the output texture we see something unexpected. A fourth of the texture is covered by textures for his tiny eye.

Texture with very large eye.

The reason for this is that we use the default value for ChartAggregatorMode. We are using the default value, TextureSizeProportions. In this mode the aggregated UV charts will be scaled to keep their relative pixel density relative to all other UV charts. Hence the eye keeps it relative dense pixel density.

Instead we will use SurfaceArea which scales the UVs depending on the geometry size in the world. After switching to this mode we can see that we have more sane UV usage.

texture with better UV usage.

Reduction with texture baking pipeline

Now lets add material baking to our reduction chain. In Unity we use compute casters to transfer material data. It works out of the box with URP/Lit shader, which is what our characters use. We will start with a helper function that creates a compute caster from a URPMaterialChannel. It also sets the correct color space.

private static spMaterialCaster CreateMaterialCaster(ISimplygon simplygon, URPMaterialChannel channel)
{
    var caster = simplygon.CreateComputeCaster();
    var casterSettings = caster.GetComputeCasterSettings();
    casterSettings.SetMaterialChannel(channel.ToString());
    casterSettings.SetOutputColorSpace(GetColorSpace(channel));
    return caster;
}

Now let's create a reduction pipeline with material casting. We start by calling CreateReductionPipelineForScreenHeight from the Automated Unity prefab optimized blog.

private static spReductionPipeline CreateReductionWithMaterialBakingPipeline(ISimplygon simplygon, uint screenHeight, uint textureSize, URPMaterialChannel[] channels)
{
    var pipeline = CreateReductionPipelineForScreenHeight(simplygon, screenHeight);

We then enable material casting by generate a mapping image and ensure it generates all properties we require.

    // Set up mapping image for texture baking.
    using var simplygonMappingImageSettings = pipeline.GetMappingImageSettings();
    simplygonMappingImageSettings.SetGenerateMappingImage(true);
    simplygonMappingImageSettings.SetGenerateTexCoords(true);
    simplygonMappingImageSettings.SetGenerateTangents(true);
    simplygonMappingImageSettings.SetUseFullRetexturing(true);
    simplygonMappingImageSettings.SetApplyNewMaterialIds(true);

Per our findings above, to get good UV usage we set the ChartAggregatorMode to SurfaceArea.

    // Use surface area for chart aggregator for good UV usage.
    simplygonMappingImageSettings.SetTexCoordGeneratorType(ETexcoordGeneratorType.ChartAggregator);
    var chartAggregatorSettings = simplygonMappingImageSettings.GetChartAggregatorSettings();
    chartAggregatorSettings.SetChartAggregatorMode(EChartAggregatorMode.SurfaceArea);

We set the texture dimensions according to our settings. When we add material casters for all textures we specified in our settings.

    // Set output texture size
    var outputSettings = simplygonMappingImageSettings.GetOutputMaterialSettings(0);
    outputSettings.SetTextureHeight(textureSize);
    outputSettings.SetTextureWidth(textureSize);

    // Add material casters for every specified material channel.
    foreach(var channel in channels)
    {
        pipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, channel), 0);
    }

    return pipeline;
}

We can now add our special reduction with material merging pipeline into our cascaded LOD chain. We modify CreatePipelinesForLODQuality from our previous blog to check if the should merge textures by calling ShouldMergeTextures, and if so use our newly created CreateReductionWithMaterialBakingPipeline function.

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 (quality.ShouldMergeTextures(i))
        {
            pipeline = CreateReductionWithMaterialBakingPipeline(simplygon, quality.GetScreenSizeForIndex(i, false), quality.TextureSize, quality.MaterialChannels);
            pipelines[i].AddCascadedPipeline(pipeline);
        }
        else
        {
            pipeline = CreateReductionPipelineForScreenHeight(simplygon, quality.GetScreenSizeForIndex(i, false));
            pipelines[i].AddCascadedPipeline(pipeline);
        }
        pipelines.Add(pipeline);
    }
    return pipelines;
}

Let us see how the texture baking works. On this asset we are doing this between LOD2 and LOD3. LOD2 is at 14% of original triangle count and LOD3 is at 7%. Before baking texture we use one 4k texture and two 2k textures for the weapons. Our baked texture is 512x512.

LOD2 - original textures
LOD3 - baked textures

Reassign skeleton

If we would process the character assets with the current pipeline then we would get an output structure like this. We would get one skeleton per LOD level. This is not what we want, we want to reuse the skeleton for each LOD level.

LODs in incorrect place in hieracy

We will use ReassignSkeleton from Reuse skeletons for Unity LODGroup to reassign the bones from the original skeleton. Another thing we need to solve are Renderers that are just attached to a bone in the skeleton, weapons tend to be attached in this fashion.

Let us create a function which iterates through all Renderers in our created LOD then parenting it to where the original Renderer is placed. If it is a SkinnedMeshRenderer we also reassign all bones to those in the original skeleton.

public static void ReassignRenderersToSkeleton(GameObject lod, GameObject original, int lodLevel)
{
    foreach(var renderer in lod.GetComponentsInChildren<Renderer>())
    {
        Debug.Log($"Moving {GetHierarcyPath(renderer.transform)} to original hierarcy.");
        var path= GetHierarcyPath(renderer.transform);
        var originalGameObject = original.transform.Find(path);

        if (originalGameObject != null)
        {
            var lodSkinnedRenderer = renderer.GetComponent<SkinnedMeshRenderer>();
            var orgSkinnedrenderer = originalGameObject.GetComponent<SkinnedMeshRenderer>();
            if (lodSkinnedRenderer && orgSkinnedrenderer)
            {
                ReassignSkeleton(lodSkinnedRenderer, orgSkinnedrenderer);
            }
            renderer.transform.parent = originalGameObject.parent;
            renderer.name += $"_LOD{lodLevel}";
        }
        else
        {
            Debug.LogWarning($"Did not find matching Renderer at {path}.");
        }
    }
}

We now need to update CreateLOD so we call ReassignRenderesToSkeleton. We do this during assigning the LODs to our LODGroup. Another change is that we now need to destroy the empty LOD skeleton from which we have moved all Renderers. Otherwise we get a scene filled with empty skeletons after processing, very scary.

private static void CreateLOD(GameObject prefab)
        {
            ...

                    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);
                        SkinnedMeshSkeletonRebinding.ReassignRenderersToSkeleton(lod, prefab, i+1);

                        // Destroy the empty skeleton we moved Renderers from.
                        GameObject.DestroyImmediate(simplygonProcessing.LODGameObjects[i]);
                    }

After processing the asset we get the LOD renders in correct place in the prefab's skeleton.

LODs at correct place in hierarcy, bound to correct skeleton.

Result

Now it is time to test our system. We create 3 LODQuality settings and assign them to our prefabs.

Main character

Since we only have one main character, and expect the player to look at it quite a lot we want to spend more quality on her. Our quality setting is set higher then for our low quality character pipelines. We don't expect the camera to view the main character from so far away so we do not build LODs for far away viewing, only down to 25% of screen height. We do not bake textures for our main character. We skip baking textures for her, enabling us to perform customization of her materials.

Quality settings for main character.

MercenaryFemale_01

LOD Triangle count Materials
LOD0 51k 7
LOD1 29k 7
LOD2 23k 7
LOD3 16k 7
LOD4 7k 7

Low quality character

For with low importance, and that we will have many of at the screen at once, we use these settings. We create a LOD chain that going from up close to very far away. We also set quality to a very low value 0.15 = 1/8, giving us an expected 8 pixels of error during LOD switching. The cross fade LOD transition hides this very well, enabling us to be perform very aggressive reduction. After LOD2 we merge all materials into one. This makes enemies far away only require one draw call. 512 might seem like a small texture size, but it works well for that far away.

Quality settings for low quality character

DignitaryNazdik

LOD Triangle count Materials
LOD0 14k 3
LOD1 2.8k 3
LOD2 1.5k 3
LOD3 627 1
LOD4 378 1
LOD5 336 1

Low quality character emissive

One of our lesser important characters are using emissive materials. Instead of wasting lots of texture memory and add an emissive channel to every less important character's material we create a variant that also bakes emissive channel.

Quality settings fow low quality character with emissive material

Droid-01_Desert

LOD Triangle count Materials
LOD0 27k 3
LOD1 7.3k 3
LOD2 3.9k 3
LOD3 1.8k 1
LOD4 976 1
LOD5 870 1

Resulting quality

When inspecting LODs it is best to do this in the correct context. For our characters that means we should inspect the quality and LOD transitions while they are animated. Can you spot the LOD transitions?

Future work

We now have a pipeline that can optimize our character prefabs with ease. Here are some improvements that we could look into next.

  • Using Vertex weights we can allocate more geometry to the areas where it matter, like the face, and less in areas where it is not that noticeable. This allows us to switch to lower LODs earlier.
  • We can use Bone reducer to make the skinning simpler to calculate on the GPU and CPU. This is especially useful for less powerful platforms.

Complete scripts

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.LODCharacterPrefab
{
    [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);
                    }
                }
            }
        }
    }
}

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.LODCharacterPrefab
{
    public class PrefabLODBuilder
    {
        /// <summary>
        /// Clear all generated LODs.
        /// </summary>
        [MenuItem("Simplygon/Characters/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/Characters/Rebuild all LODs")]
        public static void RebuildLODs()
        {
            BuildLODs(true);
        }

        /// <summary>
        /// Build LODs for prefabs missing generated LOD data.
        /// </summary>
        [MenuItem("Simplygon/Characters/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();

                    simplygonProcessing.Run(simplygon, pipelines[0], new List<GameObject>() { prefab }, EPipelineRunMode.RunInThisProcess, true, exportMeshesAsUnique: true);
                    prefab.GetComponent<LODSettings>().LODPath = simplygonProcessing.PrefabAssetFolder;

                    // To move skeletons from one skinnedmeshrenderer to another they need to be in the same place. Reset our transform.
                    prefab.transform.position = Vector3.zero;
                    prefab.transform.localScale = Vector3.one;
                    prefab.transform.rotation = Quaternion.identity;

                    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);
                        SkinnedMeshSkeletonRebinding.ReassignRenderersToSkeleton(lod, prefab, i+1);

                        // Destroy the empty skeleton we moved Renderers from.
                        GameObject.DestroyImmediate(simplygonProcessing.LODGameObjects[i]);
                    }


                    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) && type != typeof(SkinnedMeshRenderer))
                {
                    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 (quality.ShouldMergeTextures(i))
                {
                    pipeline = CreateReductionWithMaterialBakingPipeline(simplygon, quality.GetScreenSizeForIndex(i, false), quality.TextureSize, quality.MaterialChannels);
                    pipelines[i].AddCascadedPipeline(pipeline);
                }
                else
                {
                    pipeline = CreateReductionPipelineForScreenHeight(simplygon, quality.GetScreenSizeForIndex(i, false));
                    pipelines[i].AddCascadedPipeline(pipeline);
                }
                pipelines.Add(pipeline);
            }
            return pipelines;
        }

        /// <summary>
        /// Creates a reduction pipeline with material merging enabled.
        /// </summary>
        private static spReductionPipeline CreateReductionWithMaterialBakingPipeline(ISimplygon simplygon, uint screenHeight, uint textureSize, URPMaterialChannel[] channels)
        {
            var pipeline = CreateReductionPipelineForScreenHeight(simplygon, screenHeight);

            // Set up mapping image for texture baking.
            using var simplygonMappingImageSettings = pipeline.GetMappingImageSettings();
            simplygonMappingImageSettings.SetGenerateMappingImage(true);
            simplygonMappingImageSettings.SetGenerateTexCoords(true);
            simplygonMappingImageSettings.SetGenerateTangents(true);
            simplygonMappingImageSettings.SetUseFullRetexturing(true);
            simplygonMappingImageSettings.SetApplyNewMaterialIds(true);

            // Use surface area for chart aggregator for good UV usage.
            simplygonMappingImageSettings.SetTexCoordGeneratorType(ETexcoordGeneratorType.ChartAggregator);
            var chartAggregatorSettings = simplygonMappingImageSettings.GetChartAggregatorSettings();
            chartAggregatorSettings.SetChartAggregatorMode(EChartAggregatorMode.SurfaceArea);

            // Set output texture size
            var outputSettings = simplygonMappingImageSettings.GetOutputMaterialSettings(0);
            outputSettings.SetTextureHeight(textureSize);
            outputSettings.SetTextureWidth(textureSize);

            // Add material casters for every specified material channel.
            foreach(var channel in channels)
            {
                pipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, channel), 0);
            }

            return pipeline;
        }

        /// <summary>
        /// Add material caster for specified URP material channel name.
        /// </summary>
        private static spMaterialCaster CreateMaterialCaster(ISimplygon simplygon, URPMaterialChannel channel)
        {
            var caster = simplygon.CreateComputeCaster();
            var casterSettings = caster.GetComputeCasterSettings();
            casterSettings.SetMaterialChannel(channel.ToString());
            casterSettings.SetOutputColorSpace(GetColorSpace(channel));
            return caster;
        }

        /// <summary>
        /// Returns correct color space for material channel.
        /// From https://documentation.simplygon.com/SimplygonSDK_10.4.148.0/unity/concepts/materialmapping.html#srgb-channel-support
        /// </summary>
        static public EImageColorSpace GetColorSpace(URPMaterialChannel channel)
        {
            if (channel == URPMaterialChannel.BaseMap || channel == URPMaterialChannel.EmissionMap)
                return EImageColorSpace.sRGB;
            return EImageColorSpace.Linear;
        }

        /// <summary>
        /// Create reduction pipeline with screen size as reduction target.
        /// </summary>
        private static spReductionPipeline 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;
        }
    }
}

SkinnedMeshSkeletonRebinding.cs

This script should be placed in an Editor folder.

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

using UnityEngine;
using System.Collections.Generic;

namespace Simplygon.Examples.LODCharacterPrefab
{
    public class SkinnedMeshSkeletonRebinding
    {
        /// <summary>
        /// Get path to transform from it's root object.
        /// </summary>
        public static string GetHierarcyPath(Transform transform)
        {
            if (transform == transform.root)
                return "";

            var currentNode = transform;
            var path = "";
            while (currentNode != currentNode.root)
            {
                path = $"{currentNode.name}/{path}";
                currentNode = currentNode.parent;
            }
            return path.Substring(0, path.Length-1);
        }

        /// <summary>
        /// Parents GameObjects from lod hierarcy into original hierarcy.
        /// Reassigns skeleton for any SkinnedMeshRenderer to match original's bones.
        /// </summary>
        public static void ReassignRenderersToSkeleton(GameObject lod, GameObject original, int lodLevel)
        {
            foreach(var renderer in lod.GetComponentsInChildren<Renderer>())
            {
                Debug.Log($"Moving {GetHierarcyPath(renderer.transform)} to original hierarcy.");
                var path= GetHierarcyPath(renderer.transform);
                var originalGameObject = original.transform.Find(path);

                if (originalGameObject != null)
                {
                    var lodSkinnedRenderer = renderer.GetComponent<SkinnedMeshRenderer>();
                    var orgSkinnedrenderer = originalGameObject.GetComponent<SkinnedMeshRenderer>();
                    if (lodSkinnedRenderer && orgSkinnedrenderer)
                    {
                        ReassignSkeleton(lodSkinnedRenderer, orgSkinnedrenderer);
                    }
                    renderer.transform.parent = originalGameObject.parent;
                    renderer.name += $"_LOD{lodLevel}";
                }
                else
                {
                    Debug.LogWarning($"Did not find matching Renderer at {path}.");
                }
            }
        }

        /// <summary>
        /// Reassigns all bones from one skeleton to another.
        /// For this to work properly the SkinnedMeshRenderers needs to be places in same location.
        /// See https://simplygon.com/posts/45ff2ece-1d74-484c-840c-47b12fea76fa
        /// </summary>
        private static void ReassignSkeleton(SkinnedMeshRenderer lodSkinnedRenderer, SkinnedMeshRenderer originalSkinnedRenderer)
        {
            // Gets all bones from original skinned mesh renderer
            Dictionary<string, Transform> originalBoneMap = new Dictionary<string, Transform>();
            foreach (Transform bone in originalSkinnedRenderer.bones)
                originalBoneMap[bone.gameObject.name] = bone;


            // Create a new bone list for target skinned renderer and maps it via names to original bones
            Transform[] newBones = new Transform[lodSkinnedRenderer.bones.Length];
            for (int i = 0; i < lodSkinnedRenderer.bones.Length; ++i)
            {
                GameObject bone = lodSkinnedRenderer.bones[i].gameObject;
                if (!originalBoneMap.TryGetValue(bone.name, out newBones[i]))
                {
                    Debug.LogWarning("Unable to find bone \"" + bone.name + "\" on original skeleton.");
                }
            }
            lodSkinnedRenderer.bones = newBones;

            // Assigns target skinned mesh renderer to same parent and root bone as original
            lodSkinnedRenderer.rootBone = originalSkinnedRenderer.rootBone;
        }
    }
}

LODQuality.cs

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

using UnityEngine;

namespace Simplygon.Examples.LODCharacterPrefab
{
    [CreateAssetMenu(fileName = "New LOD Quality", menuName = "Simplygon/LOD Quality")]
    public class LODQuality : ScriptableObject
    {
        // LOD quality settings
        public float[] RelativeScreenHeights;
        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;

        // Merge texture settings
        public bool MergeTextures;
        public URPMaterialChannel[] MaterialChannels;
        public int MergeTexturesLODIndex;
        public uint TextureSize;

        // 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 RelativeScreenHeights.Length;
        }

        /// <summary>
        /// Should we merge textures when processing this LOD level.
        /// </summary>
        public bool ShouldMergeTextures(int index)
        {
            return MergeTextures && index == MergeTexturesLODIndex;
        }

        /// <summary>
        /// Get relative screen heights for LOD index i.
        /// </summary>
        public float GetRelativeScreenHeightForIndex(int i)
        {
            if (i >= RelativeScreenHeights.Length)
                return FadeAwayDistance;
            return RelativeScreenHeights[i];

        }

        /// <summary>
        /// Get screen size in pixels for LOD index i.
        /// </summary>
        public uint GetScreenSizeForIndex(int i, bool ignoreQuality)
        {
            return GetScreenSizeForRelativeScreenHeight(RelativeScreenHeights[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.LODCharacterPrefab
{
    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;
            }
        }
    }
}

URPMaterialChannel.cs

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

namespace Simplygon.Examples.LODCharacterPrefab
{
    /// <summary>
    /// Channel names avaliable in URP render pipeline.
    /// https://documentation.simplygon.com/SimplygonSDK_10.4.148.0/unity/concepts/materialmapping.html#urp-to-simplygon-channel-mapping
    /// </summary>
    [System.Serializable]
    public enum URPMaterialChannel
    {
        BaseMap,
        NormalMap,
        EmissionMap,
        MetallicMap,
        OcclusionMap
    }
}
⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*