Add PoseIconStage editor component and Pose Icon scene

Applies a pose clip plus root position/rotation to the staged Paintman
for authoring pose icons (screenshot pipeline follows later).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-12 18:48:07 +02:00
1 parent 7e9f255de5
commit 083d0b8430
4 files changed
+1131

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Authoring tool for the Pose Icon scene: holds a list of pose entries (animation clip + root
/// position/rotation) and applies the selected one to the paintman rig in edit mode, for framing
/// each pose in front of the camera (icon screenshots are a later step). Clips are sampled via
/// AnimationClip.SampleAnimation (no foot IK), matching how PlayerPoseController plays them.
/// </summary>
[ExecuteAlways]
[DisallowMultipleComponent]
public class PoseIconStage : MonoBehaviour
{
[System.Serializable]
public class Entry
{
[Tooltip("Pose clip, sampled at time 0 onto the target rig.")]
public AnimationClip clip;
[Tooltip("World position applied to the target root for this pose.")]
public Vector3 position;
[Tooltip("World rotation (euler angles) applied to the target root for this pose.")]
public Vector3 rotation;
}
[Tooltip("Rig the poses are applied to (the paintman). Defaults to the Animator in this object's children.")]
[SerializeField] private Animator target;
[Tooltip("Pose entries: clip + root position/rotation.")]
[SerializeField] private List<Entry> entries = new();
[Tooltip("Index of the entry currently applied. Change to preview another pose.")]
[Min(0)]
[SerializeField] private int currentIndex;
private bool _dirty;
// Rest-pose snapshot of the rig, taken before the first sample this session. A clip only writes
// the bones its curves cover, so switching between clips would otherwise leave residue from the
// previous pose on un-keyed bones (same reasoning as PlayerPoseController.CaptureRestPose).
// Not serialized: reload the scene to get a clean rest pose if it was saved mid-pose.
private Transform[] _restBones;
private Vector3[] _restPos;
private Quaternion[] _restRot;
void OnEnable()
{
if (!target)
target = GetComponentInChildren<Animator>();
_dirty = true;
}
void OnValidate()
{
// Only flag here; Update applies (coalesced to once per frame, safe object access).
_dirty = true;
#if UNITY_EDITOR
UnityEditor.EditorApplication.QueuePlayerLoopUpdate();
#endif
}
void Update()
{
if (!_dirty)
return;
_dirty = false;
Apply();
}
[ContextMenu("Apply Current Pose")]
private void Apply()
{
if (!target || entries.Count == 0)
return;
Entry entry = entries[Mathf.Clamp(currentIndex, 0, entries.Count - 1)];
if (entry == null || !entry.clip)
return;
EnsureRestPose();
ResetToRestPose();
entry.clip.SampleAnimation(target.gameObject, 0f);
// Root placement wins over any root curves the clip may carry.
target.transform.SetPositionAndRotation(entry.position, Quaternion.Euler(entry.rotation));
}
private void EnsureRestPose()
{
if (_restBones != null)
return;
_restBones = target.GetComponentsInChildren<Transform>(true);
int n = _restBones.Length;
_restPos = new Vector3[n];
_restRot = new Quaternion[n];
for (int i = 0; i < n; i++)
{
_restPos[i] = _restBones[i].localPosition;
_restRot[i] = _restBones[i].localRotation;
}
}
private void ResetToRestPose()
{
for (int i = 0; i < _restBones.Length; i++)
{
Transform t = _restBones[i];
if (!t)
continue;
t.localPosition = _restPos[i];
t.localRotation = _restRot[i];
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cf75c420c02c16249939d0a6f6515d01