Mesh Baker LOD [RELEASED]

THIS ASSET REQUIRES MESH BAKER

Make your open world fly! It’s like static batching and LOD combined!

In test scenes drawcalls are reduced by 95%. rendered vertices reduced by 60%.

A replacement for Unity’s LOD. Mesh Baker LODs are baked into combined meshes at runtime, dramatically reducing drawcalls. Provides an easy, flexible way to optimize huge scenes or scenes with procedural content at runtime.

  • Works with Unity free
  • No scripting required
  • All baking happens at runtime
  • Easily bake many skinned meshes into mobs that take one drawcall to render
  • Fine grained control over how many meshes are allowed in each level of detail.

There are some major shortcomings to the Static Batching and LOD included with Unity Pro:

  • Static Batching bakes combined meshes at build time

  • The memory footprint for the combined meshes can be prohibitively large

  • If static content is created at runtime it can’t be included in the combined mesh.

  • Builds can become too large for mobile platforms

  • Dynamic Batching bakes every frame

Mesh Baker LOD overcomes these problems by only baking what is necessary at the moment.

Mesh Baker LOD Videos
Demo

Tutorial Part 1

Tutorial Part 2

Tutorial Part 3

Tutorial Part 4

Mesh Baker LOD is now available in the asset store

Looks promising.

I am designing a sandbox game with procedurally generated terrain. We are creating an infinite landscape by creating terrain chunks in front of the player and deleting them them behind the player. Can these LODs be prefabed then Instantiated and Destroyed at runtime?

Yes. The LODs and manager setup can be prefabed and re-used in other scenes.

The recommended workflow is to build and prefab the LODs in a simple scene then just drag them into other scenes to use them.

Version 1.1 Released

  • Fixed bug which would cause SkinnedMeshLODs with max per level set to bake every frame
  • Can add and remove bakers at runtime
  • Can use orthographic cameras

Do you plan to release an evaluation version of Mesh Baker LOD as you have done for the Mesh baker?

I don’t plan to. My experience with the Mesh Baker evaluation version has not been great. It is very time consuming to manage and I hate the limitations of DLLs. If you contact me personally and can convince me that you are reasonably honest I would consider sending you a version for evaluation purposes.

Is there a way to get rid of the popping between LOD levels? I’ve tried lots of different settings, but it always happens. :neutral:

Here’s a quick tool that I threw together to convert LODGroups to MB2_LOD. It doesn’t sort by material, so some cleanup is required, but it’s nice to use Unity’s built-in LODGroup inspector tools for visually determining the Screen Percentages. The code is sloppy, but it worked for converting more than 2k LODGroups.

using UnityEngine;
using UnityEditor;
using System.Collections;
using DigitalOpus.MB.Core;

public class LODGroupToMeshBakerLOD : ScriptableObject
{
	// TODO: sort by materials
    [MenuItem ("Tools/Mesh Baker/Convert LODGroup to MeshBaker LOD")]
    static void MenuConvLODGroupToMBLOD()
    {
        Transform[] transforms = Selection.GetTransforms(SelectionMode.TopLevel | SelectionMode.OnlyUserModifiable);

		foreach(Transform curTransform in transforms) {
			// TODO: the undo doesn't always work as expected
			Undo.RegisterCompleteObjectUndo(curTransform.gameObject, "Convert LODGroup To MeshBakerLOD");
			LODGroup curLODGroup = curTransform.GetComponent<LODGroup>();
			if(curLODGroup != null) {

//				float LODGroupBound = getBoundsOfLODGroup(curTransform.gameObject);
				MB2_LOD meshBakerLOD = curTransform.gameObject.AddComponent(typeof(MB2_LOD)) as MB2_LOD;
				meshBakerLOD.LOG_LEVEL = MB2_LogLevel.error;
				meshBakerLOD.levels = new MB2_LOD.LOD[curLODGroup.lodCount];
				for(int i = 0; i < curLODGroup.lodCount; i++)
					meshBakerLOD.levels[i] = new MB2_LOD.LOD();
				if(curTransform.gameObject.isStatic) {
					meshBakerLOD.renderType = MB_RenderType.meshRenderer;
					curTransform.gameObject.isStatic = false;
				} else
					meshBakerLOD.renderType = MB_RenderType.skinnedMeshRenderer;


				SerializedObject obj = new SerializedObject(curLODGroup);

				for(int curLOD = 0; curLOD < curLODGroup.lodCount; curLOD++) {
					float screenRelativeHeight = obj.FindProperty("m_LODs.Array.data[" + curLOD.ToString() + "].screenRelativeHeight").floatValue;
					SerializedProperty prop = obj.FindProperty("m_LODs.Array.data[" + curLOD.ToString() + "].renderers");
					ArrayList gameObjectsArrayList = new ArrayList();
					for(int curGO = 0; curGO < prop.arraySize; curGO++) {
						// TODO: catch NPE if LODGroup LOD has no meshes
						gameObjectsArrayList.Add((prop.GetArrayElementAtIndex(curGO).FindPropertyRelative("renderer").objectReferenceValue as MeshRenderer).gameObject);
					}
					GameObject[] gameObjectsArray = gameObjectsArrayList.ToArray(typeof(GameObject)) as GameObject[];

					for(int curRenderer = 0; curRenderer < gameObjectsArray.Length; curRenderer++) {
						GameObject curGO = gameObjectsArray[curRenderer];
						curGO.isStatic = false;

						if(curRenderer==0) {
							meshBakerLOD.levels[curLOD].lodObject = curGO.GetComponent< Renderer >();
							meshBakerLOD.levels[curLOD].screenPercentage = Mathf.Max(screenRelativeHeight,0.0001f);
							curGO.transform.parent = curTransform;
							curGO.name = "LOD" + curLOD + "_" + curGO.name;
						} else {
							GameObject tempParent = new GameObject();
							tempParent.transform.parent = curGO.transform;
							tempParent.transform.localRotation = Quaternion.identity;
							tempParent.transform.localPosition = Vector3.zero;
							tempParent.transform.localScale = Vector3.one;
							tempParent.transform.parent = gameObjectsArray[0].transform;
							curGO.transform.parent = tempParent.transform;
							tempParent.name = "LOD" + curLOD + "_" + curGO.name;

							MB2_LOD innerMeshBakerLOD = tempParent.AddComponent(typeof(MB2_LOD)) as MB2_LOD;
							innerMeshBakerLOD.LOG_LEVEL = MB2_LogLevel.error;
							innerMeshBakerLOD.levels = new MB2_LOD.LOD[1];
							innerMeshBakerLOD.levels[0] = new MB2_LOD.LOD();
							innerMeshBakerLOD.renderType = meshBakerLOD.renderType;
							innerMeshBakerLOD.levels[0].lodObject = curGO.GetComponent< Renderer >();
							// if it's a child of an LOD, it's going to be hidden anyway.
							innerMeshBakerLOD.levels[0].screenPercentage = .001f;//meshBakerLOD.levels[curLOD].screenPercentage;
						}
					}
				}
				DestroyImmediate(curLODGroup,false);
			} else {
				Debug.Log(curTransform.name + " has no LODGroup.");
			}
		}
    }

