Improved animations

This commit is contained in:
Pascal Serrarens 2026-04-08 16:50:09 +02:00
parent 6fab0a35cf
commit dce7835dfc
4 changed files with 1498 additions and 216 deletions

View File

@ -23,8 +23,33 @@ namespace Passer.CreatureControl {
/// </summary>
public Quaternion targetToModelRotation;
public Animator animator;
/// <summary>
/// The maximum height of objects from the ground which do not stop the creature
/// </summary>
public float stepOffset = 0.3F;
/// <summary>
/// If there is not static object below the feet of the avatar the avatar will fall down until it reaches solid ground
/// </summary>
public bool useGravity = true;
[Range(0f, 1f)]
public float slopeAlignment = 0.3f;
/// <summary>
/// The velocity caused by gravity
/// </summary>
[HideInInspector]
protected Vector3 fallSpeed;
#region Init
protected virtual void Awake() {
CheckColliders();
}
/// <summary>
/// Ensure a target rig is available
/// </summary>
@ -115,6 +140,16 @@ namespace Passer.CreatureControl {
targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation);
}
// void OnAnimatorMove() {
// if (animator == null || creatureRigidbody == null || creatureRigidbody.isKinematic )
// return;
// Vector3 delta = animator.deltaPosition;
// Quaternion rot = animator.deltaRotation;
// creatureRigidbody.MovePosition(creatureRigidbody.position + delta);
// creatureRigidbody.MoveRotation(creatureRigidbody.rotation * rot);
// }
/// <summary>
/// Update the pose of the creature using the target rig
/// </summary>
@ -135,6 +170,158 @@ namespace Passer.CreatureControl {
this.model.SetPositionAndRotation(newPosition, newOrientation);
}
#region Collisions
[HideInInspector]
public Rigidbody creatureRigidbody;
[HideInInspector]
public CapsuleCollider bodyCollider;
[HideInInspector]
public float colliderToGround;
private void CheckColliders() {
creatureRigidbody = this.GetComponent<Rigidbody>();
if (bodyCollider != null)
return;
bodyCollider = this.GetComponent<CapsuleCollider>();
// Assuming transform.position.y is on the ground
if (bodyCollider != null) {
Vector3 centerWorld = transform.TransformPoint(bodyCollider.center);
colliderToGround = centerWorld.y - transform.position.y;
colliderToGround -= bodyCollider.direction switch {
0 => bodyCollider.radius,
1 => bodyCollider.height / 2,
_ => bodyCollider.radius
};
}
}
#endregion Collisions
#region Ground
/// <summary>
/// The ground Transform on which the pawn is standing
/// </summary>
/// When the pawn is not standing on the ground, the value is null
public Transform ground;
protected readonly float rotationSmoothTime = 0.06f;
protected void CheckGrounded() {
Vector3 footBase = this.transform.position;
float distance = GetDistanceToGround(stepOffset, out ground, out Vector3 groundNormal);
if (distance <= 0) {
// We are on or under the ground
this.fallSpeed = Vector3.zero;
if (distance <= -stepOffset)
Debug.Log($"d: {-distance}");
// Ensure that we are on the ground again
if (creatureRigidbody != null && creatureRigidbody.isKinematic == false) {
float requiredV0 = (-distance - 0.5f * Physics.gravity.y * Time.fixedDeltaTime * Time.fixedDeltaTime) / Time.fixedDeltaTime;
requiredV0 = Mathf.Clamp(requiredV0, -20, 20);
Vector3 v = creatureRigidbody.velocity;
v.y = requiredV0;
creatureRigidbody.velocity = v;
}
else
transform.Translate(0, -distance, 0, Space.World);
}
else {
// We are above the ground
ground = null;
if (useGravity)
Fall(distance);
}
if (slopeAlignment > 0f) {
Vector3 forward = Vector3.ProjectOnPlane(this.transform.forward, groundNormal).normalized;
if (forward.sqrMagnitude < 0.001f)
forward = this.transform.forward;
Quaternion targetRot = Quaternion.LookRotation(forward, groundNormal);
// blend between no-tilt and full alignment
targetRot = Quaternion.Slerp(this.transform.rotation, targetRot, slopeAlignment);
this.transform.rotation = Quaternion.Slerp(this.transform.rotation, targetRot, 1f - Mathf.Exp(-Time.deltaTime / rotationSmoothTime));
}
}
protected void Fall(float distanceToGround) {
Vector3 translation = fallSpeed * Time.deltaTime;
if (translation.magnitude > distanceToGround)
translation = Physics.gravity.normalized * distanceToGround;
transform.Translate(translation);
fallSpeed += Physics.gravity * Time.deltaTime;
}
public float GetDistanceToGround(float maxDistance, out Transform ground, out Vector3 normal) {
normal = Physics.gravity.normalized;
Vector3 rayDirection = -normal;
int layerMask = Physics.DefaultRaycastLayers;
float distance = maxDistance;
RaycastHit[] hits;
// - Why use Ray/CapsuleCast All?
// Because I need to ignore my own colliders and
// I don't want to force users to add some layer before this can be used
if (bodyCollider != null) {
Vector3 centerWorld = transform.TransformPoint(bodyCollider.center);
float half = (bodyCollider.height * 0.5f) - bodyCollider.radius;
Vector3 up = bodyCollider.direction switch {
0 => transform.TransformDirection(Vector3.right),
1 => transform.TransformDirection(Vector3.up),
_ => transform.TransformDirection(Vector3.forward),
};
Vector3 p1 = centerWorld + up * half - normal * maxDistance; // top point
Vector3 p2 = centerWorld - up * half - normal * maxDistance; // bottom point
// Debug.DrawRay(p1, 2 * maxDistance * -rayDirection, Color.magenta);
// Debug.DrawRay(p2, 2 * maxDistance * -rayDirection, Color.magenta);
hits = Physics.CapsuleCastAll(p1, p2, bodyCollider.radius, -rayDirection, maxDistance * 2, layerMask, QueryTriggerInteraction.Ignore);
maxDistance += colliderToGround;
}
else {
Vector3 rayStart = this.transform.position + normal * maxDistance;
// Debug.DrawRay(rayStart, 2 * maxDistance * rayDirection, Color.magenta);
hits = Physics.RaycastAll(rayStart, rayDirection, maxDistance * 2, layerMask, QueryTriggerInteraction.Ignore);
}
if (hits.Length == 0) {
ground = null;
return maxDistance;
}
int closestHitIx = 0;
bool foundClosest = false;
for (int i = 0; i < hits.Length; i++) {
if (hits[i].rigidbody == null &&
hits[i].distance > 0 &&
hits[i].distance <= hits[closestHitIx].distance) {
closestHitIx = i;
foundClosest = true;
}
}
if (!foundClosest) {
ground = null;
return maxDistance;
}
RaycastHit closestHit = hits[closestHitIx];
Debug.Log($"hit.distance {closestHit.distance} - {maxDistance}");
ground = closestHit.transform;
normal = closestHit.normal;
distance = closestHit.distance - maxDistance;
return distance;
}
#endregion Ground
#endregion Update
#region Scene view

