// 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();
}
}
}