211 lines
12 KiB
C#
211 lines
12 KiB
C#
using System;
|
||
using System.Linq;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
|
||
namespace AIGame.Review
|
||
{
|
||
/// <summary>Scene-authored, synchronized skinning comparison. No UI/model construction at runtime.</summary>
|
||
public sealed class CharacterAnimationLab : MonoBehaviour
|
||
{
|
||
[Serializable] public sealed class Actor
|
||
{
|
||
public string label;
|
||
public GameObject rig;
|
||
public Transform hips;
|
||
public Transform rightHand;
|
||
public SkinnedMeshRenderer renderer;
|
||
// Optional full target-pose walk derived from the common source
|
||
// motion. Used where preserving source angles alone crosses feet.
|
||
public AnimationClip walkingClip;
|
||
// Optional bind translation adaptation. The shared source clip's
|
||
// rotation curves and time remain unchanged across characters.
|
||
public float translationScale = 1f;
|
||
public Vector3[] translationOffsets = Array.Empty<Vector3>();
|
||
public Quaternion[] translationDeltaRotations = Array.Empty<Quaternion>();
|
||
public int[] rotationParents = Array.Empty<int>();
|
||
public Quaternion[] bindRotationCorrections = Array.Empty<Quaternion>();
|
||
[NonSerialized] public Quaternion[] sampledWorldRotations;
|
||
[NonSerialized] public Quaternion[] mappedWorldRotations;
|
||
}
|
||
public Actor[] actors;
|
||
public AnimationClip[] clips;
|
||
public Vector3[] travel;
|
||
public Camera reviewCamera;
|
||
public Transform ui;
|
||
public Material texturedMaterial;
|
||
public Material clayMaterial;
|
||
public int motion;
|
||
public bool playing = true;
|
||
public bool inPlace = true;
|
||
public bool textured = true;
|
||
public float speed = 1f;
|
||
public float time;
|
||
public int focus = -1;
|
||
public int detail;
|
||
public float yaw;
|
||
TMP_Text stateText, timeText, pauseText, travelText, materialText, detailText;
|
||
RectTransform progress;
|
||
float progressWidth;
|
||
float zoom = 1f;
|
||
Vector3 lastMouse;
|
||
bool dragging;
|
||
SkinWeights previousWeights;
|
||
bool changedWeights;
|
||
bool previousRunInBackground;
|
||
public float Duration => clips != null && clips.Length > 0 ? clips[motion].length : 1;
|
||
public int FrameCount => Mathf.RoundToInt(Duration * 30) + 1;
|
||
|
||
void OnEnable()
|
||
{
|
||
if (!Application.isPlaying) return;
|
||
previousWeights = QualitySettings.skinWeights;
|
||
previousRunInBackground = Application.runInBackground;
|
||
Application.runInBackground = true;
|
||
QualitySettings.skinWeights = SkinWeights.Unlimited;
|
||
changedWeights = true;
|
||
}
|
||
void OnDisable()
|
||
{
|
||
if (changedWeights) { QualitySettings.skinWeights = previousWeights; Application.runInBackground = previousRunInBackground; }
|
||
changedWeights = false;
|
||
}
|
||
void Awake()
|
||
{
|
||
if (!ui) return;
|
||
var buttons = ui.GetComponentsInChildren<Button>(true).ToDictionary(b => b.name);
|
||
void Bind(string n, UnityEngine.Events.UnityAction action) => buttons[n].onClick.AddListener(action);
|
||
Bind("Btn_Walk", () => SetMotion(0));
|
||
Bind("Btn_Breathe", () => SetMotion(1));
|
||
Bind("Btn_Happy", () => SetMotion(2));
|
||
Bind("Btn_Play", () => { playing = !playing; RefreshUI(); });
|
||
Bind("Btn_Previous", () => Step(-1));
|
||
Bind("Btn_Next", () => Step(1));
|
||
Bind("Btn_Restart", () => { time = 0; Sample(); });
|
||
Bind("Btn_Speed025", () => SetSpeed(.25f));
|
||
Bind("Btn_Speed05", () => SetSpeed(.5f));
|
||
Bind("Btn_Speed1", () => SetSpeed(1));
|
||
Bind("Btn_Speed2", () => SetSpeed(2));
|
||
Bind("Btn_Travel", () => { inPlace = !inPlace; Sample(); });
|
||
Bind("Btn_Front", () => SetYaw(0));
|
||
Bind("Btn_Side", () => SetYaw(90));
|
||
Bind("Btn_Back", () => SetYaw(180));
|
||
Bind("Btn_Material", () => { textured = !textured; Sample(); });
|
||
Bind("Btn_All", () => SetFocus(-1));
|
||
Bind("Btn_Focus1", () => SetFocus(0));
|
||
Bind("Btn_Focus2", () => SetFocus(1));
|
||
Bind("Btn_Focus3", () => SetFocus(2));
|
||
if (buttons.TryGetValue("Btn_Focus4", out var focus4)) focus4.onClick.AddListener(() => SetFocus(3));
|
||
Bind("Btn_Detail", () => { detail = (detail + 1) % 3; if (detail > 0 && focus < 0) focus = 1; zoom = 1; Sample(); });
|
||
TMP_Text Text(string n) => ui.GetComponentsInChildren<TMP_Text>(true).First(t => t.name == n);
|
||
stateText = Text("Txt_State"); timeText = Text("Txt_Time");
|
||
pauseText = Text("Txt_Play"); travelText = Text("Txt_Travel");
|
||
materialText = Text("Txt_Material"); detailText = Text("Txt_Detail");
|
||
progress = ui.GetComponentsInChildren<RectTransform>(true).First(t => t.name == "Img_Progress");
|
||
progressWidth = ((RectTransform)progress.parent).rect.width;
|
||
var seek = ui.GetComponentInChildren<AnimationLabSeekBar>(true);
|
||
if (seek) seek.viewer = this;
|
||
Sample();
|
||
}
|
||
void Update()
|
||
{
|
||
if (Input.GetKeyDown(KeyCode.Space)) playing = !playing;
|
||
if (Input.GetKeyDown(KeyCode.Alpha1)) SetMotion(0);
|
||
if (Input.GetKeyDown(KeyCode.Alpha2)) SetMotion(1);
|
||
if (Input.GetKeyDown(KeyCode.Alpha3)) SetMotion(2);
|
||
if (Input.GetKeyDown(KeyCode.LeftArrow)) Step(-1);
|
||
if (Input.GetKeyDown(KeyCode.RightArrow)) Step(1);
|
||
bool overUI = EventSystem.current && EventSystem.current.IsPointerOverGameObject();
|
||
if (Input.GetMouseButtonDown(0) && !overUI) { dragging = true; lastMouse = Input.mousePosition; }
|
||
if (!Input.GetMouseButton(0)) dragging = false;
|
||
if (dragging) { yaw -= (Input.mousePosition.x - lastMouse.x) * .35f; lastMouse = Input.mousePosition; }
|
||
if (!overUI && Mathf.Abs(Input.mouseScrollDelta.y) > .01f)
|
||
zoom = Mathf.Clamp(zoom * Mathf.Pow(.88f, Input.mouseScrollDelta.y), .35f, 2.5f);
|
||
if (playing) time = Mathf.Repeat(time + Time.unscaledDeltaTime * speed, Mathf.Max(.001f, Duration));
|
||
Sample();
|
||
}
|
||
public void SetMotion(int value) { motion = Mathf.Clamp(value, 0, clips.Length - 1); time = 0; Sample(); }
|
||
public void SetSpeed(float value) { speed = value; RefreshUI(); }
|
||
public void SetYaw(float value) { yaw = value; Sample(); }
|
||
public void SetFocus(int value) { focus = value; if (value < 0) detail = 0; zoom = 1; Sample(); }
|
||
public void Seek(float value) { playing = false; time = Mathf.Clamp01(value) * Duration; Sample(); }
|
||
public void Step(int frames) { playing = false; time = Mathf.Clamp(Mathf.Round(time * 30) + frames, 0, FrameCount - 1) / 30f; Sample(); }
|
||
public void Sample()
|
||
{
|
||
if (actors == null || clips == null || clips.Length == 0) return;
|
||
time = Mathf.Clamp(time, 0, Duration);
|
||
float phase = Duration > 0 ? time / Duration : 0;
|
||
foreach (var actor in actors)
|
||
{
|
||
actor.renderer.enabled = focus < 0 || actor == actors[focus];
|
||
bool mappedWalk = motion == 0 && actor.walkingClip;
|
||
(mappedWalk ? actor.walkingClip : clips[motion]).SampleAnimation(actor.rig, time);
|
||
bool adapt = !mappedWalk && actor.translationOffsets != null && actor.translationOffsets.Length == actor.renderer.bones.Length;
|
||
if (adapt)
|
||
{
|
||
var bones = actor.renderer.bones;
|
||
for (int i = 0; i < bones.Length; i++)
|
||
{
|
||
var delta = bones[i].localPosition * actor.translationScale;
|
||
if (actor.translationDeltaRotations != null && actor.translationDeltaRotations.Length == bones.Length)
|
||
delta = actor.translationDeltaRotations[i] * delta;
|
||
bones[i].localPosition = delta + actor.translationOffsets[i];
|
||
}
|
||
if (actor.bindRotationCorrections != null && actor.bindRotationCorrections.Length == bones.Length && actor.rotationParents.Length == bones.Length)
|
||
{
|
||
if (actor.sampledWorldRotations == null || actor.sampledWorldRotations.Length != bones.Length)
|
||
{
|
||
actor.sampledWorldRotations = new Quaternion[bones.Length];
|
||
actor.mappedWorldRotations = new Quaternion[bones.Length];
|
||
}
|
||
for (int i = 0; i < bones.Length; i++)
|
||
{
|
||
int parent = actor.rotationParents[i];
|
||
actor.sampledWorldRotations[i] = parent < 0 ? bones[i].localRotation : actor.sampledWorldRotations[parent] * bones[i].localRotation;
|
||
actor.mappedWorldRotations[i] = actor.sampledWorldRotations[i] * actor.bindRotationCorrections[i];
|
||
}
|
||
for (int i = 0; i < bones.Length; i++)
|
||
{
|
||
int parent = actor.rotationParents[i];
|
||
bones[i].localRotation = parent < 0 ? actor.mappedWorldRotations[i] : Quaternion.Inverse(actor.mappedWorldRotations[parent]) * actor.mappedWorldRotations[i];
|
||
}
|
||
}
|
||
}
|
||
if (inPlace) actor.hips.localPosition -= travel[motion] * phase * (adapt || mappedWalk ? actor.translationScale : 1f);
|
||
actor.rig.transform.localRotation = Quaternion.Euler(0, yaw, 0);
|
||
actor.renderer.sharedMaterial = textured ? texturedMaterial : clayMaterial;
|
||
}
|
||
UpdateCamera(); RefreshUI();
|
||
}
|
||
void UpdateCamera()
|
||
{
|
||
if (!reviewCamera) return;
|
||
float x = focus >= 0 ? actors[focus].rig.transform.parent.position.x : 0;
|
||
float y = detail == 1 ? 1.18f : detail == 2 ? .78f : .82f;
|
||
Vector3 target = new Vector3(x, y, 0);
|
||
if (detail == 2 && focus >= 0 && actors[focus].rightHand) target = actors[focus].rightHand.position;
|
||
reviewCamera.transform.position = target + new Vector3(0, .12f, -6);
|
||
reviewCamera.transform.LookAt(target);
|
||
reviewCamera.orthographicSize = (focus < 0 ? Mathf.Max(1.15f, (actors.Length >= 4 ? 1.8f : 1.65f) / Mathf.Max(.1f, reviewCamera.aspect)) : detail == 1 ? .62f : detail == 2 ? .37f : 1.08f) * zoom;
|
||
}
|
||
void RefreshUI()
|
||
{
|
||
if (!stateText) return;
|
||
string[] motions = { "行走", "呼吸待机", "轻松待机" };
|
||
stateText.text = motions[motion] + " · " + (focus < 0 ? (actors.Length == 4 ? "四版同步对比" : "三版同步对比") : actors[focus].label);
|
||
timeText.text = string.Format("{0:0.00} / {1:0.00} 秒 第 {2} / {3} 帧 {4:0.##}×", time, Duration, Mathf.RoundToInt(time * 30) + 1, FrameCount, speed);
|
||
pauseText.text = playing ? "暂停" : "播放";
|
||
travelText.text = inPlace ? "原地循环" : "保留位移";
|
||
materialText.text = textured ? "切灰模" : "切贴图";
|
||
detailText.text = new[] { "全身", "上身", "手部" }[detail];
|
||
if (progress)
|
||
{
|
||
if (progressWidth <= 0) progressWidth = ((RectTransform)progress.parent).rect.width;
|
||
progress.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Mathf.Max(2, progressWidth * (time / Mathf.Max(.001f, Duration))));
|
||
}
|
||
}
|
||
}
|
||
}
|