Files
2026-05-31 14:27:52 -07:00

601 lines
20 KiB
C#

using Unity.Collections;
using Unity.Entities;
using UnityEngine;
using FixedStringName = Unity.Collections.FixedString512Bytes;
using System.Reflection;
using System.Collections.Generic;
using System;
using Unity.Assertions;
using Unity.Mathematics;
using Rukhanka.Toolbox;
using Hash128 = Unity.Entities.Hash128;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
namespace Rukhanka.Hybrid
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
[TemporaryBakingType]
internal struct BoneEntitiesToRemove : IBufferElementData
{
public Entity boneEntity;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
[TemporaryBakingType]
internal struct BoneEntityRef: IBufferElementData
{
public Entity boneEntity;
public int rigBoneIndex;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
internal class InternalSkeletonBone
{
public Transform boneTransform;
public string name;
public string parentName;
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
}
//=================================================================================================================//
public partial class RigDefinitionBaker: Baker<RigDefinitionAuthoring>
{
static FieldInfo parentBoneNameField;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static RigDefinitionBaker()
{
parentBoneNameField = typeof(SkeletonBone).GetField("parentName", BindingFlags.NonPublic | BindingFlags.Instance);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public override void Bake(RigDefinitionAuthoring a)
{
var e = GetEntity(TransformUsageFlags.Dynamic);
var animator = GetComponent<Animator>();
if (a.rigConfigSource == RigDefinitionAuthoring.RigConfigSource.FromAnimator)
{
Assert.IsNotNull(animator, "Rig is configured to use parameters from Unity.Animator, but no one found. Please switch to manual configuration mode, or attach Animator to the authoring GameObject.");
a.avatar = animator.avatar;
a.animationCulling = animator.cullingMode != AnimatorCullingMode.AlwaysAnimate;
a.applyRootMotion = animator.applyRootMotion;
}
DependsOn(a.avatar);
AddBuffer<AnimationToProcessComponent>(e);
AddComponent<GPURigFrameOffsetsComponent>(e);
CreateRigDefinitionFromRigAuthoring(e, a);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
InternalSkeletonBone CreateSkeletonBoneFromTransform(Transform t, string parentName)
{
var bone = new InternalSkeletonBone();
bone.boneTransform = t;
bone.name = t.name;
bone.position = t.localPosition;
bone.rotation = t.localRotation;
bone.scale = t.localScale;
bone.parentName = parentName;
return bone;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void TransformHierarchyWalk(Transform parent, List<InternalSkeletonBone> sb)
{
for (int i = 0; i < parent.childCount; ++i)
{
var c = parent.GetChild(i);
var ct = c.transform;
var bone = CreateSkeletonBoneFromTransform(ct, parent.name);
sb.Add(bone);
TransformHierarchyWalk(ct, sb);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<InternalSkeletonBone> CreateAvatarFromObjectHierarchy(GameObject root)
{
// Manually fill all bone transforms
var sb = new List<InternalSkeletonBone>();
var rootBone = CreateSkeletonBoneFromTransform(root.transform, "");
sb.Add(rootBone);
TransformHierarchyWalk(root.transform, sb);
return sb;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int GetRigRootBoneIndex(Avatar avatar, List<InternalSkeletonBone> rigBones)
{
if (avatar == null)
return 0;
var rootBoneName = avatar.GetRootMotionNodeName();
if (avatar.isHuman)
{
var hd = avatar.humanDescription;
var humanBoneIndexInDesc = Array.FindIndex(hd.human, x => x.humanName == "Hips");
rootBoneName = hd.human[humanBoneIndexInDesc].boneName;
}
var rv = rigBones.FindIndex(x => x.name == rootBoneName);
return math.max(rv, 0);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int DeepestHierarchyBoneCount(in BlobBuilderArray<RigBoneInfo> rigBones)
{
var rv = 0;
for (var i = 0; i < rigBones.Length; ++i)
{
var numBonesInHierarchy = 1;
var curBoneIndex = rigBones[i].parentBoneIndex;
while (curBoneIndex >= 0)
{
curBoneIndex = rigBones[curBoneIndex].parentBoneIndex;
++numBonesInHierarchy;
}
rv = math.max(rv, numBonesInHierarchy);
}
return rv;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool IsBoneInOptimizationMask(string skeletonBonesPath, AvatarMask mask)
{
if (mask == null)
return true;
for (var i = 0; i < mask.transformCount; ++i)
{
var maskPath = mask.GetTransformPath(i);
var pathActive = mask.GetTransformActive(i);
if (maskPath == skeletonBonesPath)
return pathActive;
}
return true;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
string[] MakeSkeletonBonesFullPaths(SkeletonBone[] skeletonBones)
{
var rv = new string[skeletonBones.Length];
if (skeletonBones.Length == 0)
return rv;
// First bone need to be empty, because it contains root transform name, but avatar masks have it empty string
rv[0] = "";
for (var i = 1; i < skeletonBones.Length; ++i)
{
var sb = skeletonBones[i];
var parentName = (string)parentBoneNameField.GetValue(sb);
var parentIndex = Array.FindIndex(skeletonBones, 0, i, x => x.name == parentName);
var fullName = sb.name;
if (parentIndex > 0)
fullName = rv[parentIndex] + "/" + sb.name;
rv[i] = fullName;
}
return rv;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<InternalSkeletonBone> CreateInternalRigRepresentation(Avatar avatar, RigDefinitionAuthoring rigDef)
{
if (avatar == null)
{
return CreateAvatarFromObjectHierarchy(rigDef.gameObject);
}
var skeleton = avatar.humanDescription.skeleton;
// Validate avatar optimization mask
var rigOptimizationMask = rigDef.avatarOptimizationMask;
if (rigOptimizationMask != null)
{
if (rigOptimizationMask.transformCount != skeleton.Length)
{
Debug.LogWarning($"'{rigOptimizationMask.name}' bone count ({rigOptimizationMask.transformCount}) does not match rig avatar '{avatar.name}' bone count ({skeleton.Length}). Avatar mask was created for different avatar and ignored.");
rigOptimizationMask = null;
}
}
var skeletonBonesFullPaths = MakeSkeletonBonesFullPaths(skeleton);
var rv = new List<InternalSkeletonBone>();
for (var i = 0; i < skeleton.Length; ++i)
{
var boneIsObjectRoot = i == 0;
var sb = skeleton[i];
if (!IsBoneInOptimizationMask(skeletonBonesFullPaths[i], rigOptimizationMask))
continue;
var isb = new InternalSkeletonBone()
{
boneTransform = boneIsObjectRoot ? rigDef.transform : TransformUtils.FindChildRecursively(rigDef.transform, sb.name),
name = sb.name,
position = sb.position,
rotation = sb.rotation,
scale = sb.scale,
parentName = (string)parentBoneNameField.GetValue(sb)
};
rv.Add(isb);
}
return rv;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void CreateRigDefinitionFromRigAuthoring(Entity rigEntity, RigDefinitionAuthoring rigDef)
{
var avatar = rigDef.avatar;
var skeletonBones = CreateInternalRigRepresentation(avatar, rigDef);
if (skeletonBones.Count == 0)
{
Debug.LogError($"Unity avatar '{avatar.name}' setup is incorrect. Follow <a href=\"https://docs.rukhanka.com/getting_started#rig-definition\">documentation</a> about avatar setup process please.");
return;
}
var rv = new RigDefinitionComponent();
rv.applyRootMotion = rigDef.applyRootMotion;
var rigBlobHash = rigDef.CalculateRigHash();
var rigBlobExist = TryGetBlobAssetReference<RigDefinitionBlob>(rigBlobHash, out var rigBlob);
if (!rigBlobExist)
{
rigBlob = CreateRigBlob(avatar, rigDef, skeletonBones, rigBlobHash);
AddBlobAssetWithCustomHash(ref rigBlob, rigBlobHash);
}
rv.rigBlob = rigBlob;
AddComponent(rigEntity, rv);
AddBuffer<RootMotionAnimationStateComponent>(rigEntity);
var rmvc = new RootMotionVelocityComponent()
{
worldVelocity = float3.zero,
deltaPos = float3.zero,
deltaRot = quaternion.identity,
removeBuiltinEntityMovement = rigDef.rootMotionMode == RigDefinitionAuthoring.RootMotionMode.DisableBuiltinMovement
};
AddComponent(rigEntity, rmvc);
if (rigDef.hasAnimationEvents)
{
AddBuffer<AnimationEventComponent>(rigEntity);
AddBuffer<PreviousProcessedAnimationComponent>(rigEntity);
}
if (rigDef.animationCulling)
{
AddComponent<CullAnimationsTag>(rigEntity);
}
var isGPUAnimator = rigDef.animationEngine == RigDefinitionAuthoring.AnimationEngine.GPU;
AddComponent<GPUAnimationEngineTag>(rigEntity);
SetComponentEnabled<GPUAnimationEngineTag>(rigEntity, isGPUAnimator);
ProcessBoneStrippingMask(rigEntity, rigDef, skeletonBones);
CreateBoneEntityRefs(rigEntity, skeletonBones, rigDef);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void CreateBoneEntityRefs(Entity e, List<InternalSkeletonBone> skeletonBones, RigDefinitionAuthoring rigDef)
{
var transformFlags = TransformUsageFlags.Dynamic;
var manualBoneStripping = rigDef.boneEntityStrippingMode == RigDefinitionAuthoring.BoneEntityStrippingMode.Manual;
var boneEntityRefArr = AddBuffer<BoneEntityRef>(e);
for (var i = 0; i < skeletonBones.Count; ++i)
{
var boneTransformFlags = transformFlags | (manualBoneStripping && i != 0 ? TransformUsageFlags.WorldSpace : 0);
var skeletonBone = skeletonBones[i];
var boneEntity = GetEntityForBone(skeletonBone.boneTransform, boneTransformFlags, rigDef);
boneEntityRefArr.Add(new BoneEntityRef() {boneEntity = boneEntity, rigBoneIndex = i});
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void CheckForDuplicatedBones(ref RigDefinitionBlob rdb)
{
var duplicateBoneChecker = new NativeHashSet<uint>(rdb.bones.Length, Allocator.Temp);
for (var i = 0; i < rdb.bones.Length; ++i)
{
ref var bone = ref rdb.bones[i];
if (!duplicateBoneChecker.Add(bone.hash))
{
#if RUKHANKA_DEBUG_INFO
Debug.LogError($"RigDefinitionBaker: Duplicate bone with name '{bone.name.ToString()}' in rig '{rdb.name.ToString()}'! This is not allowed!");
#else
Debug.LogError($"RigDefinitionBaker: Duplicate bone with hash '{bone.hash}' in rig '{rdb.hash}'! This is not allowed! Enable 'RUKHANKA_DEBUG_INFO' to see bone names.");
#endif
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
BlobAssetReference<RigDefinitionBlob> CreateRigBlob(Avatar avatar, RigDefinitionAuthoring rigDef, List<InternalSkeletonBone> skeletonBones, Hash128 rigHash)
{
#if RUKHANKA_DEBUG_INFO
var startTimeMarker = Time.realtimeSinceStartupAsDouble;
#endif
var bb = new BlobBuilder(Allocator.Temp);
ref var c = ref bb.ConstructRoot<RigDefinitionBlob>();
c.hash = rigHash;
c.rootBoneIndex = GetRigRootBoneIndex(avatar, skeletonBones);
#if RUKHANKA_DEBUG_INFO
var rigName = rigDef.gameObject.name;
if (rigName.Length > 0)
bb.AllocateString(ref c.name, rigName);
#endif
var bonesArrayBlob = bb.Allocate(ref c.bones, skeletonBones.Count);
for (int i = 0; i < skeletonBones.Count; ++i)
{
ref var boneBlob = ref bonesArrayBlob[i];
CreateRigBoneBlob(bb, ref boneBlob, skeletonBones, i);
}
var rigIsHuman = avatar != null && avatar.isHuman;
if (rigIsHuman)
{
CreateHumanoidData(bb, ref c, bonesArrayBlob, avatar, skeletonBones);
}
#if RUKHANKA_DEBUG_INFO
var dt = Time.realtimeSinceStartupAsDouble - startTimeMarker;
c.bakingTime = (float)dt;
#endif
var rv = bb.CreateBlobAssetReference<RigDefinitionBlob>(Allocator.Persistent);
CheckForDuplicatedBones(ref rv.Value);
return rv;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void CreateHumanoidData(BlobBuilder bb, ref RigDefinitionBlob rdb, BlobBuilderArray<RigBoneInfo> bonesArr, Avatar avatar, List<InternalSkeletonBone> skeletonBones)
{
ref var hdb = ref bb.Allocate(ref rdb.humanData);
var humanToRigArr = bb.Allocate(ref hdb.humanBoneToSkeletonBoneIndices, (int)HumanBodyBones.LastBone);
var humanRotArr = bb.Allocate(ref hdb.humanRotData, skeletonBones.Count);
//var mirroredIndicesArr = bb.Allocate(ref hdb.mirroredBoneIndices, skeletonBones.Count);
for (int j = 0; j < humanToRigArr.Length; ++j)
humanToRigArr[j] = -1;
for (int l = 0; l < humanRotArr.Length; ++l)
{
ref var hrd = ref humanRotArr[l];
var humanRigIndex = CreateHumanoidBoneRotationData(ref hrd, avatar, skeletonBones[l].name);
if (humanRigIndex >= 0)
{
humanToRigArr[humanRigIndex] = l;
// Make muscle neutral ref pose
ref var rbi = ref bonesArr[l];
rbi.refPose.rot = math.mul(hrd.preRot, hrd.postRot);
// Set human body part for this bone
rbi.humanBodyPart = humanPartToAvatarMaskPartRemapTable[humanRigIndex];
}
}
//CreateHumanoidMirrorData(humanToRigArr, mirroredIndicesArr);
SetHumanBodyBodyPartForNonAssignedBones(bonesArr, skeletonBones);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
AvatarMaskBodyPart GetAvatarMaskBodyPartFromParent(int boneIndex, BlobBuilderArray<RigBoneInfo> bonesArr)
{
if (boneIndex < 0)
return (AvatarMaskBodyPart)(-1);
ref var rb = ref bonesArr[boneIndex];
if (rb.humanBodyPart >= 0)
return rb.humanBodyPart;
return GetAvatarMaskBodyPartFromParent(rb.parentBoneIndex, bonesArr);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void CreateHumanoidMirrorData(BlobBuilderArray<int> humanToRigArr, BlobBuilderArray<int> mirroredIndicesArr)
{
for (var i = 0; i < mirroredIndicesArr.Length; ++i)
mirroredIndicesArr[i] = i;
for (var i = 0; i < humanToRigArr.Length; ++i)
{
ref var v = ref humanToRigArr[i];
var mirroredPart = humanoidMirrorTable[i];
mirroredIndicesArr[v] = humanToRigArr[(int)mirroredPart];
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void SetHumanBodyBodyPartForNonAssignedBones(BlobBuilderArray<RigBoneInfo> bonesArr, List<InternalSkeletonBone> skeletonBones)
{
// Root bone is a special case
bonesArr[0].humanBodyPart = AvatarMaskBodyPart.Root;
// For other bones search for parent with body part is set and set it to the same value
for (int i = 1; i < bonesArr.Length; ++i)
{
// Override human body part if explicitly specified
var t = skeletonBones[i].boneTransform;
HumanBodyPartOverrideAuthoring hbpo = null;
if (t != null)
hbpo = t.GetComponent<HumanBodyPartOverrideAuthoring>();
var humanBodyPart = hbpo != null ? hbpo.humanBodyPart : GetAvatarMaskBodyPartFromParent(i, bonesArr);
bonesArr[i].humanBodyPart = humanBodyPart;
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int CreateHumanoidBoneRotationData(ref HumanRotationData hrd, Avatar a, string boneName)
{
hrd = HumanRotationData.Identity();
var hd = a.humanDescription;
var humanBoneInSkeletonIndex = Array.FindIndex(hd.human, x => x.boneName == boneName);
if (humanBoneInSkeletonIndex < 0)
return -1;
var humanBones = HumanTrait.BoneName;
var humanBoneDef = hd.human[humanBoneInSkeletonIndex];
var humanBoneId = Array.FindIndex(humanBones, x => x == humanBoneDef.humanName);
Debug.Assert(humanBoneId >= 0);
hrd.preRot = a.GetPreRotation(humanBoneId);
hrd.postRot = math.inverse(a.GetPostRotation(humanBoneId));
hrd.sign = a.GetLimitSign(humanBoneId);
var minA = humanBoneDef.limit.min;
var maxA = humanBoneDef.limit.max;
if (humanBoneDef.limit.useDefaultValues)
{
minA.x = HumanTrait.GetMuscleDefaultMin(HumanTrait.MuscleFromBone(humanBoneId, 0));
minA.y = HumanTrait.GetMuscleDefaultMin(HumanTrait.MuscleFromBone(humanBoneId, 1));
minA.z = HumanTrait.GetMuscleDefaultMin(HumanTrait.MuscleFromBone(humanBoneId, 2));
maxA.x = HumanTrait.GetMuscleDefaultMax(HumanTrait.MuscleFromBone(humanBoneId, 0));
maxA.y = HumanTrait.GetMuscleDefaultMax(HumanTrait.MuscleFromBone(humanBoneId, 1));
maxA.z = HumanTrait.GetMuscleDefaultMax(HumanTrait.MuscleFromBone(humanBoneId, 2));
}
hrd.minMuscleAngles = math.radians(minA);
hrd.maxMuscleAngles = math.radians(maxA);
return humanBoneId;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
unsafe Entity GetEntityForBone(Transform t, TransformUsageFlags boneFlags, RigDefinitionAuthoring rigDef)
{
// Hierarchy root should be always included
if (t == rigDef.transform)
return GetEntity(t, boneFlags);
if (t == null || t.GetComponent<SkinnedMeshRenderer>() != null)
return Entity.Null;
var automaticBoneStripping = rigDef.boneEntityStrippingMode == RigDefinitionAuthoring.BoneEntityStrippingMode.Automatic;
var rv = automaticBoneStripping ? _State.BakedEntityData->GetEntity(t) : GetEntity(t, boneFlags);
return rv;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void CreateRigBoneBlob(BlobBuilder bb, ref RigBoneInfo rbi, List<InternalSkeletonBone> skeletonBones, int boneIndex)
{
var boneIsObjectRoot = boneIndex == 0;
var skeletonBone = skeletonBones[boneIndex];
var name = skeletonBone.name;
// Special handling of hierarchy root
if (boneIsObjectRoot)
{
name = SpecialBones.UnnamedRootBoneName;
}
var boneName = new FixedStringName(name);
var boneHash = boneName.CalculateHash32();
rbi.hash = boneHash;
rbi.refPose = CreateBoneTransformFromSkeletonBone(skeletonBone);
rbi.humanBodyPart = (AvatarMaskBodyPart)(-1);
var parentBoneIndex = skeletonBones.FindIndex(x => x.name == skeletonBone.parentName);
rbi.parentBoneIndex = parentBoneIndex;
#if RUKHANKA_DEBUG_INFO
if (name.Length > 0)
bb.AllocateString(ref rbi.name, name);
#endif
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
BoneTransform CreateBoneTransformFromSkeletonBone(InternalSkeletonBone skeletonBone)
{
var pose = new BoneTransform()
{
pos = skeletonBone.position,
rot = skeletonBone.rotation,
scale = skeletonBone.scale,
};
if (skeletonBone.boneTransform != null)
{
pose = new BoneTransform()
{
pos = skeletonBone.boneTransform.localPosition,
rot = skeletonBone.boneTransform.localRotation,
scale = skeletonBone.boneTransform.localScale,
};
}
return pose;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void ProcessBoneStrippingMask(Entity rigEntity, RigDefinitionAuthoring rda, List<InternalSkeletonBone> rigBones)
{
// Bone stripping mask is processed only in "Manual" stripping mode
if (rda.boneEntityStrippingMode != RigDefinitionAuthoring.BoneEntityStrippingMode.Manual || rda.boneStrippingMask == null)
return;
var m = rda.boneStrippingMask;
var bonesToRemove = AddBuffer<BoneEntitiesToRemove>(rigEntity);
for (int i = 0; i < m.transformCount; ++i)
{
var isActive = m.GetTransformActive(i);
if (isActive) continue;
var path = m.GetTransformPath(i);
var boneIndex = 0;
for (; boneIndex < rigBones.Count && !path.EndsWith(rigBones[boneIndex].name); ++boneIndex) { }
if (boneIndex < rigBones.Count)
{
var boneEntity = GetEntity(rigBones[boneIndex].boneTransform, TransformUsageFlags.None);
bonesToRemove.Add(new BoneEntitiesToRemove() { boneEntity = boneEntity });
}
}
}
}
}