Improve character controls and add idle breathing
This commit is contained in:
+37
-10
@@ -111,7 +111,7 @@ test('exported sheep has working joints and renders the final idle pose after wa
|
||||
assert.ok(leg.quaternion.angleTo(rest) > .01);
|
||||
assert.equal(sheep.pose('idle', 0, false, 1), true, 'stopping must invalidate one frame');
|
||||
assert.ok(leg.quaternion.angleTo(rest) < 1e-6);
|
||||
assert.equal(sheep.pose('idle', 0, false, 1), false);
|
||||
assert.equal(sheep.pose('idle', 0, false, 1), true, 'idle breathing keeps rendering');
|
||||
const arm = sheep.group.getObjectByName('ArmRight')!;
|
||||
const armRest = arm.quaternion.clone();
|
||||
sheep.pose('wave', 1, false, 1);
|
||||
@@ -121,7 +121,36 @@ test('exported sheep has working joints and renders the final idle pose after wa
|
||||
sheep.dispose();
|
||||
});
|
||||
|
||||
test('sideways and rear destinations turn the character forward and ease the camera behind it', () => {
|
||||
test('idle breathing moves torso and head while feet stay planted and clones retain authored rest transforms', async () => {
|
||||
const bytes = await readFile(new URL('../public/characters/red-sheep.glb', import.meta.url));
|
||||
const model = await new GLTFLoader().parseAsync(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), '');
|
||||
const sheep = createSheep(model.scene);
|
||||
const body = sheep.group.getObjectByName('Body')!, head = sheep.group.getObjectByName('Head')!;
|
||||
const bodyRest = body.scale.clone(), headRest = head.position.clone();
|
||||
const leg = sheep.group.getObjectByName('LegLeft')!;
|
||||
const feet = leg.getWorldPosition(new Vector3());
|
||||
sheep.pose('idle', 0, false, 0);
|
||||
for (let i = 1; i <= 8; i++) sheep.pose('idle', 0, false, i / 10);
|
||||
assert.ok(!body.scale.equals(bodyRest));
|
||||
assert.ok(!head.position.equals(headRest));
|
||||
assert.ok(leg.getWorldPosition(new Vector3()).distanceTo(feet) < 1e-8);
|
||||
const once = body.scale.clone();
|
||||
sheep.pose('idle', 0, false, .8);
|
||||
assert.deepEqual(body.scale, once, 'same timestamp cannot accumulate scaling');
|
||||
const remote = sheep.clone();
|
||||
remote.pose('wave', 0, false, 1);
|
||||
assert.deepEqual(remote.group.getObjectByName('Body')!.scale, bodyRest, 'clone does not inherit inflated rest scale');
|
||||
assert.deepEqual(remote.group.getObjectByName('Head')!.position, headRest);
|
||||
assert.deepEqual(body.scale, once, 'remote pose is independent');
|
||||
sheep.pose('idle', 0, true, .81);
|
||||
assert.ok(!body.scale.equals(bodyRest), 'breathing blends out instead of snapping');
|
||||
sheep.pose('idle', 0, true, 1.2);
|
||||
assert.deepEqual(body.scale, bodyRest);
|
||||
assert.deepEqual(head.position, headRest);
|
||||
remote.dispose(); sheep.dispose();
|
||||
});
|
||||
|
||||
test('sideways and rear destinations turn the character without changing the chosen camera angle', () => {
|
||||
for (const step of [new Vector3(1, 0, 0), new Vector3(0, 0, 1), new Vector3(-1, 0, -.01)]) {
|
||||
const character = new Group();
|
||||
character.rotation.y = Math.PI;
|
||||
@@ -129,21 +158,19 @@ test('sideways and rear destinations turn the character forward and ease the cam
|
||||
camera.rotation.set(-.2, 0, 0, 'YXZ');
|
||||
const original = camera.quaternion.clone();
|
||||
const initialFacing = character.quaternion.clone();
|
||||
const speed = faceTravel(character, camera, step, 1 / 60);
|
||||
const speed = faceTravel(character, step, 1 / 60);
|
||||
assert.ok(character.quaternion.angleTo(initialFacing) <= 4 / 60 + 1e-6, 'body must turn gradually');
|
||||
if (step.z === 1) assert.equal(speed, 0, 'rear destinations turn before translating');
|
||||
assert.ok(camera.quaternion.angleTo(original) < .22, 'camera must ease, not snap');
|
||||
for (let i = 0; i < 180; i++) faceTravel(character, camera, step, 1 / 60);
|
||||
assert.ok(camera.quaternion.equals(original), 'walking preserves the chosen camera angle');
|
||||
for (let i = 0; i < 180; i++) faceTravel(character, step, 1 / 60);
|
||||
assert.ok(character.getWorldDirection(new Vector3()).dot(step.clone().normalize()) > .999);
|
||||
const view = camera.getWorldDirection(new Vector3());
|
||||
view.y = 0;
|
||||
assert.ok(view.normalize().dot(step.clone().normalize()) > .999);
|
||||
assert.ok(camera.quaternion.equals(original));
|
||||
assert.ok(Math.abs(camera.rotation.x + .2) < 1e-6, 'preserve viewing pitch');
|
||||
const settled = character.quaternion.clone();
|
||||
faceTravel(character, camera, new Vector3(), 1 / 60);
|
||||
faceTravel(character, new Vector3(), 1 / 60);
|
||||
assert.ok(character.quaternion.angleTo(settled) < 1e-6, 'idle keeps the last facing');
|
||||
const manual = camera.quaternion.clone();
|
||||
faceTravel(character, camera, step.clone().negate(), 1 / 60, false);
|
||||
faceTravel(character, step.clone().negate(), 1 / 60);
|
||||
assert.ok(camera.quaternion.angleTo(manual) < 1e-6, 'manual looking takes priority');
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user