Bend the torso from the hips and add bow and twerk test animations
- LimbBend.anchoredAtEnd keeps the far end in place (the hips on the legs) and folds the other half, and angleAt/axis expose how far the limb has turned at a point. - PoseApplier bends the body from the hips and carries the head and arms with it: their pivots follow the bent torso and they turn with its ring. - Test animations: bow (torso only) and twerk. The gametest shoots the twerk at the top and bottom of its bounce, including from behind. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -11,6 +11,9 @@ package org.saturnclient.emotes.core.bend;
|
||||
* <p>The spine is split into {@code segments} rings, spaced evenly by arc length so the limb keeps its
|
||||
* length and each piece covers the same texture rows as before. Each ring moves its cross-section
|
||||
* rigidly, so neighbouring pieces share their vertices and the mesh stays closed however far it bends.
|
||||
*
|
||||
* <p>A limb is normally anchored at {@code start}, its pivot. The torso is anchored at {@code end}
|
||||
* instead: the hips stay on the legs and the upper half leans (see {@link #anchoredAtEnd}).
|
||||
*/
|
||||
public final class LimbBend {
|
||||
/** Samples used to measure the curve's arc length. */
|
||||
@@ -18,6 +21,8 @@ public final class LimbBend {
|
||||
|
||||
private final float start;
|
||||
private final float length;
|
||||
/** Whether the fixed end is {@code end}. Then everything is mirrored along Y around the middle. */
|
||||
private final boolean anchoredAtEnd;
|
||||
private final int segments;
|
||||
private final float angle;
|
||||
/** Unit bend direction D in the XZ plane, and the rotation axis n = Y × D. */
|
||||
@@ -37,6 +42,20 @@ public final class LimbBend {
|
||||
* @param segments number of pieces the limb is divided into
|
||||
*/
|
||||
public LimbBend(float start, float end, float angle, float axis, float sharpness, int segments) {
|
||||
this(start, end, angle, axis, sharpness, segments, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #LimbBend(float, float, float, float, float, int)}, but {@code end} stays in place and
|
||||
* the half towards {@code start} folds. Axis 0 still folds towards the front.
|
||||
*/
|
||||
public static LimbBend anchoredAtEnd(float start, float end, float angle, float axis, float sharpness,
|
||||
int segments) {
|
||||
return new LimbBend(start, end, angle, axis, sharpness, segments, true);
|
||||
}
|
||||
|
||||
private LimbBend(float start, float end, float angle, float axis, float sharpness, int segments,
|
||||
boolean anchoredAtEnd) {
|
||||
if (end <= start) {
|
||||
throw new IllegalArgumentException("end must be below start");
|
||||
}
|
||||
@@ -45,6 +64,7 @@ public final class LimbBend {
|
||||
}
|
||||
this.start = start;
|
||||
this.length = end - start;
|
||||
this.anchoredAtEnd = anchoredAtEnd;
|
||||
this.segments = segments;
|
||||
this.angle = angle;
|
||||
this.dirX = (float) Math.sin(axis);
|
||||
@@ -138,6 +158,43 @@ public final class LimbBend {
|
||||
* @param out receives the bent position as {x, y, z}
|
||||
*/
|
||||
public void bendPoint(float x, float y, float z, float[] out) {
|
||||
if (anchoredAtEnd) {
|
||||
bendPointFromStart(x, mirror(y), z, out);
|
||||
out[1] = mirror(out[1]);
|
||||
} else {
|
||||
bendPointFromStart(x, y, z, out);
|
||||
}
|
||||
}
|
||||
|
||||
/** Rotates a direction (a face normal) the same way {@link #bendPoint} rotates points at {@code atY}. */
|
||||
public void bendDirection(float atY, float nx, float ny, float nz, float[] out) {
|
||||
if (anchoredAtEnd) {
|
||||
rotate(nx, -ny, nz, angleFromStart(mirror(atY)), out);
|
||||
out[1] = -out[1];
|
||||
} else {
|
||||
rotate(nx, ny, nz, angleFromStart(atY), out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How far the limb has turned at {@code y}, as a rotation around {@link #axis()} by this angle.
|
||||
* Parts attached to the limb at {@code y} (like the head and arms on the torso) turn by this much.
|
||||
*/
|
||||
public float angleAt(float y) {
|
||||
// Mirroring along Y turns a rotation around a horizontal axis the other way.
|
||||
return anchoredAtEnd ? -angleFromStart(mirror(y)) : angleFromStart(y);
|
||||
}
|
||||
|
||||
/** The unit axis the limb turns around. It's horizontal, at right angles to the bend direction. */
|
||||
public Vec3 axis() {
|
||||
return new Vec3(axisX, 0, axisZ);
|
||||
}
|
||||
|
||||
private float mirror(float y) {
|
||||
return 2 * start + length - y;
|
||||
}
|
||||
|
||||
private void bendPointFromStart(float x, float y, float z, float[] out) {
|
||||
float along = (y - start) / length * segments;
|
||||
int ring;
|
||||
float f;
|
||||
@@ -168,11 +225,10 @@ public final class LimbBend {
|
||||
out[2] += cz;
|
||||
}
|
||||
|
||||
/** Rotates a direction (a face normal) the same way {@link #bendPoint} rotates points at {@code atY}. */
|
||||
public void bendDirection(float atY, float nx, float ny, float nz, float[] out) {
|
||||
float along = Math.max(0, Math.min(segments, (atY - start) / length * segments));
|
||||
private float angleFromStart(float y) {
|
||||
float along = Math.max(0, Math.min(segments, (y - start) / length * segments));
|
||||
int ring = Math.min((int) along, segments - 1);
|
||||
rotate(nx, ny, nz, lerp(ringAngle[ring], ringAngle[ring + 1], along - ring), out);
|
||||
return lerp(ringAngle[ring], ringAngle[ring + 1], along - ring);
|
||||
}
|
||||
|
||||
/** Rodrigues' rotation of v around the bend axis n by {@code a}. */
|
||||
|
||||
@@ -110,4 +110,66 @@ class LimbBendTest {
|
||||
arm(HALF_PI, 0).bendDirection(10, 0, 0, -1, normal);
|
||||
assertArrayEquals(new float[] { 0, -1, 0 }, normal, 1e-3f);
|
||||
}
|
||||
|
||||
/** A torso: box from y = 0 (neck) to 12 (hips), anchored at the hips. */
|
||||
private static LimbBend torso(float angle, float axis) {
|
||||
return LimbBend.anchoredAtEnd(0, 12, angle, axis, 0.5f, 12);
|
||||
}
|
||||
|
||||
@Test
|
||||
void torsoKeepsItsHipsInPlace() {
|
||||
assertArrayEquals(new float[] { 3, 12, -1 }, bend(torso(1, 0), 3, 12, -1), 1e-4f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void torsoLeansForward() {
|
||||
// Axis 0 still means forward: the neck moves to the front (-Z) and down (+Y).
|
||||
float[] neck = bend(torso(HALF_PI, 0), 0, 0, 0);
|
||||
assertTrue(neck[2] < -4, "neck should be in front, z = " + neck[2]);
|
||||
assertTrue(neck[1] > 4, "neck should drop, y = " + neck[1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void torsoKeepsItsLength() {
|
||||
LimbBend bend = torso(HALF_PI, 0);
|
||||
float total = 0;
|
||||
float[] previous = bend(bend, 0, 0, 0);
|
||||
for (int i = 1; i <= 12; i++) {
|
||||
float[] point = bend(bend, 0, i, 0);
|
||||
total += distance(previous, point);
|
||||
previous = point;
|
||||
}
|
||||
assertEquals(12, total, 0.05);
|
||||
}
|
||||
|
||||
@Test
|
||||
void attachedPartsTurnWithTheRing() {
|
||||
// The offset from a ring's center to a point on it turns by angleAt around axis.
|
||||
for (LimbBend bend : new LimbBend[] { arm(1.3f, 0.4f), torso(1.3f, 0.4f) }) {
|
||||
for (float y : new float[] { -2, 0, 4, 10, 12 }) {
|
||||
float[] center = bend(bend, 0, y, 0);
|
||||
float[] point = bend(bend, 1, y, -2);
|
||||
float[] expected = rotate(new float[] { 1, 0, -2 }, bend.axis(), bend.angleAt(y));
|
||||
assertArrayEquals(expected,
|
||||
new float[] { point[0] - center[0], point[1] - center[1], point[2] - center[2] }, 1e-3f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void torsoNormalsTurnWithTheRing() {
|
||||
// A forward lean tips the front face (-Z) of the chest towards the ground (+Y).
|
||||
float[] normal = new float[3];
|
||||
torso(HALF_PI, 0).bendDirection(0, 0, 0, -1, normal);
|
||||
assertArrayEquals(new float[] { 0, 1, 0 }, normal, 1e-3f);
|
||||
}
|
||||
|
||||
/** Rodrigues' rotation of v around the unit axis n. */
|
||||
private static float[] rotate(float[] v, Vec3 n, float angle) {
|
||||
Vec3 vec = new Vec3(v[0], v[1], v[2]);
|
||||
float cos = (float) Math.cos(angle);
|
||||
float sin = (float) Math.sin(angle);
|
||||
Vec3 r = vec.scale(cos).add(n.cross(vec).scale(sin)).add(n.scale(n.dot(vec) * (1 - cos)));
|
||||
return new float[] { r.x(), r.y(), r.z() };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user