View File

@ -27,13 +27,13 @@ namespace Passer.CreatureControl {
public Receptor foodReceptor;
public Receptor homeReceptor;
public Animator animator;
public Vector3 linearVelocity;
public Vector3 angularVelocity;
#region Init
protected virtual void Awake() {
protected override void Awake() {
base.Awake();
if (this.targetRig != null)
this.animator = this.targetRig.animator;
@ -100,26 +100,50 @@ namespace Passer.CreatureControl {
Vector3 localForce = nanoBrain.brain.defaultOutput.outputValue;
this.linearVelocity = (1 - inertia) * (Time.deltaTime * localForce.normalized) + inertia * this.linearVelocity;
this.linearVelocity = this.linearVelocity.normalized * 0.2f;
this.linearVelocity = this.linearVelocity.normalized; // * this.forwardSpeed;
this.animator.SetFloat("Forward", this.forwardSpeed); //this.linearVelocity.z * this.forwardSpeed);
//this.animator.SetFloat("Forward", this.forwardSpeed); //this.linearVelocity.z * this.forwardSpeed);
float forwardParam = Mathf.Clamp01(this.linearVelocity.z); // / this.forwardSpeed);
float angleDeg = 0;
if (this.linearVelocity.magnitude > 1e-5f)
angleDeg = Mathf.Atan2(this.linearVelocity.x, this.linearVelocity.z) * Mathf.Rad2Deg;
// base turn in -1..1
float baseTurn = Mathf.Clamp(angleDeg / 45, -1f, 1f);
float turnParam = baseTurn; // * Mathf.Max(0.6f, 1 - forwardParam);
// Rotate towards the movement direction
if (this.linearVelocity != Vector3.zero) {
Quaternion targetRotation = Quaternion.LookRotation(this.linearVelocity);
Quaternion worldRotation = transform.rotation * targetRotation;
Quaternion deltaRotation = worldRotation * Quaternion.Inverse(transform.rotation);
// if (this.linearVelocity != Vector3.zero) {
// Quaternion targetRotation = Quaternion.LookRotation(this.linearVelocity);
// Quaternion worldRotation = transform.rotation * targetRotation;
// Quaternion deltaRotation = worldRotation * Quaternion.Inverse(transform.rotation);
Vector3 eulerAngleChange = deltaRotation.eulerAngles;
// Normalize the Euler angles to avoid unexpected jumps due to 360-degree rotations
eulerAngleChange = new Vector3(
LinearAlgebra.Angles.Normalize(eulerAngleChange.x),
LinearAlgebra.Angles.Normalize(eulerAngleChange.y),
LinearAlgebra.Angles.Normalize(eulerAngleChange.z)
);
// Vector3 eulerAngleChange = deltaRotation.eulerAngles;
// // Normalize the Euler angles to avoid unexpected jumps due to 360-degree rotations
// eulerAngleChange = new Vector3(
// LinearAlgebra.Angles.Normalize(eulerAngleChange.x),
// LinearAlgebra.Angles.Normalize(eulerAngleChange.y),
// LinearAlgebra.Angles.Normalize(eulerAngleChange.z)
// );
this.animator.SetFloat("Rotate", eulerAngleChange.y / 45 * this.rotationSpeed);
// float rotSpeed = (eulerAngleChange.y / 45) * this.rotationSpeed;
// this.animator.SetFloat("Rotate", rotSpeed);
// Debug.Log($"fw {this.forwardSpeed} ang {rotSpeed}");
// }
// Smooth against current animator values
// float curF = animator.GetFloat("Forward");
// float curT = animator.GetFloat("Turn");
// forwardParam = Mathf.Lerp(curF, forwardParam, 1f - Mathf.Exp(-10 * Time.deltaTime));
// turnParam = Mathf.Lerp(curT, turnParam, 1f - Mathf.Exp(-10 * Time.deltaTime));
this.animator.SetFloat("Forward", forwardParam);
this.animator.SetFloat("Rotate", turnParam);
}
public virtual void FixedUpdate() {
CheckGrounded();
}
public float beatInterval = 3;

View File

@ -25,7 +25,7 @@ BlendTree:
m_DirectBlendParameter: Forward
m_Mirror: 0
- serializedVersion: 2
m_Motion: {fileID: 0}
m_Motion: {fileID: 7400000, guid: ab82ff68e62ea3b1c8e6523f8d46c142, type: 2}
m_Threshold: 0.6666667
m_Position: {x: -1, y: 0}
m_TimeScale: 1
@ -33,7 +33,7 @@ BlendTree:
m_DirectBlendParameter: Forward
m_Mirror: 0
- serializedVersion: 2
m_Motion: {fileID: 0}
m_Motion: {fileID: 7400000, guid: 91229db5e929c379bbfd5bf417848488, type: 2}
m_Threshold: 1
m_Position: {x: 1, y: 0}
m_TimeScale: 1

File diff suppressed because it is too large Load Diff