Bend limbs along a single axis with a Bézier spine
- core: Bezier (any degree, De Casteljau), LimbBend (cubic spine through the rigid upper/joint/lower shape, one ring per texture row spaced by arc length, rigid rings so pieces share vertices) and QuadSplitter (cuts faces at the rings, interpolating UVs). - fabric: ModelPartMixin draws parts that have a bend with BentCubeRenderer, which deforms vanilla's own cubes. Overlays and armor use the limb's spine so they stay attached. Bends are reset every setupAnim because models are shared between entities. - testmod: flap and squat animations, and a client gametest that screenshots each animation from the front and side (runClientGameTest). The tester runs with the full Fabric API because single modules don't bring their version-specific dependencies. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package org.saturnclient.emotes.core.bend;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** A Bézier curve of any degree, evaluated with De Casteljau's algorithm. */
|
||||
public final class Bezier {
|
||||
private final Vec3[] points;
|
||||
|
||||
public Bezier(List<Vec3> controlPoints) {
|
||||
if (controlPoints.size() < 2) {
|
||||
throw new IllegalArgumentException("A Bézier curve needs at least two control points");
|
||||
}
|
||||
this.points = controlPoints.toArray(Vec3[]::new);
|
||||
}
|
||||
|
||||
public static Bezier of(Vec3... controlPoints) {
|
||||
return new Bezier(List.of(controlPoints));
|
||||
}
|
||||
|
||||
public int degree() {
|
||||
return points.length - 1;
|
||||
}
|
||||
|
||||
public List<Vec3> controlPoints() {
|
||||
return List.of(points);
|
||||
}
|
||||
|
||||
/** The point at {@code t} in [0, 1]. */
|
||||
public Vec3 point(float t) {
|
||||
return deCasteljau(points, t);
|
||||
}
|
||||
|
||||
/** The first derivative at {@code t}: the tangent, scaled by the curve's speed. */
|
||||
public Vec3 derivative(float t) {
|
||||
Vec3[] hodograph = new Vec3[points.length - 1];
|
||||
for (int i = 0; i < hodograph.length; i++) {
|
||||
hodograph[i] = points[i + 1].sub(points[i]).scale(degree());
|
||||
}
|
||||
return deCasteljau(hodograph, t);
|
||||
}
|
||||
|
||||
private static Vec3 deCasteljau(Vec3[] controlPoints, float t) {
|
||||
Vec3[] work = controlPoints.clone();
|
||||
for (int level = work.length - 1; level > 0; level--) {
|
||||
for (int i = 0; i < level; i++) {
|
||||
work[i] = work[i].lerp(work[i + 1], t);
|
||||
}
|
||||
}
|
||||
return work[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package org.saturnclient.emotes.core.bend;
|
||||
|
||||
/**
|
||||
* Bends one limb along a single axis, the way an elbow or knee folds.
|
||||
*
|
||||
* <p>Coordinates are the limb's model space in pixels: the limb hangs along +Y from {@code start}
|
||||
* to {@code end}, and its front faces -Z. At rest, the spine runs down the limb's center (x = z = 0).
|
||||
* When bent, the spine follows a cubic Bézier curve whose control polygon is the rigid "upper half,
|
||||
* joint, lower half" shape, so the upper and lower halves stay nearly straight and the joint curves.
|
||||
*
|
||||
* <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.
|
||||
*/
|
||||
public final class LimbBend {
|
||||
/** Samples used to measure the curve's arc length. */
|
||||
private static final int ARC_SAMPLES = 64;
|
||||
|
||||
private final float start;
|
||||
private final float length;
|
||||
private final int segments;
|
||||
private final float angle;
|
||||
/** Unit bend direction D in the XZ plane, and the rotation axis n = Y × D. */
|
||||
private final float dirX, dirZ;
|
||||
private final float axisX, axisZ;
|
||||
|
||||
/** Ring centers and their rotation around n, for rings 0..segments. */
|
||||
private final float[] ringX, ringY, ringZ, ringAngle;
|
||||
|
||||
/**
|
||||
* @param start top of the limb (its pivot end), along Y
|
||||
* @param end bottom of the limb, along Y
|
||||
* @param angle how far the lower half folds, in radians
|
||||
* @param axis direction the limb folds towards, as an angle around the limb: 0 folds to the
|
||||
* front (-Z) and PI/2 folds to +X
|
||||
* @param sharpness 0 for a long, round curve (a quadratic Bézier) up to 1 for a tight joint
|
||||
* @param segments number of pieces the limb is divided into
|
||||
*/
|
||||
public LimbBend(float start, float end, float angle, float axis, float sharpness, int segments) {
|
||||
if (end <= start) {
|
||||
throw new IllegalArgumentException("end must be below start");
|
||||
}
|
||||
if (segments < 1) {
|
||||
throw new IllegalArgumentException("segments must be at least 1");
|
||||
}
|
||||
this.start = start;
|
||||
this.length = end - start;
|
||||
this.segments = segments;
|
||||
this.angle = angle;
|
||||
this.dirX = (float) Math.sin(axis);
|
||||
this.dirZ = (float) -Math.cos(axis);
|
||||
// n = Y × D = (0, 1, 0) × (dirX, 0, dirZ)
|
||||
this.axisX = dirZ;
|
||||
this.axisZ = -dirX;
|
||||
|
||||
ringX = new float[segments + 1];
|
||||
ringY = new float[segments + 1];
|
||||
ringZ = new float[segments + 1];
|
||||
ringAngle = new float[segments + 1];
|
||||
|
||||
if (isStraight()) {
|
||||
for (int i = 0; i <= segments; i++) {
|
||||
ringY[i] = start + length * i / segments;
|
||||
}
|
||||
return;
|
||||
}
|
||||
buildRings(spine(Math.max(0, Math.min(1, sharpness))));
|
||||
}
|
||||
|
||||
/** Whether the limb is (close enough to) unbent. Straight limbs can be drawn as they are. */
|
||||
public boolean isStraight() {
|
||||
return Math.abs(angle) < 1e-4f;
|
||||
}
|
||||
|
||||
public int segments() {
|
||||
return segments;
|
||||
}
|
||||
|
||||
/** Y positions where the limb is cut into pieces, not counting its ends. */
|
||||
public float[] cuts() {
|
||||
float[] cuts = new float[segments - 1];
|
||||
for (int i = 1; i < segments; i++) {
|
||||
cuts[i - 1] = start + length * i / segments;
|
||||
}
|
||||
return cuts;
|
||||
}
|
||||
|
||||
private Bezier spine(float sharpness) {
|
||||
float half = length / 2;
|
||||
Vec3 top = new Vec3(0, start, 0);
|
||||
Vec3 joint = new Vec3(0, start + half, 0);
|
||||
Vec3 lower = new Vec3(dirX * (float) Math.sin(angle), (float) Math.cos(angle), dirZ * (float) Math.sin(angle));
|
||||
Vec3 bottom = joint.add(lower.scale(half));
|
||||
// At sharpness 0 this is the quadratic (top, joint, bottom) raised to a cubic.
|
||||
float pull = 2f / 3 + sharpness / 3;
|
||||
return Bezier.of(top, top.lerp(joint, pull), bottom.lerp(joint, pull), bottom);
|
||||
}
|
||||
|
||||
private void buildRings(Bezier curve) {
|
||||
// Arc length table, so rings can be spaced evenly along the curve.
|
||||
float[] ts = new float[ARC_SAMPLES + 1];
|
||||
float[] lengths = new float[ARC_SAMPLES + 1];
|
||||
Vec3 previous = curve.point(0);
|
||||
for (int i = 1; i <= ARC_SAMPLES; i++) {
|
||||
ts[i] = (float) i / ARC_SAMPLES;
|
||||
Vec3 point = curve.point(ts[i]);
|
||||
lengths[i] = lengths[i - 1] + point.sub(previous).length();
|
||||
previous = point;
|
||||
}
|
||||
// The curve cuts the corner, so it's shorter than the limb. Scale it about the top to fit.
|
||||
float scale = length / lengths[ARC_SAMPLES];
|
||||
Vec3 top = curve.point(0);
|
||||
Vec3 dir = new Vec3(dirX, 0, dirZ);
|
||||
|
||||
int sample = 0;
|
||||
for (int i = 0; i <= segments; i++) {
|
||||
float target = lengths[ARC_SAMPLES] * i / segments;
|
||||
while (sample < ARC_SAMPLES - 1 && lengths[sample + 1] < target) {
|
||||
sample++;
|
||||
}
|
||||
float span = lengths[sample + 1] - lengths[sample];
|
||||
float f = span > 0 ? (target - lengths[sample]) / span : 0;
|
||||
float t = ts[sample] + (ts[sample + 1] - ts[sample]) * f;
|
||||
|
||||
Vec3 center = top.add(curve.point(t).sub(top).scale(scale));
|
||||
Vec3 tangent = curve.derivative(t);
|
||||
ringX[i] = center.x();
|
||||
ringY[i] = center.y();
|
||||
ringZ[i] = center.z();
|
||||
ringAngle[i] = (float) Math.atan2(tangent.dot(dir), tangent.y());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a point from the straight limb onto the bent one. Points above or below the limb follow
|
||||
* the end rings' tangents, so overlay layers and armor that stick out past the limb stay attached.
|
||||
*
|
||||
* @param out receives the bent position as {x, y, z}
|
||||
*/
|
||||
public void bendPoint(float x, float y, float z, float[] out) {
|
||||
float along = (y - start) / length * segments;
|
||||
int ring;
|
||||
float f;
|
||||
float extra = 0;
|
||||
if (along <= 0) {
|
||||
ring = 0;
|
||||
f = 0;
|
||||
extra = y - start;
|
||||
} else if (along >= segments) {
|
||||
ring = segments - 1;
|
||||
f = 1;
|
||||
extra = y - (start + length);
|
||||
} else {
|
||||
ring = Math.min((int) along, segments - 1);
|
||||
f = along - ring;
|
||||
}
|
||||
|
||||
float cx = lerp(ringX[ring], ringX[ring + 1], f);
|
||||
float cy = lerp(ringY[ring], ringY[ring + 1], f);
|
||||
float cz = lerp(ringZ[ring], ringZ[ring + 1], f);
|
||||
float a = lerp(ringAngle[ring], ringAngle[ring + 1], f);
|
||||
|
||||
// The cross-section offset (x, extra, z) rotates with the ring. Past the ends, "extra"
|
||||
// carries on along the end tangent.
|
||||
rotate(x, extra, z, a, out);
|
||||
out[0] += cx;
|
||||
out[1] += cy;
|
||||
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));
|
||||
int ring = Math.min((int) along, segments - 1);
|
||||
rotate(nx, ny, nz, lerp(ringAngle[ring], ringAngle[ring + 1], along - ring), out);
|
||||
}
|
||||
|
||||
/** Rodrigues' rotation of v around the bend axis n by {@code a}. */
|
||||
private void rotate(float x, float y, float z, float a, float[] out) {
|
||||
float cos = (float) Math.cos(a);
|
||||
float sin = (float) Math.sin(a);
|
||||
// n = (axisX, 0, axisZ)
|
||||
float crossX = -axisZ * y;
|
||||
float crossY = axisZ * x - axisX * z;
|
||||
float crossZ = axisX * y;
|
||||
float dot = axisX * x + axisZ * z;
|
||||
out[0] = x * cos + crossX * sin + axisX * dot * (1 - cos);
|
||||
out[1] = y * cos + crossY * sin;
|
||||
out[2] = z * cos + crossZ * sin + axisZ * dot * (1 - cos);
|
||||
}
|
||||
|
||||
private static float lerp(float a, float b, float t) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.saturnclient.emotes.core.bend;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Cuts the faces of a box into horizontal strips so they can bend.
|
||||
*
|
||||
* <p>A quad is four vertices of {x, y, z, u, v}. Faces that run along Y (the sides of a limb) are cut
|
||||
* at every Y in {@code cuts}, and each strip's UVs are interpolated along the face, so the strips
|
||||
* together cover the same texture as the original face. Flat faces (the limb's ends) are kept as they
|
||||
* are. Strips keep the original winding.
|
||||
*/
|
||||
public final class QuadSplitter {
|
||||
private static final float EPSILON = 1e-4f;
|
||||
|
||||
private QuadSplitter() {
|
||||
}
|
||||
|
||||
public static List<float[][]> split(float[][] quad, float[] cuts) {
|
||||
float minY = Float.MAX_VALUE;
|
||||
float maxY = -Float.MAX_VALUE;
|
||||
for (float[] vertex : quad) {
|
||||
minY = Math.min(minY, vertex[1]);
|
||||
maxY = Math.max(maxY, vertex[1]);
|
||||
}
|
||||
if (maxY - minY < EPSILON) {
|
||||
return List.<float[][]>of(quad);
|
||||
}
|
||||
|
||||
// Each vertex's partner is its neighbour at the other end of the face's vertical edge.
|
||||
int[] partner = new int[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int next = (i + 1) % 4;
|
||||
int prev = (i + 3) % 4;
|
||||
partner[i] = Math.abs(quad[next][1] - quad[i][1]) > EPSILON ? next : prev;
|
||||
}
|
||||
|
||||
List<Float> bounds = new ArrayList<>();
|
||||
bounds.add(minY);
|
||||
for (float cut : cuts) {
|
||||
if (cut > minY + EPSILON && cut < maxY - EPSILON) {
|
||||
bounds.add(cut);
|
||||
}
|
||||
}
|
||||
bounds.add(maxY);
|
||||
|
||||
List<float[][]> strips = new ArrayList<>(bounds.size() - 1);
|
||||
for (int s = 0; s + 1 < bounds.size(); s++) {
|
||||
float top = bounds.get(s);
|
||||
float bottom = bounds.get(s + 1);
|
||||
float[][] strip = new float[4][];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float[] from = quad[i];
|
||||
float[] to = quad[partner[i]];
|
||||
float target = from[1] < to[1] ? top : bottom;
|
||||
strip[i] = lerp(from, to, (target - from[1]) / (to[1] - from[1]));
|
||||
}
|
||||
strips.add(strip);
|
||||
}
|
||||
return strips;
|
||||
}
|
||||
|
||||
private static float[] lerp(float[] a, float[] b, float t) {
|
||||
float[] out = new float[a.length];
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
out[i] = a[i] + (b[i] - a[i]) * t;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.saturnclient.emotes.core.bend;
|
||||
|
||||
/** A small immutable 3D vector, in model pixels. */
|
||||
public record Vec3(float x, float y, float z) {
|
||||
public static final Vec3 ZERO = new Vec3(0, 0, 0);
|
||||
|
||||
public Vec3 add(Vec3 o) {
|
||||
return new Vec3(x + o.x, y + o.y, z + o.z);
|
||||
}
|
||||
|
||||
public Vec3 sub(Vec3 o) {
|
||||
return new Vec3(x - o.x, y - o.y, z - o.z);
|
||||
}
|
||||
|
||||
public Vec3 scale(float s) {
|
||||
return new Vec3(x * s, y * s, z * s);
|
||||
}
|
||||
|
||||
public Vec3 lerp(Vec3 o, float t) {
|
||||
return new Vec3(x + (o.x - x) * t, y + (o.y - y) * t, z + (o.z - z) * t);
|
||||
}
|
||||
|
||||
public float dot(Vec3 o) {
|
||||
return x * o.x + y * o.y + z * o.z;
|
||||
}
|
||||
|
||||
public Vec3 cross(Vec3 o) {
|
||||
return new Vec3(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x);
|
||||
}
|
||||
|
||||
public float length() {
|
||||
return (float) Math.sqrt(dot(this));
|
||||
}
|
||||
|
||||
public Vec3 normalize() {
|
||||
float length = length();
|
||||
return length < 1e-7f ? ZERO : scale(1 / length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.saturnclient.emotes.core.bend;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class BezierTest {
|
||||
private static void assertVec(Vec3 expected, Vec3 actual) {
|
||||
assertEquals(expected.x(), actual.x(), 1e-5);
|
||||
assertEquals(expected.y(), actual.y(), 1e-5);
|
||||
assertEquals(expected.z(), actual.z(), 1e-5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void passesThroughEndpoints() {
|
||||
Bezier curve = Bezier.of(new Vec3(0, 0, 0), new Vec3(1, 2, 0), new Vec3(3, 2, 1), new Vec3(4, 0, 2));
|
||||
assertVec(new Vec3(0, 0, 0), curve.point(0));
|
||||
assertVec(new Vec3(4, 0, 2), curve.point(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quadraticMidpoint() {
|
||||
Bezier curve = Bezier.of(new Vec3(0, 0, 0), new Vec3(1, 2, 0), new Vec3(2, 0, 0));
|
||||
// B(0.5) = 0.25 P0 + 0.5 P1 + 0.25 P2
|
||||
assertVec(new Vec3(1, 1, 0), curve.point(0.5f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void endTangentsFollowControlPolygon() {
|
||||
Bezier curve = Bezier.of(new Vec3(0, 0, 0), new Vec3(0, 1, 0), new Vec3(1, 1, 0), new Vec3(2, 1, 0));
|
||||
// B'(0) = n (P1 - P0), B'(1) = n (P3 - P2)
|
||||
assertVec(new Vec3(0, 3, 0), curve.derivative(0));
|
||||
assertVec(new Vec3(3, 0, 0), curve.derivative(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void derivativeMatchesFiniteDifference() {
|
||||
Bezier curve = Bezier.of(new Vec3(0, 0, 0), new Vec3(1, 3, -1), new Vec3(2, -1, 4), new Vec3(5, 2, 0));
|
||||
float t = 0.37f;
|
||||
float h = 1e-3f;
|
||||
Vec3 numeric = curve.point(t + h).sub(curve.point(t - h)).scale(1 / (2 * h));
|
||||
Vec3 exact = curve.derivative(t);
|
||||
assertEquals(exact.x(), numeric.x(), 1e-2);
|
||||
assertEquals(exact.y(), numeric.y(), 1e-2);
|
||||
assertEquals(exact.z(), numeric.z(), 1e-2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package org.saturnclient.emotes.core.bend;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LimbBendTest {
|
||||
private static final float HALF_PI = (float) (Math.PI / 2);
|
||||
|
||||
/** An arm: pivot at the shoulder, box from y = -2 to 10. */
|
||||
private static LimbBend arm(float angle, float axis) {
|
||||
return new LimbBend(-2, 10, angle, axis, 0.5f, 12);
|
||||
}
|
||||
|
||||
private static float[] bend(LimbBend bend, float x, float y, float z) {
|
||||
float[] out = new float[3];
|
||||
bend.bendPoint(x, y, z, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static float distance(float[] a, float[] b) {
|
||||
float dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2];
|
||||
return (float) Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroBendLeavesPointsInPlace() {
|
||||
LimbBend straight = arm(0, 0);
|
||||
assertTrue(straight.isStraight());
|
||||
assertArrayEquals(new float[] { 1.5f, 7, -2 }, bend(straight, 1.5f, 7, -2), 1e-5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cutsDivideTheLimbEvenly() {
|
||||
float[] cuts = arm(1, 0).cuts();
|
||||
assertEquals(11, cuts.length);
|
||||
assertEquals(-1, cuts[0], 1e-5);
|
||||
assertEquals(9, cuts[10], 1e-5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void topStaysAttachedToThePivot() {
|
||||
LimbBend bend = arm(HALF_PI, 0);
|
||||
assertArrayEquals(new float[] { 2, -2, 2 }, bend(bend, 2, -2, 2), 1e-4f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void spineKeepsItsLength() {
|
||||
LimbBend bend = arm(HALF_PI, 0);
|
||||
float total = 0;
|
||||
float[] previous = bend(bend, 0, -2, 0);
|
||||
for (int i = 1; i <= 12; i++) {
|
||||
float[] point = bend(bend, 0, -2 + i, 0);
|
||||
total += distance(previous, point);
|
||||
previous = point;
|
||||
}
|
||||
// Rings are chords of the curve, so they come out a hair shorter than its arc.
|
||||
assertEquals(12, total, 0.05);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardBendMovesTheHandForward() {
|
||||
// Axis 0 folds towards -Z, the front of the model.
|
||||
float[] hand = bend(arm(HALF_PI, 0), 0, 10, 0);
|
||||
assertTrue(hand[2] < -4, "hand should be in front, z = " + hand[2]);
|
||||
assertEquals(0, hand[0], 1e-4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void axisTurnsTheBendDirection() {
|
||||
float[] hand = bend(arm(HALF_PI, HALF_PI), 0, 10, 0);
|
||||
assertTrue(hand[0] > 4, "hand should be towards +X, x = " + hand[0]);
|
||||
assertEquals(0, hand[2], 1e-4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void endPointsFollowTheLowerHalf() {
|
||||
// Past the bottom of the limb, points carry on along the forearm, which points forward at 90°.
|
||||
LimbBend bend = arm(HALF_PI, 0);
|
||||
float[] bottom = bend(bend, 0, 10, 0);
|
||||
float[] below = bend(bend, 0, 11, 0);
|
||||
assertEquals(0, below[1] - bottom[1], 0.05);
|
||||
assertEquals(-1, below[2] - bottom[2], 0.05);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ringsMoveRigidly() {
|
||||
// Every point on one ring keeps its distance from the ring's center.
|
||||
LimbBend bend = arm(1.2f, 0.7f);
|
||||
float[] center = bend(bend, 0, 4, 0);
|
||||
float[] corner = bend(bend, 2, 4, -2);
|
||||
assertEquals(Math.sqrt(8), distance(center, corner), 1e-4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void neighbouringPiecesShareVertices() {
|
||||
// A vertex on a cut is reached the same way from the piece above and the piece below.
|
||||
LimbBend bend = arm(2, 0);
|
||||
float[] fromAbove = bend(bend, 2, 3.99999f, 2);
|
||||
float[] fromBelow = bend(bend, 2, 4.00001f, 2);
|
||||
assertEquals(0, distance(fromAbove, fromBelow), 1e-3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalsRotateWithTheLimb() {
|
||||
// The front face's normal (-Z) points up (-Y) on the forearm after a 90° forward bend.
|
||||
float[] normal = new float[3];
|
||||
arm(HALF_PI, 0).bendDirection(10, 0, 0, -1, normal);
|
||||
assertArrayEquals(new float[] { 0, -1, 0 }, normal, 1e-3f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.saturnclient.emotes.core.bend;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class QuadSplitterTest {
|
||||
// Front face of a 4 x 12 limb from y = 0 to 12, texture rows 20 to 32.
|
||||
private static final float[][] FRONT = {
|
||||
{ 2, 0, -2, 8, 20 },
|
||||
{ -2, 0, -2, 4, 20 },
|
||||
{ -2, 12, -2, 4, 32 },
|
||||
{ 2, 12, -2, 8, 32 },
|
||||
};
|
||||
|
||||
private static final float[] CUTS = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
|
||||
|
||||
@Test
|
||||
void splitsSideFacesIntoOneStripPerSegment() {
|
||||
List<float[][]> strips = QuadSplitter.split(FRONT, CUTS);
|
||||
assertEquals(12, strips.size());
|
||||
assertArrayEquals(new float[] { 2, 5, -2, 8, 25 }, strips.get(5)[0], 1e-5f);
|
||||
assertArrayEquals(new float[] { -2, 6, -2, 4, 26 }, strips.get(5)[2], 1e-5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripsCoverTheFaceWithoutGaps() {
|
||||
List<float[][]> strips = QuadSplitter.split(FRONT, CUTS);
|
||||
for (int i = 0; i + 1 < strips.size(); i++) {
|
||||
// The bottom edge of one strip is the top edge of the next.
|
||||
assertArrayEquals(strips.get(i)[3], strips.get(i + 1)[0], 1e-5f);
|
||||
assertArrayEquals(strips.get(i)[2], strips.get(i + 1)[1], 1e-5f);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsFlatFaces() {
|
||||
float[][] top = {
|
||||
{ 2, 0, -2, 4, 16 },
|
||||
{ -2, 0, -2, 8, 16 },
|
||||
{ -2, 0, 2, 8, 20 },
|
||||
{ 2, 0, 2, 4, 20 },
|
||||
};
|
||||
List<float[][]> strips = QuadSplitter.split(top, CUTS);
|
||||
assertEquals(1, strips.size());
|
||||
assertSame(top, strips.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlaysThatStickOutGetShorterEndStrips() {
|
||||
// A sleeve inflated by 0.25 runs from -0.25 to 12.25.
|
||||
float[][] sleeve = {
|
||||
{ 2.25f, -0.25f, -2.25f, 48, 36 },
|
||||
{ -2.25f, -0.25f, -2.25f, 44, 36 },
|
||||
{ -2.25f, 12.25f, -2.25f, 44, 48 },
|
||||
{ 2.25f, 12.25f, -2.25f, 48, 48 },
|
||||
};
|
||||
List<float[][]> strips = QuadSplitter.split(sleeve, CUTS);
|
||||
assertEquals(12, strips.size());
|
||||
assertEquals(-0.25f, strips.get(0)[0][1], 1e-5);
|
||||
assertEquals(1, strips.get(0)[3][1], 1e-5);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user