Skip to content

Instantly share code, notes, and snippets.

@JLChnToZ
Last active April 1, 2023 16:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JLChnToZ/6039a50d16f8fa3bb800258e29b2ee19 to your computer and use it in GitHub Desktop.
Save JLChnToZ/6039a50d16f8fa3bb800258e29b2ee19 to your computer and use it in GitHub Desktop.
A clean mesh combiner for Unity
/**
* The MIT License (MIT)
*
* Copyright (c) 2023 Jeremy Lam aka. Vistanz
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEditor;
using UnityEditorInternal;
namespace JLChnToZ.EditorExtensions {
public class MeshCombinerWindow : EditorWindow {
const string COMBINE_INFO = "Click combine to...\n" +
"- Combine meshes while retains blendshapes and bones\n" +
"- Merge sub meshes with same material into one\n" +
"- Create extra bones for each non skinned mesh renderers\n" +
"- Derefereneces unused bones (but not delete them)\n" +
"- Bake (freeze state and then removes) selected blendshapes\n" +
"- Save the combined mesh to a file\n" +
"- Deactivates combined mesh renderer sources";
const string REMOVE_UNUSED_INFO = "Here are the dereferenced objects from the last combine operation. " +
"You can manually select them and delete them here. " +
"Beware that these objects may be referenced by other components, please check before deleting them.\n" +
"You can use Unity's undo function if you accidentally deleted something.";
static readonly string[] tabNames = new[] { "Combine Meshes", "Combine Bones", "Cleanup" };
int currentTab;
Vector2 sourceListScrollPos, boneMergeScrollPos, unusedObjectScrollPos;
List<Renderer> sources = new List<Renderer>();
SkinnedMeshRenderer destination;
BlendShapeCopyMode blendShapeCopyMode = BlendShapeCopyMode.Vertices;
bool mergeSubMeshes = true;
Dictionary<Renderer, (bool[], bool, string[])> bakeBlendShapeMap = new Dictionary<Renderer, (bool[], bool, string[])>();
Dictionary<Transform, Transform> boneReamp = new Dictionary<Transform, Transform>();
HashSet<Transform> rootTransforms = new HashSet<Transform>();
Dictionary<Transform, HashSet<Renderer>> boneToRenderersMap = new Dictionary<Transform, HashSet<Renderer>>();
Dictionary<Transform, bool> boneFolded = new Dictionary<Transform, bool>();
HashSet<Transform> bonesToMergeUpwards = new HashSet<Transform>();
HashSet<Transform> unusedObjects = new HashSet<Transform>();
ReorderableList sourceList;
[MenuItem("JLChnToZ/Tools/Skinned Mesh Combiner")]
public static void ShowWindow() => GetWindow<MeshCombinerWindow>("Skinned Mesh Combiner").Show(true);
protected virtual void OnEnable() {
sourceList = new ReorderableList(sources, typeof(Renderer), true, true, true, true) {
drawElementCallback = OnListDrawElement,
elementHeightCallback = OnListGetElementHeight,
drawHeaderCallback = OnListDrawHeader,
onAddCallback = OnListAdd,
onRemoveCallback = OnListRemove,
drawNoneElementCallback = OnListDrawNoneElement,
};
}
protected virtual void OnGUI() {
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginChangeCheck();
currentTab = GUILayout.Toolbar(currentTab, tabNames, GUILayout.ExpandWidth(true));
bool tabChanged = EditorGUI.EndChangeCheck();
EditorGUILayout.EndHorizontal();
switch (currentTab) {
case 0:
if (tabChanged) RefreshCombineMeshOptions();
DrawCombineMeshTab();
break;
case 1:
if (tabChanged) RefreshBones();
DrawCombineBoneTab();
break;
case 2: DrawUnusedObjectsTab(); break;
}
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginDisabledGroup(sources.Count == 0 || destination == null);
if (GUILayout.Button("Combine")) Combine();
EditorGUI.EndDisabledGroup();
if (GUILayout.Button("Clear")) {
sources.Clear();
bakeBlendShapeMap.Clear();
boneReamp.Clear();
unusedObjects.Clear();
bonesToMergeUpwards.Clear();
boneToRenderersMap.Clear();
boneFolded.Clear();
destination = null;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.HelpBox(COMBINE_INFO, MessageType.Info);
}
void DrawCombineMeshTab() {
sourceListScrollPos = EditorGUILayout.BeginScrollView(sourceListScrollPos);
sourceList.DoLayoutList();
EditorGUILayout.EndScrollView();
EditorGUILayout.BeginHorizontal();
destination = EditorGUILayout.ObjectField("Destination", destination, typeof(SkinnedMeshRenderer), true) as SkinnedMeshRenderer;
if (destination == null && GUILayout.Button("Auto Create", GUILayout.ExpandWidth(false))) {
Transform commonParent = null;
foreach (var source in sources) {
if (source == null) continue;
if (commonParent == null)
commonParent = source.transform;
else {
var parent = source.transform;
while (parent != null) {
if (parent == commonParent || parent.IsChildOf(commonParent))
break;
if (commonParent.IsChildOf(parent)) {
commonParent = parent;
break;
}
parent = parent.parent;
}
}
}
if (commonParent != null) {
if (!commonParent.TryGetComponent(out destination)) {
destination = commonParent.gameObject.AddComponent<SkinnedMeshRenderer>();
Undo.RegisterCreatedObjectUndo(destination, "Auto Create Skinned Mesh Renderer");
}
EditorGUIUtility.PingObject(destination);
} else
Debug.LogWarning("No common parent found for selected renderers.");
}
EditorGUILayout.EndHorizontal();
blendShapeCopyMode = (BlendShapeCopyMode)EditorGUILayout.EnumFlagsField("Blend Shape Copy Mode", blendShapeCopyMode);
mergeSubMeshes = EditorGUILayout.ToggleLeft("Merge Sub Meshes With Same Material", mergeSubMeshes);
}
void DrawCombineBoneTab() {
EditorGUILayout.HelpBox("Select bones to merge upwards (to its parent in hierachy).", MessageType.Info);
boneMergeScrollPos = EditorGUILayout.BeginScrollView(boneMergeScrollPos);
var drawStack = new Stack<(Transform, int)>();
foreach (var transform in rootTransforms)
drawStack.Push((transform, 0));
while (drawStack.Count > 0) {
var (transform, depth) = drawStack.Pop();
EditorGUILayout.BeginHorizontal();
GUILayout.Space(depth * 12);
bool folded = false;
if (transform == null) {
EditorGUILayout.LabelField("Has other children");
} else {
boneFolded.TryGetValue(transform, out folded);
var isMerge = bonesToMergeUpwards.Contains(transform);
EditorGUI.BeginChangeCheck();
folded = GUILayout.Toggle(folded, GUIContent.none, EditorStyles.foldout, GUILayout.ExpandWidth(false));
if (EditorGUI.EndChangeCheck()) boneFolded[transform] = folded;
EditorGUI.BeginChangeCheck();
isMerge = GUILayout.Toggle(isMerge, EditorGUIUtility.ObjectContent(transform, typeof(Transform)), GUILayout.Height(EditorGUIUtility.singleLineHeight), GUILayout.ExpandWidth(false));
if (EditorGUI.EndChangeCheck()) {
if (isMerge)
bonesToMergeUpwards.Add(transform);
else
bonesToMergeUpwards.Remove(transform);
RefreshBones();
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Locate", EditorStyles.miniButtonLeft, GUILayout.ExpandWidth(false)))
EditorGUIUtility.PingObject(transform);
if (GUILayout.Button("Select", EditorStyles.miniButtonRight, GUILayout.ExpandWidth(false)))
Selection.activeTransform = transform;
}
EditorGUILayout.EndHorizontal();
if (transform != null && folded) {
int childCount = transform.childCount;
bool hasChild = false;
for (int i = childCount - 1; i >= 0; i--) {
var child = transform.GetChild(i);
if (boneToRenderersMap.ContainsKey(child)) {
drawStack.Push((child, depth + 1));
hasChild = true;
}
}
if (childCount > 0 && !hasChild)
drawStack.Push((null, depth + 1));
}
}
EditorGUILayout.EndScrollView();
}
void DrawUnusedObjectsTab() {
EditorGUILayout.HelpBox(REMOVE_UNUSED_INFO, MessageType.Info);
unusedObjectScrollPos = EditorGUILayout.BeginScrollView(unusedObjectScrollPos);
foreach (var unusedObject in unusedObjects) {
if (unusedObject == null) continue;
EditorGUILayout.BeginHorizontal();
var gameObject = unusedObject.gameObject;
EditorGUILayout.LabelField(EditorGUIUtility.ObjectContent(gameObject, typeof(GameObject)), GUILayout.ExpandWidth(true));
if (GUILayout.Button("Locate", EditorStyles.miniButtonLeft, GUILayout.ExpandWidth(false)))
EditorGUIUtility.PingObject(unusedObject);
if (GUILayout.Button("Select", EditorStyles.miniButtonMid, GUILayout.ExpandWidth(false)))
Selection.activeObject = unusedObject;
if (GUILayout.Button("+", EditorStyles.miniButtonMid, GUILayout.ExpandWidth(false)))
Selection.objects = new HashSet<UnityEngine.Object>(Selection.objects) { unusedObject }.ToArray();
if (GUILayout.Button("Delete", EditorStyles.miniButtonRight, GUILayout.ExpandWidth(false)))
Undo.DestroyObjectImmediate(unusedObject.gameObject);
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Select All", GUILayout.ExpandWidth(false)))
Selection.objects = unusedObjects.Where(x => x != null).ToArray();
if (GUILayout.Button("Delete All", GUILayout.ExpandWidth(false))) {
foreach (var unusedObject in unusedObjects) {
if (unusedObject == null) continue;
Undo.DestroyObjectImmediate(unusedObject.gameObject);
}
}
EditorGUILayout.EndHorizontal();
}
static void OnListDrawHeader(Rect rect) => EditorGUI.LabelField(rect, "(Skinned) Mesh Renderers to Combine");
void OnListDrawElement(Rect rect, int index, bool isActive, bool isFocused) {
var renderer = sources[index];
rect.height = EditorGUIUtility.singleLineHeight;
var rect2 = rect;
rect2.xMin += 12;
EditorGUI.LabelField(rect2, renderer == null ? new GUIContent("(Missing)") : EditorGUIUtility.ObjectContent(renderer, renderer.GetType()));
rect2.x = rect.x;
rect2.width = 12;
if (!bakeBlendShapeMap.TryGetValue(renderer, out var bakeBlendShapeToggles)) return;
var (toggles, toggleState, blendShapeNameMap) = bakeBlendShapeToggles;
if (toggles == null || blendShapeNameMap == null) return;
EditorGUI.BeginChangeCheck();
if (blendShapeNameMap.Length > 0) toggleState = EditorGUI.Foldout(rect2, toggleState, GUIContent.none);
if (EditorGUI.EndChangeCheck()) bakeBlendShapeMap[renderer] = (toggles, toggleState, blendShapeNameMap);
if (!toggleState) return;
rect2.width = rect.width;
rect2.y += EditorGUIUtility.singleLineHeight;
if (renderer is SkinnedMeshRenderer)
for (var i = 0; i < toggles.Length; i++) {
toggles[i] = EditorGUI.ToggleLeft(rect2, $"Bake blendshape {blendShapeNameMap[i]}", toggles[i]);
rect2.y += EditorGUIUtility.singleLineHeight;
}
else if (renderer is MeshRenderer)
toggles[0] = EditorGUI.ToggleLeft(rect2, "Don't create bone for this mesh renderer.", toggles[0]);
}
float OnListGetElementHeight(int index) {
if (bakeBlendShapeMap.TryGetValue(sources[index], out var bakeBlendShapeToggles)) {
var (toggles, toggleState, blendShapeNameMap) = bakeBlendShapeToggles;
if (toggles != null && blendShapeNameMap != null && toggleState)
return EditorGUIUtility.singleLineHeight * (blendShapeNameMap.Length + 1);
}
return EditorGUIUtility.singleLineHeight;
}
void OnListAdd(ReorderableList list) {
var count = sources.Count;
sources.AddRange(Selection.GetFiltered<Renderer>(SelectionMode.Deep).Where(r => r != null && !sources.Contains(r)));
if (count == sources.Count) return;
sourceList.index = sources.Count - 1;
RefreshCombineMeshOptions();
}
void OnListRemove(ReorderableList list) {
sources.RemoveAt(list.index);
RefreshCombineMeshOptions();
}
private void OnListDrawNoneElement(Rect rect) =>
EditorGUI.LabelField(rect, "Click \"+\" button to add selected (skinned) mesh renderer to combine.");
void RefreshCombineMeshOptions() {
var oldSources = new HashSet<Renderer>(bakeBlendShapeMap.Keys);
foreach (var source in sources) {
Mesh mesh = null;
var skinnedMeshRenderer = source as SkinnedMeshRenderer;
if (skinnedMeshRenderer != null)
mesh = skinnedMeshRenderer.sharedMesh;
else {
var meshRenderer = source as MeshRenderer;
if (meshRenderer != null && source.TryGetComponent(out MeshFilter meshFilter))
mesh = meshFilter.sharedMesh;
}
if (mesh == null) continue;
int length = skinnedMeshRenderer != null ? mesh.blendShapeCount : 1;
if (bakeBlendShapeMap.TryGetValue(source, out var bakeBlendShapeToggles)) {
if (bakeBlendShapeToggles.Item1.Length != length)
bakeBlendShapeToggles.Item1 = new bool[length];
bakeBlendShapeToggles.Item3 = skinnedMeshRenderer != null ? GetBlendShapeNames(mesh) : new string[1];
} else {
bakeBlendShapeToggles = (
new bool[length], false,
skinnedMeshRenderer != null ? GetBlendShapeNames(mesh) : new string[1]
);
}
bakeBlendShapeMap[source] = bakeBlendShapeToggles;
oldSources.Remove(source);
}
foreach (var source in oldSources) bakeBlendShapeMap.Remove(source);
}
void RefreshBones() {
rootTransforms.Clear();
boneToRenderersMap.Clear();
foreach (var source in sources) {
if (source is SkinnedMeshRenderer skinnedMeshRenderer) {
var bones = skinnedMeshRenderer.bones;
if (bones == null || bones.Length == 0) continue;
foreach (var bone in bones) {
if (bone == null) continue;
var parent = bone;
while (true) {
LazyInitialize(boneToRenderersMap, parent, out var renderers);
if (parent == bone) renderers.Add(source);
if (parent.parent == null) {
rootTransforms.Add(parent);
break;
}
parent = parent.parent;
}
}
} else if (source is MeshRenderer meshRenderer) {
var parent = meshRenderer.transform;
while (true) {
LazyInitialize(boneToRenderersMap, parent, out var renderers);
if (parent == meshRenderer.transform) renderers.Add(source);
if (parent.parent == null) {
rootTransforms.Add(parent);
break;
}
parent = parent.parent;
}
}
}
}
static string[] GetBlendShapeNames(Mesh mesh) => Enumerable.Range(0, mesh.blendShapeCount).Select(mesh.GetBlendShapeName).ToArray();
void Combine() {
RefreshCombineMeshOptions();
boneReamp.Clear();
foreach (var bone in bonesToMergeUpwards) {
var targetBone = bone;
while (targetBone != null && bonesToMergeUpwards.Contains(targetBone)) {
targetBone = targetBone.parent;
boneReamp[bone] = targetBone;
}
}
unusedObjects.Clear();
foreach (var source in sources) {
unusedObjects.Add(source.transform);
if (source is SkinnedMeshRenderer skinnedMeshRenderer)
unusedObjects.UnionWith(skinnedMeshRenderer.bones.Where(b => b != null));
}
var mesh = Combine(sources.Select(source => {
if (bakeBlendShapeMap.TryGetValue(source, out var bakeBlendShapeToggles))
return (source, bakeBlendShapeToggles.Item1);
return (source, null);
}).ToArray(), destination, mergeSubMeshes, blendShapeCopyMode, boneReamp);
if (mesh != null) {
mesh.Optimize();
unusedObjects.ExceptWith(destination.bones);
unusedObjects.Remove(destination.transform);
var path = EditorUtility.SaveFilePanelInProject("Save Mesh", mesh.name, "asset", "Save Combined Mesh", sources.Select(source => {
var skinnedMeshRenderer = source as SkinnedMeshRenderer;
if (skinnedMeshRenderer != null) return skinnedMeshRenderer.sharedMesh;
var meshRenderer = source as MeshRenderer;
if (meshRenderer != null && source.TryGetComponent(out MeshFilter meshFilter)) return meshFilter.sharedMesh;
return null;
})
.Where(m => m != null)
.Select(AssetDatabase.GetAssetPath)
.Where(p => !string.IsNullOrEmpty(p))
.Select(System.IO.Path.GetDirectoryName)
.FirstOrDefault());
if (!string.IsNullOrEmpty(path)) AssetDatabase.CreateAsset(mesh, path);
} else
Debug.LogError("Failed to combine meshes.");
}
public static Mesh Combine(
ICollection<(Renderer, bool[])> sources,
SkinnedMeshRenderer destination,
bool mergeSubMeshes = true,
BlendShapeCopyMode blendShapeCopyMode = BlendShapeCopyMode.Vertices,
IDictionary<Transform, Transform> boneRemap = null
) {
if (sources.Count == 0) return null;
var combineInstances = new Dictionary<Material, List<(CombineInstance, bool[])>>(sources.Count);
var boneWeights = new Dictionary<(Mesh, int), IEnumerable<BoneWeight>>(sources.Count);
var bindposeMap = new Dictionary<(Transform, Matrix4x4), int>();
var bindposes = new List<Matrix4x4>();
var allBindposes = new List<Matrix4x4>();
var allBones = new List<Transform>();
var boneHasWeights = new HashSet<int>();
var boneMapping = new Dictionary<int, int>();
var materials = new List<Material>(sources.Count);
var bakeList = new HashSet<(Renderer, int)>();
Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache = null, vntArrayCache2 = null;
Dictionary<string, BlendShapeTimeLine> blendShapesStore = null;
var blendShapesWeights = new Dictionary<string, float>();
var vertices = blendShapeCopyMode.HasFlag(BlendShapeCopyMode.Vertices) ? new List<Vector3>() : null;
var normals = blendShapeCopyMode.HasFlag(BlendShapeCopyMode.Normals) ? new List<Vector3>() : null;
var tangents = blendShapeCopyMode.HasFlag(BlendShapeCopyMode.Tangents) ? new List<Vector4>() : null;
foreach (var (source, bakeFlags) in sources) {
if (source == null) continue;
var sourceTransform = source.transform;
if (source is SkinnedMeshRenderer skinnedMeshRenderer) {
var orgMesh = skinnedMeshRenderer.sharedMesh;
var mesh = Instantiate(orgMesh);
var sharedMaterials = skinnedMeshRenderer.sharedMaterials;
var bones = skinnedMeshRenderer.bones;
mesh.GetBindposes(bindposes);
var weights = mesh.boneWeights;
PreAllocate(allBones, bones.Length);
PreAllocate(allBindposes, bindposes.Count);
boneMapping.Clear();
boneHasWeights.Clear();
foreach (var weight in weights) {
if (weight.weight0 > 0) boneHasWeights.Add(weight.boneIndex0);
if (weight.weight1 > 0) boneHasWeights.Add(weight.boneIndex1);
if (weight.weight2 > 0) boneHasWeights.Add(weight.boneIndex2);
if (weight.weight3 > 0) boneHasWeights.Add(weight.boneIndex3);
}
for (int i = 0; i < bindposes.Count; i++) {
if (bones[i] == null || !boneHasWeights.Contains(i)) continue;
var key = (bones[i], bindposes[i]);
if (!bindposeMap.TryGetValue(key, out var index)) {
bindposeMap[key] = index = bindposeMap.Count;
var targetBone = bones[i];
var poseMatrix = bindposes[i];
if (boneRemap != null && boneRemap.TryGetValue(targetBone, out var bone)) {
poseMatrix = bone.worldToLocalMatrix * targetBone.localToWorldMatrix * poseMatrix;
targetBone = bone;
}
allBones.Add(targetBone);
allBindposes.Add(poseMatrix);
}
boneMapping[i] = index;
}
for (int i = 0, newIndex; i < weights.Length; i++) {
var weight = weights[i];
if (boneMapping.TryGetValue(weight.boneIndex0, out newIndex)) weight.boneIndex0 = newIndex;
else { weight.boneIndex0 = 0; weight.weight0 = 0; }
if (boneMapping.TryGetValue(weight.boneIndex1, out newIndex)) weight.boneIndex1 = newIndex;
else { weight.boneIndex1 = 0; weight.weight1 = 0; }
if (boneMapping.TryGetValue(weight.boneIndex2, out newIndex)) weight.boneIndex2 = newIndex;
else { weight.boneIndex2 = 0; weight.weight2 = 0; }
if (boneMapping.TryGetValue(weight.boneIndex3, out newIndex)) weight.boneIndex3 = newIndex;
else { weight.boneIndex3 = 0; weight.weight3 = 0; }
weights[i] = weight;
}
var subMeshCount = mesh.subMeshCount;
for (int i = 0; i < subMeshCount; i++) {
if (LazyInitialize(combineInstances, sharedMaterials[i], out var combines))
materials.Add(sharedMaterials[i]);
combines.Add((new CombineInstance { mesh = mesh, subMeshIndex = i, transform = Matrix4x4.identity }, bakeFlags));
var subMesh = mesh.GetSubMesh(i);
boneWeights[(mesh, i)] = new ArraySegment<BoneWeight>(weights, subMesh.firstVertex, subMesh.vertexCount);
}
if (vertices != null) mesh.GetVertices(vertices);
if (normals != null) mesh.GetNormals(normals);
if (tangents != null) mesh.GetTangents(tangents);
bool hasApplyBlendShape = false;
for (int i = 0, count = mesh.blendShapeCount; i < count; i++)
if (bakeFlags[i]) {
ApplyBlendShape(
mesh, vertices, normals, tangents, i,
skinnedMeshRenderer.GetBlendShapeWeight(i),
blendShapeCopyMode, ref vntArrayCache, ref vntArrayCache2
);
bakeList.Add((source, i));
hasApplyBlendShape = true;
} else
blendShapesWeights[mesh.GetBlendShapeName(i)] = skinnedMeshRenderer.GetBlendShapeWeight(i);
if (hasApplyBlendShape) {
if (vertices != null) mesh.SetVertices(vertices);
if (normals != null) mesh.SetNormals(normals);
if (tangents != null) mesh.SetTangents(tangents);
mesh.UploadMeshData(false);
}
source.enabled = false;
Undo.RecordObject(source, "Combine Meshes");
} else if (source is MeshRenderer meshRenderer && source.TryGetComponent(out MeshFilter meshFilter)) {
var mesh = Instantiate(meshFilter.sharedMesh);
var sharedMaterials = meshRenderer.sharedMaterials;
var subMeshCount = mesh.subMeshCount;
int index = 0;
if (!bakeFlags[0]) {
var key = (sourceTransform, Matrix4x4.identity);
if (!bindposeMap.TryGetValue(key, out index)) {
bindposeMap[key] = index = bindposeMap.Count;
allBindposes.Add(Matrix4x4.identity);
allBones.Add(sourceTransform);
}
}
var bakeFlags2 = Array.ConvertAll(bakeFlags, x => true);
var transform = bakeFlags[0] ? sourceTransform.localToWorldMatrix * destination.transform.worldToLocalMatrix : Matrix4x4.identity;
for (int i = 0; i < subMeshCount; i++) {
if (LazyInitialize(combineInstances, sharedMaterials[i], out var combines))
materials.Add(sharedMaterials[i]);
combines.Add((new CombineInstance { mesh = mesh, subMeshIndex = i, transform = transform }, bakeFlags2));
boneWeights[(mesh, i)] = Enumerable.Repeat(bakeFlags[0] ? default : new BoneWeight { boneIndex0 = index, weight0 = 1 }, mesh.GetSubMesh(i).vertexCount);
}
source.enabled = false;
Undo.RecordObject(source, "Combine Meshes");
}
}
if (combineInstances.Count < 1) return null;
var bindPoseArray = allBindposes.ToArray();
// 1st pass: merge meshes with same material.
if (mergeSubMeshes)
foreach (var kv in combineInstances) {
var combines = kv.Value;
if (combines.Count < 2) continue;
var mesh = new Mesh();
var combineArray = combines.Select(entry => entry.Item1).ToArray();
mesh.CombineMeshes(combineArray, true, false);
boneWeights[(mesh, 0)] = combineArray.SelectMany(entry => boneWeights[(entry.mesh, entry.subMeshIndex)]).ToArray();
if (blendShapeCopyMode != BlendShapeCopyMode.None)
CopyBlendShapes(mesh, combines, blendShapeCopyMode, ref blendShapesStore, ref vntArrayCache, ref vntArrayCache2);
combines.Clear();
combines.Add((new CombineInstance { mesh = mesh, transform = Matrix4x4.identity }, new bool[mesh.blendShapeCount]));
}
var combinedNewMesh = new Mesh { name = $"{destination.name} Combined Mesh" };
var combineInstanceArray = materials.SelectMany(material => combineInstances[material]).ToArray();
combinedNewMesh.CombineMeshes(combineInstanceArray.Select(entry => entry.Item1).ToArray(), false, false);
combinedNewMesh.boneWeights = combineInstanceArray.SelectMany(entry => {
boneWeights.TryGetValue((entry.Item1.mesh, entry.Item1.subMeshIndex), out var weights);
return weights;
}).Where(x => x != null).ToArray();
combinedNewMesh.bindposes = bindPoseArray;
if (blendShapeCopyMode != BlendShapeCopyMode.None)
CopyBlendShapes(combinedNewMesh, combineInstanceArray, blendShapeCopyMode, ref blendShapesStore, ref vntArrayCache, ref vntArrayCache2);
foreach (var combines in combineInstances.Values) combines.Clear();
combinedNewMesh.RecalculateBounds();
combinedNewMesh.UploadMeshData(false);
destination.sharedMesh = combinedNewMesh;
destination.localBounds = combinedNewMesh.bounds;
destination.sharedMaterials = materials.ToArray();
destination.bones = allBones.ToArray();
if (destination.rootBone == null) destination.rootBone = destination.transform;
foreach (var kv in blendShapesWeights) {
var index = combinedNewMesh.GetBlendShapeIndex(kv.Key);
if (index >= 0) destination.SetBlendShapeWeight(index, kv.Value);
}
foreach (var (mesh, _) in boneWeights.Keys)
if (mesh != null) DestroyImmediate(mesh, false);
Undo.RecordObject(destination, "Combine Meshes");
return combinedNewMesh;
}
public static void CopyBlendShapes(
Mesh combinedNewMesh,
IEnumerable<(CombineInstance, bool[])> combineInstances,
BlendShapeCopyMode copyMode = BlendShapeCopyMode.Vertices
) {
Dictionary<string, BlendShapeTimeLine> blendShapesStore = null;
Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache = null;
Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache2 = null;
CopyBlendShapes(combinedNewMesh, combineInstances, copyMode, ref blendShapesStore, ref vntArrayCache, ref vntArrayCache2);
}
static void CopyBlendShapes(
Mesh combinedNewMesh,
IEnumerable<(CombineInstance, bool[])> combineInstances,
BlendShapeCopyMode copyMode,
ref Dictionary<string, BlendShapeTimeLine> blendShapesStore,
ref Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache,
ref Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache2
) {
if (!LazyInitialize(ref blendShapesStore)) blendShapesStore.Clear();
int offset = 0;
foreach (var (entry, bakeFlags) in combineInstances) {
var mesh = entry.mesh;
var subMeshIndex = entry.subMeshIndex;
var subMesh = mesh.GetSubMesh(subMeshIndex);
for (int k = 0; k < mesh.blendShapeCount; k++) {
if (bakeFlags[k]) continue;
string key = mesh.GetBlendShapeName(k);
LazyInitialize(blendShapesStore, key, out var timeline);
timeline.AddFrom(mesh, subMeshIndex, k, offset);
}
offset += subMesh.vertexCount;
}
foreach (var timeline in blendShapesStore) timeline.Value.ApplyTo(combinedNewMesh, timeline.Key, copyMode, ref vntArrayCache, ref vntArrayCache2);
}
static bool LazyInitialize<TKey, TValue>(IDictionary<TKey, TValue> dict, TKey key, out TValue value) where TValue : new() {
if (!dict.TryGetValue(key, out value)) {
dict[key] = value = new TValue();
return true;
}
return false;
}
static bool LazyInitialize<T>(ref T value) where T : new() {
if (value == null) {
value = new T();
return true;
}
return false;
}
static void PreAllocate<T>(List<T> list, int capacity) {
capacity += list.Count;
if (list.Capacity < capacity) list.Capacity = capacity;
}
static (Vector3[], Vector3[], Vector3[]) GetVNTArrays(
ref Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache,
int vertexCount, BlendShapeCopyMode copyMode
) {
LazyInitialize(ref vntArrayCache);
if (!vntArrayCache.TryGetValue(vertexCount, out var vntArray))
vntArrayCache[vertexCount] = vntArray = (
copyMode.HasFlag(BlendShapeCopyMode.Vertices) ? new Vector3[vertexCount] : null,
copyMode.HasFlag(BlendShapeCopyMode.Normals) ? new Vector3[vertexCount] : null,
copyMode.HasFlag(BlendShapeCopyMode.Tangents) ? new Vector3[vertexCount] : null
);
return vntArray;
}
static void LerpVNTArray(
Vector3[] prev, int prevIndex, Vector3[] next, int nextIndex, Vector3[] dest, int destIndex,
float weight
) {
if (prev != null && next != null && dest != null)
dest[destIndex] = Vector3.Lerp(prev[prevIndex], next[nextIndex], weight);
}
static void CopyVNTArrays(
(Vector3[], Vector3[], Vector3[], float) frameData, (SubMeshDescriptor, int, int) subMeshData,
Vector3[] destDeltaVertices, Vector3[] destDeltaNormals, Vector3[] destDeltaTangents
) {
var (deltaVertices, deltaNormals, deltaTangents, _) = frameData;
var (subMesh, _, destOffset) = subMeshData;
var srcOffset = subMesh.firstVertex;
var srcVertexCount = subMesh.vertexCount;
if (deltaVertices != null && destDeltaVertices != null) Array.Copy(deltaVertices, srcOffset, destDeltaVertices, destOffset, srcVertexCount);
if (deltaNormals != null && destDeltaNormals != null) Array.Copy(deltaNormals, srcOffset, destDeltaNormals, destOffset, srcVertexCount);
if (deltaTangents != null && destDeltaTangents != null) Array.Copy(deltaTangents, srcOffset, destDeltaTangents, destOffset, srcVertexCount);
}
static void ApplyBlendShape(List<Vector3> source, Vector3[] blendShapeDataPrev, Vector3[] blendShapeDataNext, float lerp, int offset, int count) {
if (source == null) return;
for (int i = 0; i < count; i++) {
var index = offset + i;
source[index] += blendShapeDataPrev == null ? blendShapeDataNext[index] * lerp :
blendShapeDataNext == null || lerp <= 0 ? blendShapeDataPrev[index] :
Vector3.LerpUnclamped(blendShapeDataPrev[index], blendShapeDataNext[index], lerp);
}
}
static void ApplyBlendShape(List<Vector4> source, Vector3[] blendShapeDataPrev, Vector3[] blendShapeDataNext, float lerp, int offset, int count) {
if (source == null) return;
for (int i = 0; i < count; i++) {
var index = offset + i;
source[index] += (Vector4)(blendShapeDataPrev == null ? blendShapeDataNext[index] * lerp :
blendShapeDataNext == null || lerp <= 0 ? blendShapeDataPrev[index] :
Vector3.LerpUnclamped(blendShapeDataPrev[index], blendShapeDataNext[index], lerp));
}
}
static void ApplyBlendShape(
Mesh mesh, List<Vector3> vertices, List<Vector3> normals, List<Vector4> tangents,
int blendShapeIndex, float weight, BlendShapeCopyMode copyMode,
ref Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache,
ref Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache2
) {
var vertexCount = mesh.vertexCount;
var vntArray = GetVNTArrays(ref vntArrayCache, vertices.Count, copyMode);
var (deltaVertices, deltaNormals, deltaTangents) = vntArray;
int count = mesh.GetBlendShapeFrameCount(blendShapeIndex);
if (count == 0) return;
float frameWeight;
for (int i = 1; i < count; i++) {
frameWeight = mesh.GetBlendShapeFrameWeight(blendShapeIndex, i);
if (frameWeight > weight) {
mesh.GetBlendShapeFrameVertices(blendShapeIndex, i - 1, deltaVertices, deltaNormals, deltaTangents);
var vntArray2 = GetVNTArrays(ref vntArrayCache2, vertices.Count, copyMode);
var (deltaVertices2, deltaNormals2, deltaTangents2) = vntArray2;
mesh.GetBlendShapeFrameVertices(blendShapeIndex, i, deltaVertices2, deltaNormals2, deltaTangents2);
var nextFrameWeight = mesh.GetBlendShapeFrameWeight(blendShapeIndex, i);
var lerp = Mathf.InverseLerp(frameWeight, nextFrameWeight, weight);
ApplyBlendShape(vertices, deltaVertices, deltaVertices2, lerp, 0, vertexCount);
ApplyBlendShape(normals, deltaNormals, deltaNormals2, lerp, 0, vertexCount);
ApplyBlendShape(tangents, deltaTangents, deltaTangents2, lerp, 0, vertexCount);
return;
}
}
frameWeight = mesh.GetBlendShapeFrameWeight(blendShapeIndex, count - 1);
mesh.GetBlendShapeFrameVertices(blendShapeIndex, count - 1, deltaVertices, deltaNormals, deltaTangents);
weight /= frameWeight;
ApplyBlendShape(vertices, null, deltaVertices, weight, 0, vertexCount);
ApplyBlendShape(normals, null, deltaNormals, weight, 0, vertexCount);
ApplyBlendShape(tangents, null, deltaTangents, weight, 0, vertexCount);
}
class BlendShapeTimeLine {
readonly Dictionary<float, Dictionary<(Mesh, int), int>> frames = new Dictionary<float, Dictionary<(Mesh, int), int>>();
readonly Dictionary<(Mesh, int), (SubMeshDescriptor, int, int)> subMeshes = new Dictionary<(Mesh, int), (SubMeshDescriptor, int, int)>();
public void AddFrom(Mesh mesh, int subMeshIndex, int blendShapeIndex, int destOffset) {
var subMeshKey = (mesh, subMeshIndex);
if (subMeshes.ContainsKey(subMeshKey)) return;
var frameCount = mesh.GetBlendShapeFrameCount(blendShapeIndex);
if (frameCount < 1) return;
subMeshes[subMeshKey] = (mesh.GetSubMesh(subMeshIndex), blendShapeIndex, destOffset);
for (int i = 0; i < frameCount; i++) {
var weight = mesh.GetBlendShapeFrameWeight(blendShapeIndex, i);
LazyInitialize(frames, weight, out var frameIndexMap);
frameIndexMap[(mesh, subMeshIndex)] = i;
}
}
public void ApplyTo(
Mesh combinedMesh, string blendShapeName,
BlendShapeCopyMode copyMode,
ref Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache,
ref Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache2
) {
int destBlendShapeIndex = combinedMesh.GetBlendShapeIndex(blendShapeName);
if (destBlendShapeIndex >= 0) {
Debug.LogWarning($"Blend shape {blendShapeName} already exists in the combined mesh. Skipping.");
return;
}
var destVertexCount = combinedMesh.vertexCount;
var remainingMeshes = new HashSet<(Mesh, int)>();
var weights = new float[frames.Count];
frames.Keys.CopyTo(weights, 0);
Array.Sort(weights);
for (int i = 0; i < weights.Length; i++) {
var destDeltaVertices = copyMode.HasFlag(BlendShapeCopyMode.Vertices) ? new Vector3[destVertexCount] : null;
var destDeltaNormals = copyMode.HasFlag(BlendShapeCopyMode.Normals) ? new Vector3[destVertexCount] : null;
var destDeltaTangents = copyMode.HasFlag(BlendShapeCopyMode.Tangents) ? new Vector3[destVertexCount] : null;
var weight = weights[i];
var frameIndexMap = frames[weight];
remainingMeshes.UnionWith(subMeshes.Keys);
foreach (var kv in frameIndexMap) {
var subMeshKey = kv.Key;
var (srcMesh, _) = subMeshKey;
var srcSubMeshData = subMeshes[subMeshKey];
var (srcSubMesh, blendShapeIndex, _) = srcSubMeshData;
var (deltaVertices, deltaNormals, deltaTangents) = GetVNTArrays(ref vntArrayCache, srcMesh.vertexCount, copyMode);
srcMesh.GetBlendShapeFrameVertices(blendShapeIndex, kv.Value, deltaVertices, deltaNormals, deltaTangents);
CopyVNTArrays(
(deltaVertices, deltaNormals, deltaTangents, weight), srcSubMeshData,
destDeltaVertices, destDeltaNormals, destDeltaTangents
);
remainingMeshes.Remove(subMeshKey);
}
foreach (var key in remainingMeshes) {
var srcSubMeshData = subMeshes[key];
if (!SeekBlendShapeFrameData(key, srcSubMeshData, weights, i, false, copyMode, ref vntArrayCache, out var prevData)) {
if (SeekBlendShapeFrameData(key, srcSubMeshData, weights, i, true, copyMode, ref vntArrayCache, out prevData))
CopyVNTArrays(prevData, srcSubMeshData, destDeltaVertices, destDeltaNormals, destDeltaTangents);
} else if (!SeekBlendShapeFrameData(key, srcSubMeshData, weights, i, true, copyMode, ref vntArrayCache2, out var nextData)) {
if (SeekBlendShapeFrameData(key, srcSubMeshData, weights, i, false, copyMode, ref vntArrayCache, out nextData))
CopyVNTArrays(nextData, srcSubMeshData, destDeltaVertices, destDeltaNormals, destDeltaTangents);
} else {
var (srcSubMesh, _, destOffset) = srcSubMeshData;
var (prevDeltaVertices, prevDeltaNormals, prevDeltaTangents, prevWeight) = prevData;
var (nextDeltaVertices, nextDeltaNormals, nextDeltaTangents, nextWeight) = nextData;
float lerpWeight = Mathf.InverseLerp(prevWeight, nextWeight, weight);
for (int j = 0, srcOffset = srcSubMesh.firstVertex, srcVertexCount = srcSubMesh.vertexCount; j < srcVertexCount; j++) {
int srcOffset2 = srcOffset + j, destOffset2 = destOffset + j;
LerpVNTArray(prevDeltaVertices, srcOffset2, nextDeltaVertices, srcOffset2, destDeltaVertices, destOffset2, lerpWeight);
LerpVNTArray(prevDeltaNormals, srcOffset2, nextDeltaNormals, srcOffset2, destDeltaNormals, destOffset2, lerpWeight);
LerpVNTArray(prevDeltaTangents, srcOffset2, nextDeltaTangents, srcOffset2, destDeltaTangents, destOffset2, lerpWeight);
}
}
}
try {
combinedMesh.AddBlendShapeFrame(blendShapeName, weight, destDeltaVertices, destDeltaNormals, destDeltaTangents);
} catch (Exception e) {
Debug.LogError(e);
}
}
if (!copyMode.HasFlag(BlendShapeCopyMode.Normals)) combinedMesh.RecalculateNormals();
if (!copyMode.HasFlag(BlendShapeCopyMode.Tangents)) combinedMesh.RecalculateTangents();
}
bool SeekBlendShapeFrameData(
(Mesh, int) key, (SubMeshDescriptor, int, int) subMeshData,
float[] weights, int weightIndex, bool seekAscending, BlendShapeCopyMode copyMode,
ref Dictionary<int, (Vector3[], Vector3[], Vector3[])> vntArrayCache,
out (Vector3[], Vector3[], Vector3[], float) result
) {
while (true) {
if (seekAscending ? ++weightIndex >= weights.Length : --weightIndex < 0) {
result = default;
return false;
}
if (frames[weights[weightIndex]].TryGetValue(key, out var frameIndex)) {
var (srcMesh, _) = key;
var (_, blendShapeIndex, _) = subMeshData;
var (deltaVertices, deltaNormals, deltaTangents) = GetVNTArrays(ref vntArrayCache, srcMesh.vertexCount, copyMode);
srcMesh.GetBlendShapeFrameVertices(blendShapeIndex, frameIndex, deltaVertices, deltaNormals, deltaTangents);
result = (deltaVertices, deltaNormals, deltaTangents, weights[weightIndex]);
return true;
}
}
}
}
[Flags]
public enum BlendShapeCopyMode : byte {
None = 0,
Vertices = 0x1,
Normals = 0x2,
Tangents = 0x4,
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment