cleanup & documentation

This commit is contained in:
Pascal Serrarens 2026-03-10 17:35:34 +01:00
parent 837c5ce807
commit fc581a0dd8
6 changed files with 137 additions and 85 deletions

View File

@ -1,51 +1,54 @@
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
namespace Passer.CreatureControl {
[CustomEditor(typeof(Creature), true)]
public class Creature_Editor : Editor {
/// <summary>
/// The creature managed by this editor
/// </summary>
protected Creature creature;
#region Init
#region Start
/// <summary>
/// Enable the creature editor
/// </summary>
public virtual void OnEnable() {
creature = target as Creature;
this.creature = target as Creature;
// Keep track if anything changed while enabling the creature editor
bool anythingChanged = false;
if (!IsPrefab(creature)) {
anythingChanged |= creature.CheckTargetRig();
anythingChanged |= creature.CheckModel();
if (IsPrefab(this.creature) == false) {
// Only do this when it is not a prefab
anythingChanged |= this.creature.CheckTargetRig();
anythingChanged |= this.creature.CheckModel();
}
creature.targetRig.MatchTo(creature, ref anythingChanged);
this.creature.targetRig.MatchTo(this.creature, ref anythingChanged);
// As the above functions do not use the serialized object
// We need to manually persist the changes.
if (anythingChanged) {
EditorUtility.SetDirty(creature);
EditorUtility.SetDirty(this.creature);
AssetDatabase.SaveAssets();
}
}
/// <summary>
/// Check if the given creature is a prefab
/// </summary>
/// <param name="creature">The creature to check</param>
/// <returns>True when it is a prefab</returns>
public static bool IsPrefab(Creature creature) {
PrefabStage prefabStage = PrefabStageUtility.GetPrefabStage(creature.gameObject);
return prefabStage != null;
}
#endregion Init
#endregion Start
#region Scene
public virtual void OnSceneGUI() {
if (Application.isPlaying || creature.enabled == false)
return;
creature.UpdatePose();
}
#endregion Scene
}
}

View File

@ -3,15 +3,24 @@ using UnityEngine;
namespace Passer.CreatureControl {
public class Creature : MonoBehaviour {
/// <summary>
/// The (hopefully rigged) 3D model of the creature
/// </summary>
public Transform model;
/// <summary>The target bones rig</summary>
/// The target bones rig contain the target pose of the creature
/// The creature movements will try to move the creature such that the target pose is reached
/// as closely as possible
public Transform model;
public TargetRig targetRig;
/// <summary>
/// The positional different between the target rig and model root
/// </summary>
public Vector3 targetToModelTranslation;
/// <summary>
/// The rotational difference between the target rig and the model root
/// </summary>
public Quaternion targetToModelRotation;
#region Init
@ -20,27 +29,32 @@ namespace Passer.CreatureControl {
/// Ensure a target rig is available
/// </summary>
/// <param name="targetRigResourceName">The name of the target rig resource</param>
/// <returns>True when the creature rig has been updated</returns>
/// <returns>True when the target rig has been updated</returns>
/// The parameter is used to instantiate a new target rig when none has been found.
public bool CheckTargetRig(string targetRigResourceName) {
if (this.targetRig == null) {
// See if there is a target rig, but we just haven't found it
this.targetRig = this.GetComponentInChildren<InsectRig>();
if (this.targetRig == null) {
GameObject targetsRigPrefab = Resources.Load<GameObject>(targetRigResourceName);
GameObject targetRig = Instantiate(targetsRigPrefab);
targetRig.name = "Target Rig";
targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation);
targetRig.transform.SetParent(this.transform);
this.targetRig = targetRig.GetComponent<InsectRig>();
}
return true;
}
else
if (this.targetRig != null)
return false;
// See if there is a target rig, but we just haven't found it
this.targetRig = this.GetComponentInChildren<TargetRig>();
if (this.targetRig == null) {
// Nope, there is no target rig, so instantiate it using the given resource name
GameObject targetsRigPrefab = Resources.Load<GameObject>(targetRigResourceName);
GameObject targetRig = Instantiate(targetsRigPrefab);
targetRig.name = "Target Rig";
targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation);
targetRig.transform.SetParent(this.transform);
this.targetRig = targetRig.GetComponent<TargetRig>();
}
return true;
}
/// <summary>
/// Ensure a target rig is available
/// </summary>
/// <returns>True when the target rig has been updated</returns>
/// This tries to instantiate the default target rig resource
public virtual bool CheckTargetRig() {
return CheckTargetRig("TargetRig");
}
@ -50,25 +64,30 @@ namespace Passer.CreatureControl {
/// </summary>
/// <returns>True when the creature rig has been updated</returns>
public bool CheckModel() {
if (this.model == null) {
SkinnedMeshRenderer[] skinnedMeshRenderers = this.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer skinnedMeshRenderer in skinnedMeshRenderers) {
Transform rendererParent = skinnedMeshRenderer.transform.parent;
if (this.model == null || this.model == rendererParent)
this.model = rendererParent;
else
Debug.LogWarning("Unclear model root");
}
return this.model != null;
}
else
if (this.model != null)
return false;
// We determine the model root as the parent of the renderers
SkinnedMeshRenderer[] skinnedMeshRenderers = this.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer skinnedMeshRenderer in skinnedMeshRenderers) {
Transform rendererParent = skinnedMeshRenderer.transform.parent;
if (this.model == null || this.model == rendererParent)
this.model = rendererParent;
else
// Oops! There are multiple renders with different parents....
Debug.LogWarning("Unclear model root");
// We still return a model root, but this may not be the correct one...
}
return this.model != null;
}
#endregion Init
#region Start
/// <summary>
/// Start the creature
/// </summary>
protected virtual void Start() {
this.CheckTargetRig();
this.CheckModel();
@ -79,28 +98,38 @@ namespace Passer.CreatureControl {
#region Update
/// <summary>
/// Update the creature
/// </summary>
public virtual void Update() {
if (this.targetRig == null)
// Without a target rig, the creature cannot move
return;
UpdatePose();
// copy animator root motion to the humanoid
// copy animator root motion to the creature
this.transform.SetPositionAndRotation(targetRig.transform.position, targetRig.transform.rotation);
// As target rig is probably a child of this.transform,
// We need to restore the position/rotation of the targetsRig.
targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation);
targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation);
}
/// <summary>
/// Update the pose of the creature using the target rig
/// </summary>
public void UpdatePose() {
if (this.targetRig == null)
return;
this.targetRig.PoseLimbs();
UpdateBones();
this.targetRig.Pose();
UpdateModel();
}
public virtual void UpdateBones() {
/// <summary>
/// Update the bones of the creature's rig from the target rig pose
/// </summary>
public virtual void UpdateModel() {
Vector3 newPosition = this.targetRig.transform.position + this.targetToModelTranslation;
Quaternion newOrientation = this.targetRig.transform.rotation * this.targetToModelRotation;
this.model.SetPositionAndRotation(newPosition, newOrientation);
@ -108,12 +137,19 @@ namespace Passer.CreatureControl {
#endregion Update
#region Scene view
/// <summary>
/// Update the pose of the creature when the application is not running
/// </summary>
void OnDrawGizmos() {
// This ensures that the model is always following the target rig
if (Application.isPlaying == false) {
this.UpdatePose();
}
}
#endregion Scene view
}
}

View File

@ -31,8 +31,8 @@ namespace Passer.CreatureControl {
#region Update
public override void UpdateBones() {
base.UpdateBones();
public override void UpdateModel() {
base.UpdateModel();
if (this.insectRig.leftFrontLeg != null)
this.insectRig.leftFrontLeg.UpdateBones(this.leftFrontLeg);

View File

@ -12,7 +12,7 @@ namespace Passer.CreatureControl {
public TargetLeg rightMiddleLeg;
public TargetLeg rightBackLeg;
public override void PoseLimbs() {
public override void Pose() {
this.leftBackLeg.PoseLimb();
this.leftMiddleLeg.PoseLimb();
this.leftFrontLeg.PoseLimb();

View File

@ -14,6 +14,19 @@ namespace Passer.CreatureControl {
public Quaternion targetToBoneFemur;
public Quaternion targetToBoneTibia;
public float femurLength;
public float tibiaLength;
public float length;
/// <summary>
/// Update the lenghts of the leg bones
/// </summary>
private void CalculateLengths() {
this.femurLength = Vector3.Distance(this.femurTarget.position, this.tibiaTarget.position);
this.tibiaLength = Vector3.Distance(this.tibiaTarget.position, this.tarsusTarget.position);
this.length = femurLength + tibiaLength;
}
public void MatchTo(Leg leg) {
this.femurTarget.position = leg.femur.position;
this.tibiaTarget.position = leg.tibia.position;
@ -21,15 +34,13 @@ namespace Passer.CreatureControl {
targetToBoneFemur = TargetRig.TargetToBoneRotation(leg.femur, leg.tibia);
targetToBoneTibia = TargetRig.TargetToBoneRotation(leg.tibia, leg.tarsus);
float femurLength = Vector3.Distance(this.femurTarget.position, this.tibiaTarget.position);
float tibiaLength = Vector3.Distance(this.tibiaTarget.position, this.tarsusTarget.position);
float leglength = femurLength + tibiaLength;
CalculateLengths();
// Put the end-effector target for IK in a sensible place
Vector3 legDirection = (this.tarsusTarget.position - this.femurTarget.position).normalized;
this.target.position = this.femurTarget.position + 0.7f * leglength * legDirection.normalized;
this.target.rotation = Quaternion.LookRotation(legDirection);
//this.target.SetPositionAndRotation(this.tarsusTarget.position, this.tarsusTarget.rotation);
Vector3 targetPosition = this.femurTarget.position + 0.7f * this.length * legDirection.normalized;
Quaternion targetRotation = Quaternion.LookRotation(legDirection);
this.target.SetPositionAndRotation(targetPosition, targetRotation);
this.target.localPosition = new(this.target.localPosition.x, 0, this.target.localPosition.z);
}

View File

@ -9,14 +9,26 @@ namespace Passer.CreatureControl {
public Animator animator;
public virtual void PoseLimbs() {
/// <summary>
/// Pose the target rig using the IK targets
/// </summary>
public virtual void Pose() {
}
/// <summary>
/// Align the target rig with a creature
/// </summary>
/// <param name="creature">The creature to align to</param>
public void MatchTo(Creature creature) {
bool anythingChangedDummy = false;
MatchTo(creature, ref anythingChangedDummy);
}
/// <summary>
/// Align the target rig with a creature
/// </summary>
/// <param name="creature">The creature to align to</param>
/// <param name="anythingChanged">True when any property of the creature has changed</param>
public virtual void MatchTo(Creature creature, ref bool anythingChanged) {
Vector3 targetToModelTranslation = creature.model.position - this.transform.position;
bool changed = targetToModelTranslation != creature.targetToModelTranslation;
@ -33,6 +45,14 @@ namespace Passer.CreatureControl {
}
}
/// <summary>
/// Compute the rotation from the target bone to a creature's bone
/// </summary>
/// <param name="bone">The creature's bone for this target bone</param>
/// <param name="nextBone">The next bone in the creature's hierarchy</param>
/// <returns>The rotation from the target bone rotation to the creature bone rotation</returns>
/// The next bone is used to compute the direction of the bone.
/// The 'up'-direction of the bone is currently fixed to (world) up.
public static Quaternion TargetToBoneRotation(Transform bone, Transform nextBone) {
if (bone == null || nextBone == null)
return Quaternion.identity;
@ -42,24 +62,6 @@ namespace Passer.CreatureControl {
Quaternion toBoneRotation = Quaternion.Inverse(targetRotation) * bone.rotation;
return toBoneRotation;
}
public static Quaternion TargetToBoneRotation(Transform target, Transform nextTarget, Transform bone) {
Vector3 direction = nextTarget.position - bone.position;
Quaternion targetRotation = Quaternion.LookRotation(direction, Vector3.up);
Quaternion toBoneRotation = Quaternion.Inverse(targetRotation) * bone.rotation;
return toBoneRotation;
}
public static Quaternion TargetToBoneRotation_Debug(Transform target, Transform nextTarget, Transform bone) {
Vector3 direction = nextTarget.position - target.position;
Quaternion targetRotation = Quaternion.LookRotation(direction, Vector3.up);
Debug.DrawRay(bone.position, targetRotation * Vector3.forward, Color.blue);
Debug.DrawRay(bone.position, targetRotation * Vector3.up, Color.green);
Quaternion toBoneRotation = Quaternion.Inverse(targetRotation) * bone.rotation;
Debug.Log($"{targetRotation.eulerAngles} {bone.rotation.eulerAngles} -> {toBoneRotation.eulerAngles}");
return toBoneRotation;
}
}
}