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;
using UnityEditor.SceneManagement; using UnityEditor.SceneManagement;
using UnityEngine;
namespace Passer.CreatureControl { namespace Passer.CreatureControl {
[CustomEditor(typeof(Creature), true)] [CustomEditor(typeof(Creature), true)]
public class Creature_Editor : Editor { public class Creature_Editor : Editor {
/// <summary>
/// The creature managed by this editor
/// </summary>
protected Creature creature; protected Creature creature;
#region Init #region Start
/// <summary>
/// Enable the creature editor
/// </summary>
public virtual void OnEnable() { 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; bool anythingChanged = false;
if (!IsPrefab(creature)) { if (IsPrefab(this.creature) == false) {
anythingChanged |= creature.CheckTargetRig(); // Only do this when it is not a prefab
anythingChanged |= creature.CheckModel(); 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) { if (anythingChanged) {
EditorUtility.SetDirty(creature); EditorUtility.SetDirty(this.creature);
AssetDatabase.SaveAssets(); 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) { public static bool IsPrefab(Creature creature) {
PrefabStage prefabStage = PrefabStageUtility.GetPrefabStage(creature.gameObject); PrefabStage prefabStage = PrefabStageUtility.GetPrefabStage(creature.gameObject);
return prefabStage != null; 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 { namespace Passer.CreatureControl {
public class Creature : MonoBehaviour { public class Creature : MonoBehaviour {
/// <summary>
/// The (hopefully rigged) 3D model of the creature
/// </summary>
public Transform model;
/// <summary>The target bones rig</summary> /// <summary>The target bones rig</summary>
/// The target bones rig contain the target pose of the creature /// 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 /// The creature movements will try to move the creature such that the target pose is reached
/// as closely as possible /// as closely as possible
public Transform model;
public TargetRig targetRig; public TargetRig targetRig;
/// <summary>
/// The positional different between the target rig and model root
/// </summary>
public Vector3 targetToModelTranslation; public Vector3 targetToModelTranslation;
/// <summary>
/// The rotational difference between the target rig and the model root
/// </summary>
public Quaternion targetToModelRotation; public Quaternion targetToModelRotation;
#region Init #region Init
@ -20,27 +29,32 @@ namespace Passer.CreatureControl {
/// Ensure a target rig is available /// Ensure a target rig is available
/// </summary> /// </summary>
/// <param name="targetRigResourceName">The name of the target rig resource</param> /// <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. /// The parameter is used to instantiate a new target rig when none has been found.
public bool CheckTargetRig(string targetRigResourceName) { public bool CheckTargetRig(string targetRigResourceName) {
if (this.targetRig == null) { 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
return false; 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() { public virtual bool CheckTargetRig() {
return CheckTargetRig("TargetRig"); return CheckTargetRig("TargetRig");
} }
@ -50,25 +64,30 @@ namespace Passer.CreatureControl {
/// </summary> /// </summary>
/// <returns>True when the creature rig has been updated</returns> /// <returns>True when the creature rig has been updated</returns>
public bool CheckModel() { public bool CheckModel() {
if (this.model == null) { 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
return false; 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 #endregion Init
#region Start #region Start
/// <summary>
/// Start the creature
/// </summary>
protected virtual void Start() { protected virtual void Start() {
this.CheckTargetRig(); this.CheckTargetRig();
this.CheckModel(); this.CheckModel();
@ -79,28 +98,38 @@ namespace Passer.CreatureControl {
#region Update #region Update
/// <summary>
/// Update the creature
/// </summary>
public virtual void Update() { public virtual void Update() {
if (this.targetRig == null) if (this.targetRig == null)
// Without a target rig, the creature cannot move
return; return;
UpdatePose(); UpdatePose();
// copy animator root motion to the humanoid // copy animator root motion to the creature
this.transform.SetPositionAndRotation(targetRig.transform.position, targetRig.transform.rotation); this.transform.SetPositionAndRotation(targetRig.transform.position, targetRig.transform.rotation);
// As target rig is probably a child of this.transform, // As target rig is probably a child of this.transform,
// We need to restore the position/rotation of the targetsRig. // 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() { public void UpdatePose() {
if (this.targetRig == null) if (this.targetRig == null)
return; return;
this.targetRig.PoseLimbs(); this.targetRig.Pose();
UpdateBones(); 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; Vector3 newPosition = this.targetRig.transform.position + this.targetToModelTranslation;
Quaternion newOrientation = this.targetRig.transform.rotation * this.targetToModelRotation; Quaternion newOrientation = this.targetRig.transform.rotation * this.targetToModelRotation;
this.model.SetPositionAndRotation(newPosition, newOrientation); this.model.SetPositionAndRotation(newPosition, newOrientation);
@ -108,12 +137,19 @@ namespace Passer.CreatureControl {
#endregion Update #endregion Update
#region Scene view
/// <summary>
/// Update the pose of the creature when the application is not running
/// </summary>
void OnDrawGizmos() { void OnDrawGizmos() {
// This ensures that the model is always following the target rig // This ensures that the model is always following the target rig
if (Application.isPlaying == false) { if (Application.isPlaying == false) {
this.UpdatePose(); this.UpdatePose();
} }
} }
#endregion Scene view
} }
} }

View File

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

View File

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

View File

@ -14,6 +14,19 @@ namespace Passer.CreatureControl {
public Quaternion targetToBoneFemur; public Quaternion targetToBoneFemur;
public Quaternion targetToBoneTibia; 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) { public void MatchTo(Leg leg) {
this.femurTarget.position = leg.femur.position; this.femurTarget.position = leg.femur.position;
this.tibiaTarget.position = leg.tibia.position; this.tibiaTarget.position = leg.tibia.position;
@ -21,15 +34,13 @@ namespace Passer.CreatureControl {
targetToBoneFemur = TargetRig.TargetToBoneRotation(leg.femur, leg.tibia); targetToBoneFemur = TargetRig.TargetToBoneRotation(leg.femur, leg.tibia);
targetToBoneTibia = TargetRig.TargetToBoneRotation(leg.tibia, leg.tarsus); targetToBoneTibia = TargetRig.TargetToBoneRotation(leg.tibia, leg.tarsus);
float femurLength = Vector3.Distance(this.femurTarget.position, this.tibiaTarget.position); CalculateLengths();
float tibiaLength = Vector3.Distance(this.tibiaTarget.position, this.tarsusTarget.position);
float leglength = femurLength + tibiaLength;
// Put the end-effector target for IK in a sensible place
Vector3 legDirection = (this.tarsusTarget.position - this.femurTarget.position).normalized; Vector3 legDirection = (this.tarsusTarget.position - this.femurTarget.position).normalized;
this.target.position = this.femurTarget.position + 0.7f * leglength * legDirection.normalized; Vector3 targetPosition = this.femurTarget.position + 0.7f * this.length * legDirection.normalized;
this.target.rotation = Quaternion.LookRotation(legDirection); Quaternion targetRotation = Quaternion.LookRotation(legDirection);
this.target.SetPositionAndRotation(targetPosition, targetRotation);
//this.target.SetPositionAndRotation(this.tarsusTarget.position, this.tarsusTarget.rotation);
this.target.localPosition = new(this.target.localPosition.x, 0, this.target.localPosition.z); 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 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) { public void MatchTo(Creature creature) {
bool anythingChangedDummy = false; bool anythingChangedDummy = false;
MatchTo(creature, ref anythingChangedDummy); 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) { public virtual void MatchTo(Creature creature, ref bool anythingChanged) {
Vector3 targetToModelTranslation = creature.model.position - this.transform.position; Vector3 targetToModelTranslation = creature.model.position - this.transform.position;
bool changed = targetToModelTranslation != creature.targetToModelTranslation; 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) { public static Quaternion TargetToBoneRotation(Transform bone, Transform nextBone) {
if (bone == null || nextBone == null) if (bone == null || nextBone == null)
return Quaternion.identity; return Quaternion.identity;
@ -42,24 +62,6 @@ namespace Passer.CreatureControl {
Quaternion toBoneRotation = Quaternion.Inverse(targetRotation) * bone.rotation; Quaternion toBoneRotation = Quaternion.Inverse(targetRotation) * bone.rotation;
return toBoneRotation; 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;
}
} }
} }