mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-18 12:20:06 +08:00
c454bc1ed8
* Add integration test for blendshape sync * fix: blendshape sync not being processed This change refactors AnimationDatabase to be part of the same extension context as the TrackObjectRenames functionality (which is renamed back to PathMappings). This then allows us to sequence deactivation of this context to come after blendshape processing completes. Fixes: #461
280 lines
9.8 KiB
C#
280 lines
9.8 KiB
C#
/*
|
|
* MIT License
|
|
*
|
|
* Copyright (c) 2022 bd_
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using JetBrains.Annotations;
|
|
using nadena.dev.modular_avatar.animation;
|
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
|
using UnityEngine;
|
|
|
|
namespace nadena.dev.modular_avatar.core.editor
|
|
{
|
|
internal class BoneDatabase
|
|
{
|
|
private Dictionary<Transform, bool> m_IsRetargetable = new Dictionary<Transform, bool>();
|
|
|
|
internal void ResetBones()
|
|
{
|
|
m_IsRetargetable.Clear();
|
|
}
|
|
|
|
internal bool IsRetargetable(Transform t)
|
|
{
|
|
return m_IsRetargetable.TryGetValue(t, out var result) && result;
|
|
}
|
|
|
|
internal void AddMergedBone(Transform bone)
|
|
{
|
|
m_IsRetargetable[bone] = true;
|
|
}
|
|
|
|
internal void RetainMergedBone(Transform bone)
|
|
{
|
|
if (bone == null) return;
|
|
if (m_IsRetargetable.ContainsKey(bone)) m_IsRetargetable[bone] = false;
|
|
}
|
|
|
|
internal Transform GetRetargetedBone(Transform bone)
|
|
{
|
|
if (bone == null || !m_IsRetargetable.ContainsKey(bone)) return null;
|
|
|
|
while (bone != null && m_IsRetargetable.ContainsKey(bone) && m_IsRetargetable[bone]) bone = bone.parent;
|
|
|
|
if (m_IsRetargetable.ContainsKey(bone)) return null;
|
|
return bone;
|
|
}
|
|
|
|
internal IEnumerable<KeyValuePair<Transform, Transform>> GetRetargetedBones()
|
|
{
|
|
return m_IsRetargetable.Where((kvp) => kvp.Value)
|
|
.Select(kvp => new KeyValuePair<Transform, Transform>(kvp.Key, GetRetargetedBone(kvp.Key)))
|
|
.Where(kvp => kvp.Value != null);
|
|
}
|
|
|
|
public Transform GetRetargetedBone(Transform bone, bool fallbackToOriginal)
|
|
{
|
|
Transform retargeted = GetRetargetedBone(bone);
|
|
|
|
return retargeted ? retargeted : (fallbackToOriginal ? bone : null);
|
|
}
|
|
}
|
|
|
|
internal class RetargetMeshes
|
|
{
|
|
private BoneDatabase _boneDatabase;
|
|
private PathMappings _pathTracker;
|
|
|
|
internal void OnPreprocessAvatar(GameObject avatarGameObject, BoneDatabase boneDatabase,
|
|
PathMappings pathMappings)
|
|
{
|
|
this._boneDatabase = boneDatabase;
|
|
this._pathTracker = pathMappings;
|
|
|
|
foreach (var renderer in avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
|
{
|
|
BuildReport.ReportingObject(renderer, () =>
|
|
{
|
|
bool isRetargetable = false;
|
|
foreach (var bone in renderer.bones)
|
|
{
|
|
if (_boneDatabase.GetRetargetedBone(bone) != null)
|
|
{
|
|
isRetargetable = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
isRetargetable |= _boneDatabase.GetRetargetedBone(renderer.rootBone);
|
|
|
|
if (isRetargetable)
|
|
{
|
|
new MeshRetargeter(renderer, _boneDatabase).Retarget();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Now remove retargeted bones
|
|
if (true)
|
|
{
|
|
foreach (var bonePair in _boneDatabase.GetRetargetedBones())
|
|
{
|
|
if (_boneDatabase.GetRetargetedBone(bonePair.Key) == null) continue;
|
|
|
|
var sourceBone = bonePair.Key;
|
|
var destBone = bonePair.Value;
|
|
|
|
// Check that we don't have any components left over (e.g. Unity colliders) that need to stick
|
|
// around.
|
|
var components = sourceBone.GetComponents<Component>();
|
|
bool has_unknown_component = false;
|
|
foreach (var component in components)
|
|
{
|
|
if (component is Transform || component is AvatarTagComponent)
|
|
{
|
|
continue; // we assume MA components are okay to purge by this point
|
|
}
|
|
|
|
has_unknown_component = true;
|
|
break;
|
|
}
|
|
|
|
if (has_unknown_component) continue;
|
|
|
|
var children = new List<Transform>();
|
|
foreach (Transform child in sourceBone)
|
|
{
|
|
children.Add(child);
|
|
}
|
|
|
|
foreach (Transform child in children)
|
|
{
|
|
child.SetParent(destBone, true);
|
|
}
|
|
|
|
_pathTracker.MarkRemoved(sourceBone.gameObject);
|
|
UnityEngine.Object.DestroyImmediate(sourceBone.gameObject);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This class processes a given mesh, adjusting the bind poses for any bones that are to be merged to instead match
|
|
* the bind pose of the original avatar's bone.
|
|
*/
|
|
internal class MeshRetargeter
|
|
{
|
|
private readonly SkinnedMeshRenderer renderer;
|
|
private readonly BoneDatabase _boneDatabase;
|
|
|
|
[CanBeNull] private Mesh src, dst;
|
|
|
|
public MeshRetargeter(SkinnedMeshRenderer renderer, BoneDatabase boneDatabase)
|
|
{
|
|
this.renderer = renderer;
|
|
this._boneDatabase = boneDatabase;
|
|
}
|
|
|
|
[CanBeNull]
|
|
public Mesh Retarget()
|
|
{
|
|
var avatar = RuntimeUtil.FindAvatarInParents(renderer.transform);
|
|
if (avatar == null) throw new System.Exception("Could not find avatar in parents of " + renderer.name);
|
|
var avatarTransform = avatar.transform;
|
|
|
|
var avPos = avatarTransform.position;
|
|
var avRot = avatarTransform.rotation;
|
|
var avScale = avatarTransform.lossyScale;
|
|
|
|
avatarTransform.position = Vector3.zero;
|
|
avatarTransform.rotation = Quaternion.identity;
|
|
avatarTransform.localScale = Vector3.one;
|
|
|
|
src = renderer.sharedMesh;
|
|
if (src != null)
|
|
{
|
|
dst = Mesh.Instantiate(src);
|
|
dst.name = "RETARGETED__" + src.name;
|
|
}
|
|
|
|
RetargetBones();
|
|
AdjustShapeKeys();
|
|
|
|
avatarTransform.position = avPos;
|
|
avatarTransform.rotation = avRot;
|
|
avatarTransform.localScale = avScale;
|
|
|
|
return dst;
|
|
}
|
|
|
|
private void AdjustShapeKeys()
|
|
{
|
|
// TODO
|
|
}
|
|
|
|
private void RetargetBones()
|
|
{
|
|
var originalBindPoses = src ? src.bindposes : null;
|
|
var originalBones = renderer.bones;
|
|
|
|
var newBones = (Transform[]) originalBones.Clone();
|
|
var newBindPoses = (Matrix4x4[]) originalBindPoses?.Clone();
|
|
|
|
for (int i = 0; i < originalBones.Length; i++)
|
|
{
|
|
Transform newBindTarget = _boneDatabase.GetRetargetedBone(originalBones[i]);
|
|
if (newBindTarget == null) continue;
|
|
newBones[i] = newBindTarget;
|
|
|
|
if (originalBindPoses != null)
|
|
{
|
|
Matrix4x4 Bp = newBindTarget.worldToLocalMatrix * originalBones[i].localToWorldMatrix *
|
|
originalBindPoses[i];
|
|
|
|
newBindPoses[i] = Bp;
|
|
}
|
|
}
|
|
|
|
var rootBone = renderer.rootBone;
|
|
var scaleBone = rootBone;
|
|
if (rootBone == null)
|
|
{
|
|
// Sometimes meshes have no root bone set. This is usually not ideal, but let's make sure we don't
|
|
// choke on the scale computation below.
|
|
scaleBone = renderer.transform;
|
|
}
|
|
|
|
renderer.bones = newBones;
|
|
if (dst)
|
|
{
|
|
dst.bindposes = newBindPoses;
|
|
renderer.sharedMesh = dst;
|
|
}
|
|
|
|
var newRootBone = _boneDatabase.GetRetargetedBone(rootBone, true);
|
|
if (newRootBone == null)
|
|
{
|
|
newRootBone = renderer.transform;
|
|
}
|
|
|
|
var oldLossyScale = scaleBone.transform.lossyScale;
|
|
var newLossyScale = newRootBone.transform.lossyScale;
|
|
|
|
var bounds = renderer.localBounds;
|
|
bounds.extents = new Vector3(
|
|
bounds.extents.x * oldLossyScale.x / newLossyScale.x,
|
|
bounds.extents.y * oldLossyScale.y / newLossyScale.y,
|
|
bounds.extents.z * oldLossyScale.z / newLossyScale.z
|
|
);
|
|
bounds.center = newRootBone.transform.InverseTransformPoint(
|
|
scaleBone.transform.TransformPoint(bounds.center)
|
|
);
|
|
renderer.localBounds = bounds;
|
|
|
|
renderer.rootBone = newRootBone;
|
|
renderer.probeAnchor = _boneDatabase.GetRetargetedBone(renderer.probeAnchor, true);
|
|
}
|
|
}
|
|
} |