	static float getBoundsOfLODGroup(GameObject GO) {
		float toReturn = 0f;
		GameObject dummy = (GameObject)Instantiate( GO, Vector3.zero, Quaternion.identity );
		Renderer[] renderers = dummy.GetComponentsInChildren< Renderer >();
		if( renderers.Length >= 1 )
		{
			Bounds bounds = new Bounds( Vector3.zero, Vector3.zero );
			foreach( Renderer r in renderers )
			{
				bounds.Encapsulate( r.bounds );
			}
			
			Vector3 size = bounds.size;
			toReturn = (size.x + size.y + size.z) / 3f;
		}
		DestroyImmediate( dummy ,false);
		return toReturn;
	}

	static float getBoundsOfSingleObject(GameObject GO) {
		float toReturn = 0f;
		GameObject dummy = (GameObject)Instantiate( GO, Vector3.zero, Quaternion.identity );
		Renderer renderer = dummy.GetComponent< Renderer >();
		if( renderer != null )
		{
			Vector3 size = renderer.bounds.size;
			toReturn = (size.x + size.y + size.z) / 3f;
		}
		DestroyImmediate( dummy ,false);
		return toReturn;
	}
}

Cheers,
IFL

Hi IFL,

Thanks for posting the LOD code! This is a great idea, will try to include something like this in the next version.

By poping I presume that you mean the noticeable transition when a model switches to another level of detail. About the only thing you can do is have lots of levels of detail. Also try to make sure that the silhouette is preserved when the switch happens.

There is something called a progressive mesh. Mesh Baker LOD does NOT implement this currently but I would like to implement it at some point. The idea is that the edges in a mesh are sorted by importance in a pre-processing step. Then at runtime the mesh can be put into any level of detail! Not easy to implement though. Sorting the edges is very tricky. Especially when factoring UVs and Submeshes on top of considering the geometry.

Thanks for the fast reply!

[quote=“Phong, post:8, topic: 518925, username:Phong”]
By poping I presume that you mean the noticeable transition when a model switches to another level of detail. About the only thing you can do is have lots of levels of detail. Also try to make sure that the silhouette is preserved when the switch happens.
[/quote] I figured that out just a while ago. I’m rewriting the script above to account for it.

[quote=“Phong, post:8, topic: 518925, username:Phong”]
There is something called a progressive mesh. Mesh Baker LOD does NOT implement this currently but I would like to implement it at some point. The idea is that the edges in a mesh are sorted by importance in a pre-processing step. Then at runtime the mesh can be put into any level of detail! Not easy to implement though. Sorting the edges is very tricky. Especially when factoring UVs and Submeshes on top of considering the geometry.
[/quote] Looked up that technique - it’s very intriguing. It’d do away with a lot of the work that artists would normally have to do. I might be missing something, but having multiple viewpoints seems to be the only thing that would make it an ordeal to implement. Collapsing edges is pretty straightforward, and the sorting could be as simple as going from smallest to largest. Thanks for sharing that - it gives me something to play around with in my downtime.

