// Copyright (c) 2024 Synty Studios Limited. All rights reserved. // // Use of this software is subject to the terms and conditions of the Synty Studios End User Licence Agreement (EULA) // available at: https://syntystore.com/pages/end-user-licence-agreement // // For additional details, see the LICENSE.MD file bundled with this software. using Synty.SidekickCharacters.Utils; using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Synty.SidekickCharacters.SkinnedMesh { /// /// Combines a set of given SkinnedMeshRenderers into a single SkinnedMEshRenderer. /// public static class Combiner { /// /// Merges meshes together, including maintaining blend shape data. /// /// Meshes to merge. /// The mesh to merge everything into. /// The SkinnedMeshRenderer to attach the combined mesh to. public static void MergeAndGetAllBlendShapeDataOfSkinnedMeshRenderers( SkinnedMeshRenderer[] skinnedMeshesToMerge, Mesh finalMesh, SkinnedMeshRenderer finalSkinnedMeshRenderer ) { List allBlendShapeData = new List(); //Verify each skinned mesh renderer and get info about all blendshapes of all meshes int totalVerticesVerifiedAtHereForBlendShapes = 0; foreach (SkinnedMeshRenderer combine in skinnedMeshesToMerge) { // Skip any parts that have not been assigned if (combine == null) { continue; } List newData = BlendShapeUtils.GetBlendShapeData( finalMesh, combine, Array.Empty(), totalVerticesVerifiedAtHereForBlendShapes, allBlendShapeData ); //Set vertices verified at here, after processing all blendshapes for this mesh totalVerticesVerifiedAtHereForBlendShapes += combine.sharedMesh.vertexCount; } BlendShapeUtils.RestoreBlendShapeData(allBlendShapeData, finalMesh, finalSkinnedMeshRenderer); } /// /// Processes the given GameObject and combines the objects contained into combined meshes grouped by their material. /// Then returns a new GameObject with the combined data. /// /// All of the meshes to combine into a single model /// The base model that has the base rig and where the skinned meshes will be combined to. /// The base material to use for the combined model. /// A new GameObject containing all the combined objects, grouped and combined by Material. public static GameObject CreateCombinedSkinnedMesh( List skinnedMeshesToCombine, GameObject baseModel, Material baseMaterial ) { // Create the new base GameObject. This will store all the combined meshes. GameObject combinedModel = new GameObject("Prefab Character"); GameObject combinedSkinnedMesh = new GameObject("mesh"); combinedSkinnedMesh.transform.parent = combinedModel.transform; Transform modelRootBone = baseModel.GetComponentInChildren().rootBone; // Initialise bone data stores. Transform[] bones = Array.Empty(); int boneCount = 0; skinnedMeshesToCombine.Sort((a, b) => string.Compare(a.name, b.name)); Material material = null; Mesh mesh = new Mesh(); int boneOffset = 0; GameObject rootBone = GameObject.Instantiate(modelRootBone.gameObject, combinedModel.transform, true); rootBone.name = modelRootBone.name; Hashtable boneNameMap = CreateBoneNameMap(rootBone); Transform[] additionalBones = FindAdditionalBones(boneNameMap, new List(skinnedMeshesToCombine)); if (additionalBones.Length > 0) { JoinAdditionalBonesToBoneArray(bones, additionalBones, boneNameMap); // Need to redo the name map now that we have updated the bone array. boneNameMap = CreateBoneNameMap(rootBone); } List combineInstances = new List(); List bindPosesToMerge = new List(); // Iterate through the skinned meshes and process them into Material groupings, and also process the bones as required. foreach (SkinnedMeshRenderer child in skinnedMeshesToCombine) { material = child.sharedMaterial; mesh = MeshUtils.CopyMesh(child.sharedMesh); boneCount += child.bones.Length; Transform[] existingBones = bones; bones = new Transform[boneCount]; Array.Copy(existingBones, bones, existingBones.Length); Transform[] newBones = new Transform[child.bones.Length]; for (int i = 0; i < newBones.Length; i++) { Transform currentBone = (Transform) boneNameMap[child.bones[i].name]; newBones[i] = currentBone; bindPosesToMerge.Add(currentBone.worldToLocalMatrix * child.transform.worldToLocalMatrix); } Array.Copy(newBones, 0, bones, boneOffset, child.bones.Length); boneOffset = bones.Length; Matrix4x4 transformMatrix = child.localToWorldMatrix; CombineInstance combineInstance = new CombineInstance(); combineInstance.mesh = mesh; combineInstance.transform = transformMatrix; combineInstances.Add(combineInstance); } SkinnedMeshRenderer renderer = combinedSkinnedMesh.AddComponent(); renderer.bones = bones; renderer.updateWhenOffscreen = true; Mesh newMesh = new Mesh(); newMesh.CombineMeshes(combineInstances.ToArray(), true, true); newMesh.RecalculateBounds(); newMesh.name = combinedModel.name; renderer.rootBone = combinedModel.transform.Find("root"); renderer.sharedMesh = newMesh; renderer.enabled = true; renderer.sharedMesh.bindposes = bindPosesToMerge.ToArray(); renderer.sharedMaterial = baseMaterial == null ? material : baseMaterial; MergeAndGetAllBlendShapeDataOfSkinnedMeshRenderers(skinnedMeshesToCombine.ToArray(), renderer.sharedMesh, renderer); return combinedModel; } /// /// Processes the movement of bones if required for the given movement dictionary. /// /// The bone name map that has all the bones of the rig. /// The dictionary of bones to process the movement from. /// The dictionary of bone rotations to process. public static void ProcessBoneMovement(Hashtable boneNameMap, Dictionary movementDictionary, Dictionary rotationDictionary) { Dictionary bonePositionDictionary = new Dictionary(); Dictionary boneRotationDictionary = new Dictionary(); Dictionary boneMovementDictionary = new Dictionary(); foreach (Transform currentBone in boneNameMap.Values) { // Store bone positions from rig before processing joints. bonePositionDictionary.TryAdd(currentBone.name, currentBone.transform.localPosition); boneRotationDictionary.TryAdd(currentBone.name, currentBone.transform.localRotation); if (movementDictionary.ContainsKey(currentBone.name)) { float jointDistance = Vector3.Distance(bonePositionDictionary[currentBone.name], movementDictionary[currentBone.name]); float rotationDistance = Quaternion.Angle(boneRotationDictionary[currentBone.name], rotationDictionary[currentBone.name]); // If the bone in the new part is at a different location, move the actual bone to the same position. if (jointDistance > 0.0001) { Vector3 rigMovement = movementDictionary[currentBone.name]; // If an existing joint movement exists, and is further from the standard joint position, use that instead. if (boneMovementDictionary.TryGetValue(currentBone.name, out Vector3 existingMovement) && Math.Abs(Vector3.Distance(bonePositionDictionary[currentBone.name], existingMovement)) > Math.Abs(jointDistance)) { rigMovement = existingMovement; } currentBone.transform.localPosition = rigMovement; boneMovementDictionary[currentBone.name] = rigMovement; } if (rotationDistance > 0.01) { Quaternion rigRotation = rotationDictionary[currentBone.name]; if (boneRotationDictionary.TryGetValue(currentBone.name, out Quaternion existingRotation) && Math.Abs(Quaternion.Angle(boneRotationDictionary[currentBone.name], existingRotation)) > Math.Abs(rotationDistance)) { rigRotation = existingRotation; } currentBone.transform.localRotation = rigRotation; boneRotationDictionary[currentBone.name] = rigRotation; } } } } /// /// Creates a map between bones and their names. /// /// The Current bone being mapped. /// A hashmap between bone names and bones. public static Hashtable CreateBoneNameMap(GameObject currentBone) { Hashtable boneNameMap = new Hashtable(); boneNameMap.Add(currentBone.name, currentBone.transform); for (int i = 0; i < currentBone.transform.childCount; i++) { Hashtable childBoneMap = CreateBoneNameMap(currentBone.transform.GetChild(i).gameObject); foreach (DictionaryEntry entry in childBoneMap) { if (!boneNameMap.ContainsKey(entry.Key)) { boneNameMap.Add(entry.Key, (Transform) entry.Value); } } } return boneNameMap; } /// /// Finds any bones in a given list of SkinnedMeshRenderers that aren't in the given bone map. /// /// The bone map to check for the existence of bones. /// The list of SkinnedMeshRenderers to check for additional bones. /// An array of all additional bones. public static Transform[] FindAdditionalBones(Hashtable boneMap, List meshes) { List newBones = new List(); foreach (SkinnedMeshRenderer mesh in meshes) { foreach (Transform bone in mesh.bones) { if (!boneMap.ContainsKey(bone.name)) { newBones.Add(bone); } } } return newBones.ToArray(); } /// /// Adds additional bones to the current bone array. /// /// The current bone array. /// The new bones to add. /// The current bone name map. /// The new bone array. public static Transform[] JoinAdditionalBonesToBoneArray(Transform[] bones, Transform[] additionBones, Hashtable boneMap) { List fullBones = new List(); fullBones.AddRange(bones); foreach (Transform bone in additionBones) { Transform newParent = (Transform) boneMap[bone.parent.name]; if (newParent != null && !newParent.Find(bone.name)) { GameObject newBone = GameObject.Instantiate(bone.gameObject, newParent); newBone.name = newBone.name.Replace("(Clone)", ""); fullBones.Add(newBone.transform); if (!boneMap.ContainsKey(bone.name)) { boneMap.Add(bone.name, newBone.transform); } } } return fullBones.ToArray(); } } }