update puppet model
This commit is contained in:
@@ -11,10 +11,12 @@ export class PuppetAnimation {
|
|||||||
private readonly movementDelta = new THREE.Vector3();
|
private readonly movementDelta = new THREE.Vector3();
|
||||||
private readonly idleRotationY: number;
|
private readonly idleRotationY: number;
|
||||||
private idleTargetRotationY: number;
|
private idleTargetRotationY: number;
|
||||||
|
private readonly legRestY: number;
|
||||||
private hasSmoothedPosition = false;
|
private hasSmoothedPosition = false;
|
||||||
private wasMoving = false;
|
private wasMoving = false;
|
||||||
|
|
||||||
constructor(private readonly rig: PuppetRig) {
|
constructor(private readonly rig: PuppetRig) {
|
||||||
|
this.legRestY = rig.leftLeg.position.y;
|
||||||
this.idleRotationY = rig.root.rotation.y;
|
this.idleRotationY = rig.root.rotation.y;
|
||||||
this.idleTargetRotationY = this.idleRotationY;
|
this.idleTargetRotationY = this.idleRotationY;
|
||||||
}
|
}
|
||||||
@@ -77,6 +79,7 @@ export class PuppetAnimation {
|
|||||||
if (Math.abs(limb.rotation.x) <= ANGLE_EPSILON) limb.rotation.x = 0;
|
if (Math.abs(limb.rotation.x) <= ANGLE_EPSILON) limb.rotation.x = 0;
|
||||||
else active = true;
|
else active = true;
|
||||||
}
|
}
|
||||||
|
this.placeFeet();
|
||||||
return active;
|
return active;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,10 +122,27 @@ export class PuppetAnimation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private animateWalkCycle(elapsedSeconds: number) {
|
private animateWalkCycle(elapsedSeconds: number) {
|
||||||
const swing = Math.sin(elapsedSeconds * 8) * 0.5;
|
const swing = Math.sin(elapsedSeconds * 10);
|
||||||
this.rig.leftLeg.rotation.x = swing * 2;
|
this.rig.leftLeg.rotation.x = swing * 0.45;
|
||||||
this.rig.rightLeg.rotation.x = -swing * 2;
|
this.rig.rightLeg.rotation.x = -swing * 0.45;
|
||||||
this.rig.leftArm.rotation.x = -swing * 2;
|
this.rig.leftArm.rotation.x = -swing * 0.35;
|
||||||
this.rig.rightArm.rotation.x = swing * 2;
|
this.rig.rightArm.rotation.x = swing * 0.35;
|
||||||
|
this.placeFeet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lift each short leg enough to keep its broad foot from clipping the ground.
|
||||||
|
// Recompute while settling too, so stopping restores the exact standing pose.
|
||||||
|
private placeFeet() {
|
||||||
|
for (const [pivot, mesh] of [
|
||||||
|
[this.rig.leftLeg, this.rig.leftLegMesh],
|
||||||
|
[this.rig.rightLeg, this.rig.rightLegMesh],
|
||||||
|
] as const) {
|
||||||
|
const angle = pivot.rotation.x;
|
||||||
|
const { height, depth } = mesh.geometry.parameters;
|
||||||
|
pivot.position.y = this.legRestY + Math.max(
|
||||||
|
0,
|
||||||
|
height * (Math.cos(angle) - 1) + depth / 2 * Math.abs(Math.sin(angle)),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,45 +110,48 @@ function createRig(): PuppetRig {
|
|||||||
root.rotation.y = CAMERA_FACING_ROTATION_Y;
|
root.rotation.y = CAMERA_FACING_ROTATION_Y;
|
||||||
const material = new THREE.MeshStandardMaterial();
|
const material = new THREE.MeshStandardMaterial();
|
||||||
|
|
||||||
const body = new THREE.Mesh(new THREE.BoxGeometry(16, 24, 8), material);
|
const body = new THREE.Mesh(createTaperedBox(18, 20, 12, 0.8), material);
|
||||||
body.position.y = 36;
|
body.position.y = 26;
|
||||||
|
|
||||||
const head = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), material);
|
const head = new THREE.Mesh(createTaperedBox(30, 28, 24, 0.92), material);
|
||||||
head.position.y = 56;
|
head.position.y = 50;
|
||||||
|
|
||||||
const { pivot: leftArm, mesh: leftArmMesh } = createLimbPivot(
|
const { pivot: leftArm, mesh: leftArmMesh } = createLimbPivot(
|
||||||
-12,
|
-12,
|
||||||
48,
|
34,
|
||||||
8,
|
7,
|
||||||
24,
|
16,
|
||||||
8,
|
8,
|
||||||
material,
|
material,
|
||||||
);
|
);
|
||||||
const { pivot: rightArm, mesh: rightArmMesh } = createLimbPivot(
|
const { pivot: rightArm, mesh: rightArmMesh } = createLimbPivot(
|
||||||
12,
|
12,
|
||||||
48,
|
34,
|
||||||
8,
|
7,
|
||||||
24,
|
16,
|
||||||
8,
|
8,
|
||||||
material,
|
material,
|
||||||
);
|
);
|
||||||
const { pivot: leftLeg, mesh: leftLegMesh } = createLimbPivot(
|
const { pivot: leftLeg, mesh: leftLegMesh } = createLimbPivot(
|
||||||
-4,
|
-4,
|
||||||
24,
|
16,
|
||||||
8,
|
|
||||||
24,
|
|
||||||
8,
|
8,
|
||||||
|
16,
|
||||||
|
10,
|
||||||
material,
|
material,
|
||||||
);
|
);
|
||||||
const { pivot: rightLeg, mesh: rightLegMesh } = createLimbPivot(
|
const { pivot: rightLeg, mesh: rightLegMesh } = createLimbPivot(
|
||||||
4,
|
4,
|
||||||
24,
|
16,
|
||||||
8,
|
|
||||||
24,
|
|
||||||
8,
|
8,
|
||||||
|
16,
|
||||||
|
10,
|
||||||
material,
|
material,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Splayed shoulders keep the short hands clear of the wider torso.
|
||||||
|
leftArm.rotation.z = -0.16;
|
||||||
|
rightArm.rotation.z = 0.16;
|
||||||
root.add(body, head, leftArm, rightArm, leftLeg, rightLeg);
|
root.add(body, head, leftArm, rightArm, leftLeg, rightLeg);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -178,7 +181,7 @@ function createLimbPivot(
|
|||||||
pivot.position.set(x, y, 0);
|
pivot.position.set(x, y, 0);
|
||||||
|
|
||||||
const limb = new THREE.Mesh(
|
const limb = new THREE.Mesh(
|
||||||
new THREE.BoxGeometry(width, height, depth),
|
createTaperedBox(width, height, depth, 0.85),
|
||||||
material,
|
material,
|
||||||
);
|
);
|
||||||
limb.position.y = -height / 2;
|
limb.position.y = -height / 2;
|
||||||
@@ -186,3 +189,24 @@ function createLimbPivot(
|
|||||||
|
|
||||||
return { pivot, mesh: limb };
|
return { pivot, mesh: limb };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Keep BoxGeometry's six four-vertex faces so the existing skin atlas still fits.
|
||||||
|
* The narrower upper end gives the torso shoulders and the limbs chunky ends.
|
||||||
|
*/
|
||||||
|
function createTaperedBox(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
depth: number,
|
||||||
|
topScale: number,
|
||||||
|
) {
|
||||||
|
const geometry = new THREE.BoxGeometry(width, height, depth);
|
||||||
|
const positions = geometry.attributes.position;
|
||||||
|
for (let i = 0; i < positions.count; i++) {
|
||||||
|
if (positions.getY(i) > 0) {
|
||||||
|
positions.setX(i, positions.getX(i) * topScale);
|
||||||
|
positions.setZ(i, positions.getZ(i) * topScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
geometry.computeVertexNormals();
|
||||||
|
return geometry;
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,3 +97,31 @@ test('limb relaxation has the same speed at 30 and 120 FPS and finishes', () =>
|
|||||||
fast.dispose();
|
fast.dispose();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('broad feet stay above ground throughout a stride and return to rest', () => {
|
||||||
|
const visual = new PuppetVisual('feet');
|
||||||
|
const point = new THREE.Vector3();
|
||||||
|
const restPositions = visual.root.children.map((part) => part.position.clone());
|
||||||
|
try {
|
||||||
|
for (let frame = 0; frame <= 120; frame++) {
|
||||||
|
visual.walkInPlace(0, frame / 120 * Math.PI * 2 / 10);
|
||||||
|
visual.root.updateMatrixWorld(true);
|
||||||
|
visual.root.traverse((part) => {
|
||||||
|
if (!(part instanceof THREE.Mesh)) return;
|
||||||
|
const vertices = part.geometry.attributes.position;
|
||||||
|
for (let i = 0; i < vertices.count; i++) {
|
||||||
|
point.fromBufferAttribute(vertices, i).applyMatrix4(part.matrixWorld);
|
||||||
|
assert.ok(point.y >= -1e-6, `vertex below ground: ${point.y}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
visual.walkInPlace(0, 0.1);
|
||||||
|
for (let frame = 0; frame < 120; frame++) visual.pause(1 / 60);
|
||||||
|
assert.equal(visual.pause(0), false);
|
||||||
|
visual.root.children.forEach((part, index) => {
|
||||||
|
assert.deepEqual(part.position, restPositions[index]);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
visual.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user