//////////////////////////////////////////////////////
// Copyright (c) BrainFailProductions
//////////////////////////////////////////////////////
#if UNITY_2018_2_OR_NEWER
#define UNITY_8UV_SUPPORT
#endif
#if UNITY_2017_3_OR_NEWER
#define UNITY_MESH_INDEXFORMAT_SUPPORT
#endif
#if UNITY_MESH_INDEXFORMAT_SUPPORT
using UnityEngine.Rendering;
#endif
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace BrainFailProductions.PolyFewRuntime
{
public static class MeshCombiner
{
#region DATA_STRUCTURES
private static MeshRenderer[] unityCombinedMeshRenderers = null;
private static Material[] unityCombinedMeshesMats = null;
private static bool didUseUnityCombine = false;
public static bool generateUV2 = false;
public struct StaticRenderer
{
public string name;
public bool isNewMesh;
public Transform transform;
public Mesh mesh;
public Material[] materials;
}
public struct SkinnedRenderer
{
public bool hasBlendShapes;
public string name;
public bool isNewMesh;
public Transform transform;
public Mesh mesh;
public Material[] materials;
public Transform rootBone;
public Transform[] bones;
}
[Serializable]
public struct BlendShape
{
///
/// The name of the blend shape.
///
public string ShapeName;
///
/// The blend shape frames.
///
public BlendShapeFrame[] Frames;
///
/// Creates a new blend shape.
///
/// The name of the blend shape.
/// The blend shape frames.
public BlendShape(string shapeName, BlendShapeFrame[] frames)
{
this.ShapeName = shapeName;
this.Frames = frames;
}
}
[Serializable]
public struct BlendShapeFrame
{
///
/// The name of the blend shape this frame is associated with.
///
public string shapeName;
///
/// The weight of the blend shape frame.
///
public float frameWeight;
///
/// The delta vertices of the blend shape frame.
///
public Vector3[] deltaVertices;
///
/// The delta normals of the blend shape frame.
///
public Vector3[] deltaNormals;
///
/// The delta tangents of the blend shape frame.
///
public Vector3[] deltaTangents;
///
/// The vertex offset to be used in the combined mesh vertex array.
///
public int vertexOffset;
///
/// Creates a new blend shape frame.
///
/// The weight of the blend shape frame.
/// The delta vertices of the blend shape frame.
/// The delta normals of the blend shape frame.
/// The delta tangents of the blend shape frame.
public BlendShapeFrame(float frameWeight, Vector3[] deltaVertices, Vector3[] deltaNormals, Vector3[] deltaTangents)
{
this.frameWeight = frameWeight;
this.deltaVertices = deltaVertices;
this.deltaNormals = deltaNormals;
this.deltaTangents = deltaTangents;
this.shapeName = "";
this.vertexOffset = -1;
}
public BlendShapeFrame(string shapeName, float frameWeight, Vector3[] deltaVertices, Vector3[] deltaNormals, Vector3[] deltaTangents, int vertexOffset)
{
this.shapeName = shapeName;
this.frameWeight = frameWeight;
this.deltaVertices = deltaVertices;
this.deltaNormals = deltaNormals;
this.deltaTangents = deltaTangents;
this.vertexOffset = vertexOffset;
}
}
public static class MeshUtils
{
#region Consts
///
/// The count of supported UV channels.
///
#if UNITY_8UV_SUPPORT
public const int UVChannelCount = 8;
#else
public const int UVChannelCount = 4;
#endif
#endregion
#region Public Methods
///
/// Creates a new mesh.
///
/// The mesh vertices.
/// The mesh sub-mesh indices.
/// The mesh normals.
/// The mesh tangents.
/// The mesh colors.
/// The mesh bone-weights.
/// The mesh 4D UV sets.
/// The mesh bindposes.
/// The created mesh.
public static Mesh CreateMesh(Vector3[] vertices, int[][] indices, Vector3[] normals, Vector4[] tangents, Color[] colors, BoneWeight[] boneWeights, List[] uvs, Matrix4x4[] bindposes, BlendShape[] blendShapes)
{
return CreateMesh(vertices, indices, normals, tangents, colors, boneWeights, uvs, null, null, bindposes, blendShapes);
}
///
/// Creates a new mesh.
///
/// The mesh vertices.
/// The mesh sub-mesh indices.
/// The mesh normals.
/// The mesh tangents.
/// The mesh colors.
/// The mesh bone-weights.
/// The mesh 4D UV sets.
/// The mesh bindposes.
/// The created mesh.
public static Mesh CreateMesh(Vector3[] vertices, int[][] indices, Vector3[] normals, Vector4[] tangents, Color[] colors, BoneWeight[] boneWeights, List[] uvs, Matrix4x4[] bindposes, BlendShape[] blendShapes)
{
return CreateMesh(vertices, indices, normals, tangents, colors, boneWeights, null, null, uvs, bindposes, blendShapes);
}
///
/// Creates a new mesh.
///
/// The mesh vertices.
/// The mesh sub-mesh indices.
/// The mesh normals.
/// The mesh tangents.
/// The mesh colors.
/// The mesh bone-weights.
/// The mesh 2D UV sets.
/// The mesh 3D UV sets.
/// The mesh 4D UV sets.
/// The mesh bindposes.
/// The created mesh.
public static Mesh CreateMesh(Vector3[] vertices, int[][] indices, Vector3[] normals, Vector4[] tangents, Color[] colors, BoneWeight[] boneWeights, List[] uvs2D, List[] uvs3D, List[] uvs4D, Matrix4x4[] bindposes, BlendShape[] blendShapes)
{
var newMesh = new Mesh();
int subMeshCount = indices.Length;
#if UNITY_MESH_INDEXFORMAT_SUPPORT
IndexFormat indexFormat;
var indexMinMax = MeshUtils.GetSubMeshIndexMinMax(indices, out indexFormat);
newMesh.indexFormat = indexFormat;
#endif
if (bindposes != null && bindposes.Length > 0)
{
newMesh.bindposes = bindposes;
}
newMesh.subMeshCount = subMeshCount;
newMesh.vertices = vertices;
// If after assigning normals blendshapes are assigned, then blendshapes do not work correctly
// In URP and HDRP configurations, so we add blendshapes first and then assign normals
if (blendShapes != null)
{
MeshUtils.ApplyMeshBlendShapes(newMesh, blendShapes);
}
if (normals != null && normals.Length > 0)
{
newMesh.normals = normals;
}
if (tangents != null && tangents.Length > 0)
{
newMesh.tangents = tangents;
}
if (colors != null && colors.Length > 0)
{
newMesh.colors = colors;
}
if (boneWeights != null && boneWeights.Length > 0)
{
newMesh.boneWeights = boneWeights;
}
if (uvs2D != null)
{
for (int uvChannel = 0; uvChannel < uvs2D.Length; uvChannel++)
{
if (uvs2D[uvChannel] != null && uvs2D[uvChannel].Count > 0)
{
newMesh.SetUVs(uvChannel, uvs2D[uvChannel]);
}
}
}
if (uvs3D != null)
{
for (int uvChannel = 0; uvChannel < uvs3D.Length; uvChannel++)
{
if (uvs3D[uvChannel] != null && uvs3D[uvChannel].Count > 0)
{
newMesh.SetUVs(uvChannel, uvs3D[uvChannel]);
}
}
}
if (uvs4D != null)
{
for (int uvChannel = 0; uvChannel < uvs4D.Length; uvChannel++)
{
if (uvs4D[uvChannel] != null && uvs4D[uvChannel].Count > 0)
{
newMesh.SetUVs(uvChannel, uvs4D[uvChannel]);
}
}
}
//if (blendShapes != null)
//{
// MeshUtils.ApplyMeshBlendShapes(newMesh, blendShapes); //baw did
//}
for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++)
{
var subMeshTriangles = indices[subMeshIndex];
#if UNITY_MESH_INDEXFORMAT_SUPPORT
var minMax = indexMinMax[subMeshIndex];
if (indexFormat == UnityEngine.Rendering.IndexFormat.UInt16 && minMax.y > ushort.MaxValue)
{
int baseVertex = minMax.x;
for (int index = 0; index < subMeshTriangles.Length; index++)
{
subMeshTriangles[index] -= baseVertex;
}
newMesh.SetTriangles(subMeshTriangles, subMeshIndex, false, baseVertex);
}
else
{
newMesh.SetTriangles(subMeshTriangles, subMeshIndex, false, 0);
}
#else
newMesh.SetTriangles(subMeshTriangles, subMeshIndex, false);
#endif
}
newMesh.RecalculateBounds();
return newMesh;
}
///
/// Returns the blend shapes of a mesh.
///
/// The mesh.
/// The mesh blend shapes.
public static BlendShape[] GetMeshBlendShapes(Mesh mesh)
{
if (mesh == null)
throw new ArgumentNullException(nameof(mesh));
int vertexCount = mesh.vertexCount;
int blendShapeCount = mesh.blendShapeCount;
if (blendShapeCount == 0)
return null;
var blendShapes = new BlendShape[blendShapeCount];
for (int blendShapeIndex = 0; blendShapeIndex < blendShapeCount; blendShapeIndex++)
{
string shapeName = mesh.GetBlendShapeName(blendShapeIndex);
int frameCount = mesh.GetBlendShapeFrameCount(blendShapeIndex);
var frames = new BlendShapeFrame[frameCount];
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
float frameWeight = mesh.GetBlendShapeFrameWeight(blendShapeIndex, frameIndex);
var deltaVertices = new Vector3[vertexCount];
var deltaNormals = new Vector3[vertexCount];
var deltaTangents = new Vector3[vertexCount];
mesh.GetBlendShapeFrameVertices(blendShapeIndex, frameIndex, deltaVertices, deltaNormals, deltaTangents);
frames[frameIndex] = new BlendShapeFrame(frameWeight, deltaVertices, deltaNormals, deltaTangents);
}
blendShapes[blendShapeIndex] = new BlendShape(shapeName, frames);
}
return blendShapes;
}
///
/// Applies and overrides the specified blend shapes on the specified mesh.
///
/// The mesh.
/// The mesh blend shapes.
public static void ApplyMeshBlendShapes(Mesh mesh, BlendShape[] blendShapes)
{
if (mesh == null)
throw new ArgumentNullException(nameof(mesh));
mesh.ClearBlendShapes();
if (blendShapes == null || blendShapes.Length == 0)
return;
for (int blendShapeIndex = 0; blendShapeIndex < blendShapes.Length; blendShapeIndex++)
{
string shapeName = blendShapes[blendShapeIndex].ShapeName;
var frames = blendShapes[blendShapeIndex].Frames;
if (frames != null)
{
for (int frameIndex = 0; frameIndex < frames.Length; frameIndex++)
{
mesh.AddBlendShapeFrame(shapeName, frames[frameIndex].frameWeight, frames[frameIndex].deltaVertices, frames[frameIndex].deltaNormals, frames[frameIndex].deltaTangents);
}
}
}
}
///
/// Returns the UV sets for a specific mesh.
///
/// The mesh.
/// The UV sets.
public static List[] GetMeshUVs(Mesh mesh)
{
if (mesh == null)
throw new ArgumentNullException(nameof(mesh));
var uvs = new List[UVChannelCount];
for (int channel = 0; channel < UVChannelCount; channel++)
{
uvs[channel] = GetMeshUVs(mesh, channel);
}
return uvs;
}
///
/// Returns the UV list for a specific mesh and UV channel.
///
/// The mesh.
/// The UV channel.
/// The UV list.
public static List GetMeshUVs(Mesh mesh, int channel)
{
if (mesh == null)
throw new ArgumentNullException(nameof(mesh));
else if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException(nameof(channel));
var uvList = new List(mesh.vertexCount);
mesh.GetUVs(channel, uvList);
return uvList;
}
///
/// Returns the number of used UV components in a UV set.
///
/// The UV set.
/// The number of used UV components.
public static int GetUsedUVComponents(List uvs)
{
if (uvs == null || uvs.Count == 0)
return 0;
int usedComponents = 0;
foreach (var uv in uvs)
{
if (usedComponents < 1 && uv.x != 0f)
{
usedComponents = 1;
}
if (usedComponents < 2 && uv.y != 0f)
{
usedComponents = 2;
}
if (usedComponents < 3 && uv.z != 0f)
{
usedComponents = 3;
}
if (usedComponents < 4 && uv.w != 0f)
{
usedComponents = 4;
break;
}
}
return usedComponents;
}
///
/// Converts a list of 4D UVs into 2D.
///
/// The list of UVs.
/// The array of 2D UVs.
public static Vector2[] ConvertUVsTo2D(List uvs)
{
if (uvs == null)
return null;
var uv2D = new Vector2[uvs.Count];
for (int i = 0; i < uv2D.Length; i++)
{
var uv = uvs[i];
uv2D[i] = new Vector2(uv.x, uv.y);
}
return uv2D;
}
///
/// Converts a list of 4D UVs into 3D.
///
/// The list of UVs.
/// The array of 3D UVs.
public static Vector3[] ConvertUVsTo3D(List uvs)
{
if (uvs == null)
return null;
var uv3D = new Vector3[uvs.Count];
for (int i = 0; i < uv3D.Length; i++)
{
var uv = uvs[i];
uv3D[i] = new Vector3(uv.x, uv.y, uv.z);
}
return uv3D;
}
#if UNITY_MESH_INDEXFORMAT_SUPPORT
///
/// Returns the minimum and maximum indices for each submesh along with the needed index format.
///
/// The indices for the submeshes.
/// The output index format.
/// The minimum and maximum indices for each submesh.
public static Vector2Int[] GetSubMeshIndexMinMax(int[][] indices, out IndexFormat indexFormat)
{
if (indices == null)
throw new ArgumentNullException(nameof(indices));
var result = new Vector2Int[indices.Length];
indexFormat = IndexFormat.UInt16;
for (int subMeshIndex = 0; subMeshIndex < indices.Length; subMeshIndex++)
{
int minIndex, maxIndex;
GetIndexMinMax(indices[subMeshIndex], out minIndex, out maxIndex);
result[subMeshIndex] = new Vector2Int(minIndex, maxIndex);
int indexRange = (maxIndex - minIndex);
if (indexRange > ushort.MaxValue)
{
indexFormat = IndexFormat.UInt32;
}
}
return result;
}
#endif
#endregion
#region Private Methods
private static void GetIndexMinMax(int[] indices, out int minIndex, out int maxIndex)
{
if (indices == null || indices.Length == 0)
{
minIndex = maxIndex = 0;
return;
}
minIndex = int.MaxValue;
maxIndex = int.MinValue;
for (int i = 0; i < indices.Length; i++)
{
if (indices[i] < minIndex)
{
minIndex = indices[i];
}
if (indices[i] > maxIndex)
{
maxIndex = indices[i];
}
}
}
#endregion
}
#endregion DATA_STRUCTURES
#region PUBLIC_METHODS
public static StaticRenderer[] GetStaticRenderers(MeshRenderer[] renderers)
{
var newRenderers = new List(renderers.Length);
for (int rendererIndex = 0; rendererIndex < renderers.Length; rendererIndex++)
{
var renderer = renderers[rendererIndex];
var meshFilter = renderer.GetComponent();
if (meshFilter == null)
{
Debug.LogWarning("A renderer was missing a mesh filter and was ignored.", renderer);
continue;
}
var mesh = meshFilter.sharedMesh;
if (mesh == null)
{
Debug.LogWarning("A renderer was missing a mesh and was ignored.", renderer);
continue;
}
newRenderers.Add(new StaticRenderer()
{
name = renderer.name,
isNewMesh = false,
transform = renderer.transform,
mesh = mesh,
materials = renderer.sharedMaterials
});
}
return newRenderers.ToArray();
}
public static SkinnedRenderer[] GetSkinnedRenderers(SkinnedMeshRenderer[] renderers)
{
var newRenderers = new List(renderers.Length);
for (int rendererIndex = 0; rendererIndex < renderers.Length; rendererIndex++)
{
var renderer = renderers[rendererIndex];
var mesh = renderer.sharedMesh;
if (mesh == null)
{
Debug.LogWarning("A renderer was missing a mesh and was ignored.", renderer);
continue;
}
newRenderers.Add(new SkinnedRenderer()
{
name = renderer.name,
isNewMesh = false,
transform = renderer.transform,
mesh = mesh,
materials = renderer.sharedMaterials,
rootBone = renderer.rootBone,
bones = renderer.bones
});
}
return newRenderers.ToArray();
}
public static StaticRenderer[] CombineStaticMeshes(Transform transform, int levelIndex, MeshRenderer[] renderers, bool autoName = true, string combinedBaseName = "")
{
if (renderers.Length == 0)
return null;
var newRenderers = new List(renderers.Length);
if (renderers.Length > 1)
{
var staticMeshes = (from renderer in renderers
where renderer.GetComponent() != null && renderer.GetComponent().sharedMesh != null
select renderer.GetComponent()).ToArray();
CombineMeshesUnity(transform, staticMeshes);
didUseUnityCombine = true;
}
Material[] combinedMaterials;
Mesh combinedMesh;
if (unityCombinedMeshRenderers == null)
{
combinedMesh = CombineMeshes(transform, renderers, out combinedMaterials);
}
else
{
if (unityCombinedMeshRenderers.Length == 1)
{
combinedMaterials = unityCombinedMeshesMats.ToArray();
combinedMesh = unityCombinedMeshRenderers[0].GetComponent().sharedMesh;
}
else if (unityCombinedMeshRenderers.Length == 0)
{
combinedMesh = CombineMeshes(transform, renderers, out combinedMaterials);
}
else
{
combinedMesh = CombineMeshes(transform, unityCombinedMeshRenderers, out combinedMaterials);
}
}
if (unityCombinedMeshRenderers != null)
{
foreach (var item in unityCombinedMeshRenderers) { UnityEngine.GameObject.DestroyImmediate(item.gameObject); }
}
unityCombinedMeshRenderers = null;
unityCombinedMeshesMats = null;
string baseName = string.IsNullOrWhiteSpace(combinedBaseName) ? transform.name : combinedBaseName;
string rendererName = string.Format("{0}_combined_static", baseName);
if (autoName)
{
if (transform != null)
{
combinedMesh.name = string.Format("{0}_static{1:00}", transform.name, levelIndex);
}
}
newRenderers.Add(new StaticRenderer()
{
name = rendererName,
isNewMesh = true,
transform = null,
mesh = combinedMesh,
materials = combinedMaterials
});
#if UNITY_EDITOR
//UnityEditor.MeshUtility.Optimize(combinedMesh);
// Optimizing screws up the combined mesh sometimes by streching it into wierd shapes
if (generateUV2)
{
UnityEditor.Unwrapping.GenerateSecondaryUVSet(combinedMesh);
}
#endif
didUseUnityCombine = false;
return newRenderers.ToArray();
}
public static SkinnedRenderer[] CombineSkinnedMeshes(Transform transform, int levelIndex, SkinnedMeshRenderer[] renderers, ref SkinnedMeshRenderer[] renderersActuallyCombined, bool autoName = true, string combinedBaseName = "")
{
if (renderers.Length == 0)
return null;
// TODO: Support to merge sub-meshes and atlas textures
var newRenderers = new List(renderers.Length);
//var blendShapeRenderers = (from renderer in renderers
// where renderer.sharedMesh != null && renderer.sharedMesh.blendShapeCount > 0
// select renderer); //baw did
var renderersWithoutMesh = (from renderer in renderers
where renderer.sharedMesh == null
select renderer);
var combineRenderers = (from renderer in renderers
where renderer.sharedMesh != null // && renderer.sharedMesh.blendShapeCount == 0 baw did
select renderer).ToArray();
renderersActuallyCombined = combineRenderers;
// Warn about renderers without a mesh
foreach (var renderer in renderersWithoutMesh)
{
Debug.LogWarning("A renderer was missing a mesh and was ignored.", renderer);
}
//Don't combine meshes with blend shapes
//foreach (var renderer in blendShapeRenderers)
//{
// newRenderers.Add(new SkinnedRenderer()
// {
// name = renderer.name,
// isNewMesh = false,
// transform = renderer.transform,
// mesh = renderer.sharedMesh,
// materials = renderer.sharedMaterials,
// rootBone = renderer.rootBone,
// bones = renderer.bones,
// hasBlendShapes = true
// });
//}
if (combineRenderers.Length > 0)
{
Material[] combinedMaterials;
Transform[] combinedBones;
var combinedMesh = CombineMeshes(transform, combineRenderers, out combinedMaterials, out combinedBones);
string baseName = string.IsNullOrWhiteSpace(combinedBaseName) ? transform.name : combinedBaseName;
string rendererName = string.Format("{0}_combined_skinned", baseName);
if (autoName)
{
combinedMesh.name = string.Format("{0}_skinned{1:00}", transform.name, levelIndex);
}
var rootBone = FindBestRootBone(transform, combineRenderers);
newRenderers.Add(new SkinnedRenderer()
{
name = rendererName,
isNewMesh = false,
transform = null,
mesh = combinedMesh,
materials = combinedMaterials,
rootBone = rootBone,
bones = combinedBones
});
#if UNITY_EDITOR
//UnityEditor.MeshUtility.Optimize(combinedMesh);
// Optimizing screws up the combined mesh sometimes by streching it into wierd shapes
if (generateUV2)
{
UnityEditor.Unwrapping.GenerateSecondaryUVSet(combinedMesh);
}
#endif
}
return newRenderers.ToArray();
}
public static Mesh CombineMeshes(Transform rootTransform, MeshRenderer[] renderers, out Material[] resultMaterials, Dictionary topLevelParents = null, Dictionary blendShapes = null)
{
bool hasUnknownRootTransform = false;
if (rootTransform == null)
hasUnknownRootTransform = true;
//throw new System.ArgumentNullException(nameof(rootTransform));
if (renderers == null)
throw new System.ArgumentNullException(nameof(renderers));
var meshes = new Mesh[renderers.Length];
var transforms = new Matrix4x4[renderers.Length];
Tuple[] normalsTransforms = new Tuple[renderers.Length];
var materials = new Material[renderers.Length][];
for (int i = 0; i < renderers.Length; i++)
{
var renderer = renderers[i];
if (renderer == null)
throw new System.ArgumentException(string.Format("The renderer at index {0} is null.", i), nameof(renderers));
var rendererTransform = renderer.transform;
var meshFilter = renderer.GetComponent();
if (meshFilter == null)
throw new System.ArgumentException(string.Format("The renderer at index {0} has no mesh filter.", i), nameof(renderers));
else if (meshFilter.sharedMesh == null)
throw new System.ArgumentException(string.Format("The mesh filter for renderer at index {0} has no mesh.", i), nameof(renderers));
else if (!meshFilter.sharedMesh.isReadable)
throw new System.ArgumentException(string.Format("The mesh in the mesh filter for renderer at index {0} is not readable.", i), nameof(renderers));
meshes[i] = meshFilter.sharedMesh;
if (hasUnknownRootTransform)
{
rootTransform = topLevelParents[rendererTransform];
}
if (didUseUnityCombine) { transforms[i] = rendererTransform.localToWorldMatrix; }
else { transforms[i] = rootTransform.worldToLocalMatrix * rendererTransform.localToWorldMatrix; }
Vector3 lossyScale = rendererTransform.transform.lossyScale;
bool isUniformScale = Mathf.Approximately(lossyScale.x, lossyScale.y) && Mathf.Approximately(lossyScale.y, lossyScale.z);
if (!isUniformScale)
{
Debug.LogWarning($"The GameObject \"{rendererTransform.name}\" has non uniform scaling applied. This might cause the combined mesh normals to be incorrectly calculated resulting in slight variation in lighting.");
}
normalsTransforms[i] = Tuple.Create(rootTransform.localToWorldMatrix * rendererTransform.localToWorldMatrix, !isUniformScale); //baw did
materials[i] = renderer.sharedMaterials;
}
return CombineMeshes(meshes, transforms, normalsTransforms, materials, out resultMaterials, blendShapes);
}
public static Mesh CombineMeshes(Transform rootTransform, SkinnedMeshRenderer[] renderers, out Material[] resultMaterials, out Transform[] resultBones)
{
//if (rootTransform == null)
//throw new System.ArgumentNullException(nameof(rootTransform));
if (renderers == null)
throw new System.ArgumentNullException(nameof(renderers));
var meshes = new Mesh[renderers.Length];
var transforms = new Matrix4x4[renderers.Length];
Tuple[] normalsTransforms = new Tuple[renderers.Length];
var materials = new Material[renderers.Length][];
var bones = new Transform[renderers.Length][];
Dictionary blendShapes = new Dictionary();
int vertexOffset = 0;
for (int i = 0; i < renderers.Length; i++)
{
var renderer = renderers[i];
if (renderer == null)
throw new System.ArgumentException(string.Format("The renderer at index {0} is null.", i), nameof(renderers));
else if (renderer.sharedMesh == null)
throw new System.ArgumentException(string.Format("The renderer at index {0} has no mesh.", i), nameof(renderers));
else if (!renderer.sharedMesh.isReadable)
throw new System.ArgumentException(string.Format("The mesh in the renderer at index {0} is not readable.", i), nameof(renderers));
var rendererTransform = renderer.transform;
meshes[i] = renderer.sharedMesh;
// IF NO BONES
//transforms[i] = rootTransform.worldToLocalMatrix * rendererTransform.localToWorldMatrix;
//transforms[i] = rendererTransform.worldToLocalMatrix * rendererTransform.localToWorldMatrix;
// IF ANY BONES
Vector3 lossyScale = rendererTransform.transform.lossyScale;
bool isUniformScale = Mathf.Approximately(lossyScale.x, lossyScale.y) && Mathf.Approximately(lossyScale.y, lossyScale.z);
// baw did
if (renderer.bones == null || renderer.bones.Length == 0)
{
transforms[i] = rootTransform.worldToLocalMatrix * rendererTransform.localToWorldMatrix;
normalsTransforms[i] = Tuple.Create(rootTransform.localToWorldMatrix * rendererTransform.localToWorldMatrix, !isUniformScale); //baw did
}
else
{
transforms[i] = rendererTransform.worldToLocalMatrix * rendererTransform.localToWorldMatrix;
normalsTransforms[i] = Tuple.Create(rendererTransform.worldToLocalMatrix * rendererTransform.localToWorldMatrix, !isUniformScale);
}
if (!isUniformScale)
{
Debug.LogWarning($"The GameObject \"{rendererTransform.name}\" has non uniform scaling applied. This might cause the combined mesh normals to be incorrectly calculated resulting in slight variation in lighting.");
}
materials[i] = renderer.sharedMaterials;
bones[i] = renderer.bones;
for (int a = 0; a < bones[i].Length; a++)
{
Transform t = bones[i][a];
MeshFilter mf = t == null ? null : t.GetComponent();
Mesh m = mf == null ? null : mf.sharedMesh;
if (m != null)
{
Debug.LogWarning($"You have a static mesh attached to the bone:\"{t.name}\". The mesh combination logic will not deal with this properly, since that would require it to modify the original game object hierarchy. You might get erroneous results on mesh combination.");
}
}
Mesh mesh = renderer.sharedMesh;
int rendererId = renderer.GetHashCode();
if (mesh.blendShapeCount > 0)
{
for (int s = 0; s < mesh.blendShapeCount; s++)
{
for (int f = 0; f < mesh.GetBlendShapeFrameCount(s); f++)
{
Vector3[] deltaVertices = new Vector3[mesh.vertexCount];
Vector3[] deltaNormals = new Vector3[mesh.vertexCount];
Vector3[] deltaTangents = new Vector3[mesh.vertexCount];
if (!blendShapes.ContainsKey(mesh.GetBlendShapeName(s) + rendererId))
{
mesh.GetBlendShapeFrameVertices(s, f, deltaVertices, deltaNormals, deltaTangents);
blendShapes.Add(mesh.GetBlendShapeName(s) + rendererId, new BlendShapeFrame(mesh.GetBlendShapeName(s) + rendererId, mesh.GetBlendShapeFrameWeight(s, f), deltaVertices, deltaNormals, deltaTangents, vertexOffset));
}
}
}
}
vertexOffset += mesh.vertexCount;
}
return CombineMeshes(meshes, transforms, normalsTransforms, materials, bones, out resultMaterials, out resultBones, blendShapes);
}
public static Mesh CombineMeshes(Mesh[] meshes, Matrix4x4[] transforms, Tuple[] normalsTransforms, Material[][] materials, out Material[] resultMaterials, Dictionary blendShapes = null)
{
if (meshes == null)
throw new System.ArgumentNullException(nameof(meshes));
else if (transforms == null)
throw new System.ArgumentNullException(nameof(transforms));
else if (materials == null)
throw new System.ArgumentNullException(nameof(materials));
Transform[] resultBones;
return CombineMeshes(meshes, transforms, normalsTransforms, materials, null, out resultMaterials, out resultBones, blendShapes);
}
public static Mesh CombineMeshes(Mesh[] meshes, Matrix4x4[] transforms, Tuple[] normalsTransforms, Material[][] materials, Transform[][] bones, out Material[] resultMaterials, out Transform[] resultBones, Dictionary blendShapes = null)
{
if (meshes == null)
throw new System.ArgumentNullException(nameof(meshes));
else if (transforms == null)
throw new System.ArgumentNullException(nameof(transforms));
else if (materials == null)
throw new System.ArgumentNullException(nameof(materials));
else if (transforms.Length != meshes.Length)
throw new System.ArgumentException("The array of transforms doesn't have the same length as the array of meshes.", nameof(transforms));
else if (materials.Length != meshes.Length)
throw new System.ArgumentException("The array of materials doesn't have the same length as the array of meshes.", nameof(materials));
else if (bones != null && bones.Length != meshes.Length)
throw new System.ArgumentException("The array of bones doesn't have the same length as the array of meshes.", nameof(bones));
int totalVertexCount = 0;
int totalSubMeshCount = 0;
for (int meshIndex = 0; meshIndex < meshes.Length; meshIndex++)
{
var mesh = meshes[meshIndex];
if (mesh == null)
throw new System.ArgumentException(string.Format("The mesh at index {0} is null.", meshIndex), nameof(meshes));
else if (!mesh.isReadable)
throw new System.ArgumentException(string.Format("The mesh at index {0} is not readable.", meshIndex), nameof(meshes));
totalVertexCount += mesh.vertexCount;
totalSubMeshCount += mesh.subMeshCount;
// Validate the mesh materials
var meshMaterials = materials[meshIndex];
if (meshMaterials == null)
throw new System.ArgumentException(string.Format("The materials for mesh at index {0} is null.", meshIndex), nameof(materials));
else if (meshMaterials.Length != mesh.subMeshCount)
throw new System.ArgumentException(string.Format("The materials for mesh at index {0} doesn't match the submesh count ({1} != {2}).", meshIndex, meshMaterials.Length, mesh.subMeshCount), nameof(materials));
for (int materialIndex = 0; materialIndex < meshMaterials.Length; materialIndex++)
{
if (meshMaterials[materialIndex] == null)
throw new System.ArgumentException(string.Format("The material at index {0} for mesh {1} is null.", materialIndex, mesh.name), nameof(materials));
}
// Validate the mesh bones
if (bones != null)
{
var meshBones = bones[meshIndex];
if (meshBones == null)
throw new System.ArgumentException(string.Format("The bones for mesh at index {0} is null.", meshIndex), nameof(meshBones));
for (int boneIndex = 0; boneIndex < meshBones.Length; boneIndex++)
{
if (meshBones[boneIndex] == null)
throw new System.ArgumentException(string.Format("The bone at index {0} for mesh at index {1} is null.", boneIndex, meshIndex), nameof(meshBones));
}
}
}
var combinedVertices = new List(totalVertexCount);
var combinedIndices = new List(totalSubMeshCount);
List combinedNormals = null;
List combinedTangents = null;
List combinedColors = null;
List combinedBoneWeights = null;
var combinedUVs = new List[MeshUtils.UVChannelCount];
List usedBindposes = null;
List usedBones = null;
var usedMaterials = new List(totalSubMeshCount);
var materialMap = new Dictionary(totalSubMeshCount);
int currentVertexCount = 0;
for (int meshIndex = 0; meshIndex < meshes.Length; meshIndex++)
{
var mesh = meshes[meshIndex];
var meshTransform = transforms[meshIndex];
var normalsTransform = normalsTransforms[meshIndex];
var meshMaterials = materials[meshIndex];
var meshBones = (bones != null ? bones[meshIndex] : null);
int subMeshCount = mesh.subMeshCount;
int meshVertexCount = mesh.vertexCount;
var meshVertices = mesh.vertices;
var meshNormals = mesh.normals;
var meshTangents = mesh.tangents;
var meshUVs = MeshUtils.GetMeshUVs(mesh);
var meshColors = mesh.colors;
var meshBoneWeights = mesh.boneWeights;
var meshBindposes = mesh.bindposes;
// Transform vertices with bones to keep only one bindpose
if (meshBones != null && meshBoneWeights != null && meshBoneWeights.Length > 0 && meshBindposes != null && meshBindposes.Length > 0 && meshBones.Length == meshBindposes.Length)
{
if (usedBindposes == null)
{
usedBindposes = new List(meshBindposes);
usedBones = new List(meshBones);
}
int[] boneIndices = new int[meshBones.Length];
for (int i = 0; i < meshBones.Length; i++)
{
int usedBoneIndex = usedBones.IndexOf(meshBones[i]);
if (usedBoneIndex == -1 || meshBindposes[i] != usedBindposes[usedBoneIndex])
{
usedBoneIndex = usedBones.Count;
usedBones.Add(meshBones[i]);
usedBindposes.Add(meshBindposes[i]);
}
boneIndices[i] = usedBoneIndex;
}
// Then we remap the bones
RemapBones(meshBoneWeights, boneIndices);
}
// Transforms the vertices, normals and tangents using the mesh transform
TransformVertices(meshVertices, ref meshTransform);
TransformNormals(meshNormals, ref normalsTransform);
TransformTangents(meshTangents, ref normalsTransform);
// Copy vertex positions & attributes
CopyVertexPositions(combinedVertices, meshVertices);
CopyVertexAttributes(ref combinedNormals, meshNormals, currentVertexCount, meshVertexCount, totalVertexCount, new Vector3(1f, 0f, 0f));
CopyVertexAttributes(ref combinedTangents, meshTangents, currentVertexCount, meshVertexCount, totalVertexCount, new Vector4(0f, 0f, 1f, 1f));
CopyVertexAttributes(ref combinedColors, meshColors, currentVertexCount, meshVertexCount, totalVertexCount, new Color(1f, 1f, 1f, 1f));
CopyVertexAttributes(ref combinedBoneWeights, meshBoneWeights, currentVertexCount, meshVertexCount, totalVertexCount, new BoneWeight());
for (int channel = 0; channel < meshUVs.Length; channel++)
{
CopyVertexAttributes(ref combinedUVs[channel], meshUVs[channel], currentVertexCount, meshVertexCount, totalVertexCount, new Vector4(0f, 0f, 0f, 0f));
}
for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++)
{
var subMeshMaterial = meshMaterials[subMeshIndex];
#if UNITY_MESH_INDEXFORMAT_SUPPORT
var subMeshIndices = mesh.GetTriangles(subMeshIndex, true);
#else
var subMeshIndices = mesh.GetTriangles(subMeshIndex);
#endif
if (currentVertexCount > 0)
{
for (int index = 0; index < subMeshIndices.Length; index++)
{
subMeshIndices[index] += currentVertexCount;
}
}
int existingSubMeshIndex;
if (materialMap.TryGetValue(subMeshMaterial, out existingSubMeshIndex))
{
combinedIndices[existingSubMeshIndex] = MergeArrays(combinedIndices[existingSubMeshIndex], subMeshIndices);
}
else
{
int materialIndex = combinedIndices.Count;
materialMap.Add(subMeshMaterial, materialIndex);
usedMaterials.Add(subMeshMaterial);
combinedIndices.Add(subMeshIndices);
}
}
currentVertexCount += meshVertexCount;
}
var resultVertices = combinedVertices.ToArray();
var resultIndices = combinedIndices.ToArray();
var resultNormals = (combinedNormals != null ? combinedNormals.ToArray() : null);
var resultTangents = (combinedTangents != null ? combinedTangents.ToArray() : null);
var resultColors = (combinedColors != null ? combinedColors.ToArray() : null);
var resultBoneWeights = (combinedBoneWeights != null ? combinedBoneWeights.ToArray() : null);
var resultUVs = combinedUVs.ToArray();
var resultBindposes = (usedBindposes != null ? usedBindposes.ToArray() : null);
resultMaterials = usedMaterials.ToArray();
resultBones = (usedBones != null ? usedBones.ToArray() : null);
Mesh combinedMesh = MeshUtils.CreateMesh(resultVertices, resultIndices, resultNormals, resultTangents, resultColors, resultBoneWeights, resultUVs, resultBindposes, null);
if (blendShapes != null && blendShapes.Count > 0)
{
foreach (BlendShapeFrame blendShape in blendShapes.Values)
{
Vector3[] deltaVertices = new Vector3[combinedMesh.vertexCount];
Vector3[] deltaNormals = new Vector3[combinedMesh.vertexCount];
Vector3[] deltaTangents = new Vector3[combinedMesh.vertexCount];
for (int p = 0; p < blendShape.deltaVertices.Length; p++)
{
deltaVertices.SetValue(blendShape.deltaVertices[p], p + blendShape.vertexOffset);
deltaNormals.SetValue(blendShape.deltaNormals[p], p + blendShape.vertexOffset);
deltaTangents.SetValue(blendShape.deltaTangents[p], p + blendShape.vertexOffset);
}
combinedMesh.AddBlendShapeFrame(blendShape.shapeName, blendShape.frameWeight, deltaVertices, deltaNormals, deltaTangents);
}
}
// If after assigning normals blendshapes are assigned, then blendshapes do not work correctly
// In URP and HDRP configurations, so we add blendshapes first and then assign normals
combinedMesh.normals = resultNormals;
combinedMesh.tangents = resultTangents;
combinedMesh.RecalculateBounds();
return combinedMesh;
}
#endregion PUBLIC_METHODS
#region PRIVATE_METHODS
private static void ParentAndResetTransform(Transform transform, Transform parentTransform)
{
transform.SetParent(parentTransform);
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.localScale = Vector3.one;
}
private static void ParentAndOffsetTransform(Transform transform, Transform parentTransform, Transform originalTransform)
{
transform.position = originalTransform.position;
transform.rotation = originalTransform.rotation;
transform.localScale = originalTransform.lossyScale;
transform.SetParent(parentTransform, true);
}
private static Transform FindBestRootBone(Transform transform, SkinnedMeshRenderer[] skinnedMeshRenderers)
{
if (skinnedMeshRenderers == null || skinnedMeshRenderers.Length == 0)
return null;
Transform bestBone = null;
float bestDistance = float.MaxValue;
for (int i = 0; i < skinnedMeshRenderers.Length; i++)
{
if (skinnedMeshRenderers[i] == null || skinnedMeshRenderers[i].rootBone == null)
continue;
var rootBone = skinnedMeshRenderers[i].rootBone;
var distance = (rootBone.position - transform.position).sqrMagnitude;
if (distance < bestDistance)
{
bestBone = rootBone;
bestDistance = distance;
}
}
return bestBone;
}
private static Transform FindBestRootBone(Dictionary topLevelParents, SkinnedMeshRenderer[] skinnedMeshRenderers)
{
if (skinnedMeshRenderers == null || skinnedMeshRenderers.Length == 0)
return null;
Transform bestBone = null;
float bestDistance = float.MaxValue;
for (int i = 0; i < skinnedMeshRenderers.Length; i++)
{
if (skinnedMeshRenderers[i] == null || skinnedMeshRenderers[i].rootBone == null)
continue;
Transform topParent = topLevelParents[skinnedMeshRenderers[i].transform];
var rootBone = skinnedMeshRenderers[i].rootBone;
var distance = (rootBone.position - topParent.position).sqrMagnitude;
if (distance < bestDistance)
{
bestBone = rootBone;
bestDistance = distance;
}
}
return bestBone;
}
private static Transform GetTopLevelParent(Transform forObject)
{
Transform topLevelParent = forObject;
while (topLevelParent.parent != null) { topLevelParent = topLevelParent.parent; }
return topLevelParent;
}
private static void CopyVertexPositions(List list, Vector3[] arr)
{
if (arr == null || arr.Length == 0)
return;
for (int i = 0; i < arr.Length; i++)
{
list.Add(arr[i]);
}
}
private static void CopyVertexAttributes(ref List dest, IEnumerable src, int previousVertexCount, int meshVertexCount, int totalVertexCount, T defaultValue)
{
if (src == null || src.Count() == 0)
{
if (dest != null)
{
for (int i = 0; i < meshVertexCount; i++)
{
dest.Add(defaultValue);
}
}
return;
}
if (dest == null)
{
dest = new List(totalVertexCount);
for (int i = 0; i < previousVertexCount; i++)
{
dest.Add(defaultValue);
}
}
dest.AddRange(src);
}
private static T[] MergeArrays(T[] arr1, T[] arr2)
{
var newArr = new T[arr1.Length + arr2.Length];
System.Array.Copy(arr1, 0, newArr, 0, arr1.Length);
System.Array.Copy(arr2, 0, newArr, arr1.Length, arr2.Length);
return newArr;
}
private static void TransformVertices(Vector3[] vertices, ref Matrix4x4 transform)
{
for (int i = 0; i < vertices.Length; i++)
{
vertices[i] = transform.MultiplyPoint3x4(vertices[i]);
}
}
private static void TransformNormals(Vector3[] normals, ref Tuple transform)
{
if (normals == null)
return;
for (int i = 0; i < normals.Length; i++)
{
// Non-UniformScaling
if (transform.Item2 == true)
{
Quaternion rotation = Quaternion.LookRotation(transform.Item1.GetColumn(2), transform.Item1.GetColumn(1));
normals[i] = rotation * normals[i]; //baw did
}
else
{
normals[i] = transform.Item1.MultiplyVector(normals[i]);
}
}
}
private static void TransformTangents(Vector4[] tangents, ref Tuple transform)
{
if (tangents == null)
return;
Vector3 tengentDir;
for (int i = 0; i < tangents.Length; i++)
{
tengentDir = transform.Item1.MultiplyVector(new Vector3(tangents[i].x, tangents[i].y, tangents[i].z));
tangents[i] = new Vector4(tengentDir.x, tengentDir.y, tengentDir.z, tangents[i].w);
}
}
private static void RemapBones(BoneWeight[] boneWeights, int[] boneIndices)
{
for (int i = 0; i < boneWeights.Length; i++)
{
if (boneWeights[i].weight0 > 0)
{
boneWeights[i].boneIndex0 = boneIndices[boneWeights[i].boneIndex0];
}
if (boneWeights[i].weight1 > 0)
{
boneWeights[i].boneIndex1 = boneIndices[boneWeights[i].boneIndex1];
}
if (boneWeights[i].weight2 > 0)
{
boneWeights[i].boneIndex2 = boneIndices[boneWeights[i].boneIndex2];
}
if (boneWeights[i].weight3 > 0)
{
boneWeights[i].boneIndex3 = boneIndices[boneWeights[i].boneIndex3];
}
}
}
private static Matrix4x4 ScaleMatrix(ref Matrix4x4 matrix, float scale)
{
return new Matrix4x4()
{
m00 = matrix.m00 * scale,
m01 = matrix.m01 * scale,
m02 = matrix.m02 * scale,
m03 = matrix.m03 * scale,
m10 = matrix.m10 * scale,
m11 = matrix.m11 * scale,
m12 = matrix.m12 * scale,
m13 = matrix.m13 * scale,
m20 = matrix.m20 * scale,
m21 = matrix.m21 * scale,
m22 = matrix.m22 * scale,
m23 = matrix.m23 * scale,
m30 = matrix.m30 * scale,
m31 = matrix.m31 * scale,
m32 = matrix.m32 * scale,
m33 = matrix.m33 * scale
};
}
private static void CombineMeshesUnity(Transform parentTransform, MeshFilter[] meshFilters)
{
var combineMeshInstanceDictionary = new Dictionary>();
int totalVertsCount = 0;
foreach (var meshFilter in meshFilters)
{
// Check if we are in older versions of Unity with max vertex limit <= 65534
if (meshFilter == null) { continue; }
Mesh m = meshFilter.sharedMesh;
if (m == null) { continue; }
totalVertsCount += m.vertexCount;
}
foreach (var meshFilter in meshFilters)
{
var mesh = meshFilter.sharedMesh;
//var vertices = new List(); //uncomment when manual mesh duplication is uncommented
//mesh.GetVertices(vertices); //uncomment when manual mesh duplication is uncommented
var materials = meshFilter.GetComponent().sharedMaterials;
var subMeshCount = meshFilter.sharedMesh.subMeshCount;
if (materials == null)
{
throw new System.ArgumentException(string.Format("The materials for GameObject are null.", meshFilter.transform.name), nameof(materials));
}
else if (materials.Length != mesh.subMeshCount)
{
throw new System.ArgumentException(string.Format("The materials for mesh {0} on GameObject {1} doesn't match the submesh count ({2} != {3}).", mesh.name, meshFilter.transform.name, materials.Length, mesh.subMeshCount), nameof(materials));
}
for (int materialIndex = 0; materialIndex < materials.Length; materialIndex++)
{
if (materials[materialIndex] == null)
{
throw new System.ArgumentException(string.Format("The material at index {0} for mesh {1} on GameObject {2} is null.", materialIndex, mesh.name, meshFilter.transform.name), nameof(materials));
}
}
for (var i = 0; i < subMeshCount; i++)
{
var material = materials[i];
var triangles = new List();
mesh.GetTriangles(triangles, i);
//manual mesh duplication
//var newMesh = new Mesh
//{
// vertices = vertices.ToArray(),
// triangles = triangles.ToArray(),
// uv = mesh.uv,
// normals = mesh.normals,
// colors = mesh.colors
//};
var newMesh = UnityEngine.Object.Instantiate(mesh);
newMesh.triangles = triangles.ToArray();
if (!combineMeshInstanceDictionary.ContainsKey(material))
{
combineMeshInstanceDictionary.Add(material, new List());
}
//var combineInstance = new CombineInstance
//{ transform = meshFilter.transform.localToWorldMatrix, mesh = newMesh };
var combineInstance = new CombineInstance
{ transform = (parentTransform.worldToLocalMatrix * meshFilter.transform.localToWorldMatrix) , mesh = newMesh };
combineMeshInstanceDictionary[material].Add(combineInstance);
}
}
unityCombinedMeshRenderers = new MeshRenderer[combineMeshInstanceDictionary.Count];
unityCombinedMeshesMats = new Material[combineMeshInstanceDictionary.Count];
int index = 0;
foreach (var kvp in combineMeshInstanceDictionary)
{
var newObject = new GameObject(kvp.Key.name);
var meshRenderer = newObject.AddComponent();
var meshFilter = newObject.AddComponent();
meshRenderer.material = kvp.Key;
var combinedMesh = new Mesh();
#if UNITY_MESH_INDEXFORMAT_SUPPORT
if (totalVertsCount > 65534) { combinedMesh.indexFormat = IndexFormat.UInt32; }
#endif
combinedMesh.CombineMeshes(kvp.Value.ToArray());
meshFilter.sharedMesh = combinedMesh;
//#if UNITY_EDITOR
// UnityEditor.MeshUtility.Optimize(meshFilter.sharedMesh);
// if (generateUV2)
// {
// UnityEditor.Unwrapping.GenerateSecondaryUVSet(meshFilter.sharedMesh);
// }
//#endif
newObject.transform.parent = parentTransform.parent;
unityCombinedMeshesMats[index] = kvp.Key;
unityCombinedMeshRenderers[index] = meshRenderer;
index++;
}
}
#endregion PRIVATE_METHODS
}
}