////////////////////////////////////////////////////// // 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 } }