It is fun to play around with. The ordeal comes when trying to handle the UVs and normals. The UVs are by far the hardest to deal with. You need to find all the UV islands and attempt to measure how much of a distortion will be introduced by eliminating an edge in UV space taking into account concavity in the UV island boundaries. To be robust you even need to look at the pixel values in the the UV texture to try to ensure sharp contrasts are preserved as much as possible. Therein lies the path to madness.

There is a new version in the asset store with two bug fixes:

  1. Fixes errors when switching scenes with the LOD Manager singleton.

  2. Level of detail hierarchies are now deactivated from the child parented to the LOD object instead of the game object with the renderer attached.

Thanks for the updates!

Is there an easy way to set the combined-mesh(es) of specific MeshBakers to specific layers? Or maybe just retrieve the linked CombinedMesh (parent) GO tied to each baker? I know how to do it in a complicated way, but I haven’t found an easy route that’s also efficient.

I will add this to the baker class and submit an update. In the meantime here is how to access the combined mesh objects, it is a bit of a hack:

In file MB2_LODClusterBase you will need to make line 70 public instead of protected:

		public MB2_MultiMeshCombiner combinedMesh;

In the script where you want to access the result scene object you will need to add:

                using DigitalOpus.MB.Core;
                using DigitalOpus.MB.Lod;

If the baker type is grid use this:

LODClusterManagerGrid gridClusterManager = (LODClusterManagerGrid) MB2_LODManager.Manager().bakers[0].baker;
for (int i = 0; i < gridClusterManager.clusters.Count; i++){
        //this is actually the parent. you will need to set the layer on its children
	gridClusterManager.clusters[i].combinedMesh.resultSceneObject.layer = LayerMask.NameToLayer("testLayer");;
}

If the baker type is simple use this (you will need to make cluster public in file MB2_ClusterManagerSimple (line 33):

LODClusterManagerSimple simpleClusterManager = (LODClusterManagerSimple) MB2_LODManager.Manager().bakers[0].baker;
//this is actually the parent. you will need to set the layer on its children
simpleClusterManager.cluster.combinedMesh.resultSceneObject.layer = LayerMask.NameToLayer("testLayer");

Note that “resultSceneObject” is the parent to the mesh filter game objects so you will need to set the layer on the children.

Hey, I didn’t see your edited reply until just now.

Your layer solution works perfectly, and the update is great.

If you don’t mind sharing, has this asset done well on the Asset Store? There aren’t many ratings, but I can’t imagine publishing a project on mobile without it - it’s one of the best assets I’ve purchased. Regardless, I appreciate the hard work you’ve put into it.

[Edit]
A nice addition would be the ability to enable/disable the casting and receiving of shadows for each group. I’ve done it through code, but others might like it in LOD Manager’s inspector. I’d say make it possible to enable light probes as well, but those work off of one point in space, so it wouldn’t make much sense for a group of baked meshes.

Thanks for the feedback. It hasn’t done great in the Asset Store so far but these things can take a while to catch on. So far January looks like an improvement. I am trying to get it in a sale.

The shadow casting options and light probes are a good idea. I will try to get these in the next version.

A new update for Mesh Baker LOD is now available. New features include:

  • Can do LOD without baking meshes
  • Can set the layer for the combined meshes

Before I try to implement it, is there anything that would make having multiple meshes per LOD level difficult? It’d save a lot in terms of run-time performance, but not be worth it if it would take too much time to implement. It seems like the bounds checks and editor code would be the most difficult part, unless I’m missing something. This isn’t a feature request, just asking your opinion.

Off the top of my head I can’t think of any serious reason why this would be particularly difficult. I think your effort assessment is fairly good. If I think of anything in the next little while I will modify this post.

I’m looking at Mesh Baker LOD as a possible way to handle a large forest. I’m thinking of a three-level LOD system, with 3D mesh trees closest to the camera (LOD1), a tree-shaped mesh billboard for LOD2, and a single mesh with 20-30 trees for LOD 3 (seen only at a great distance). Basically, it shifts LOD detection from individual trees to a tree cluster for LOD3. This is sorta the opposite of nesting as described in your manual. Is it possible with your tool?

I went ahead and purchased this. Testing the “CityScene” in the editor and on an ipad 2, the MB2_LODManager.LateUpdate has a pretty major performance hit every second. Even in the Editor, on my 2013 MBP, the frame rate drops to 30 each time, and on iPad, it drops to 15fps every second. See the profiler screenshot for latter. What can I do to improve that?

EDIT: Image doesn’t seem to be showing up. On iPad2, MB2_LODManager.LateUpdate spikes to 86ms every second, which pushes fps down to 15. During that spike, GC is 8ms, GameObject.Activate/Deactivate are .09 and .04.