minecraft characters.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import * as THREE from "three";
|
||||
import type { PuppetRig } from ".";
|
||||
|
||||
export class PuppetAnimation {
|
||||
private readonly smoothedGroundPosition = new THREE.Vector3();
|
||||
private readonly movementDelta = new THREE.Vector3();
|
||||
private hasSmoothedPosition = false;
|
||||
|
||||
constructor(private readonly rig: PuppetRig) {}
|
||||
|
||||
update(
|
||||
targetGroundPosition: THREE.Vector3,
|
||||
isMoving: boolean,
|
||||
deltaSeconds: number,
|
||||
elapsedSeconds: number,
|
||||
) {
|
||||
if (!this.hasSmoothedPosition) {
|
||||
this.smoothedGroundPosition.copy(targetGroundPosition);
|
||||
this.rig.root.position.copy(targetGroundPosition);
|
||||
this.hasSmoothedPosition = true;
|
||||
}
|
||||
|
||||
this.movementDelta
|
||||
.copy(targetGroundPosition)
|
||||
.sub(this.smoothedGroundPosition);
|
||||
|
||||
if (this.movementDelta.lengthSq() <= 0.01) {
|
||||
this.pause();
|
||||
this.rig.root.position.copy(targetGroundPosition);
|
||||
return;
|
||||
}
|
||||
|
||||
const alpha = 1 - Math.exp(-12 * deltaSeconds);
|
||||
this.smoothedGroundPosition.lerp(targetGroundPosition, alpha);
|
||||
this.rig.root.position.copy(this.smoothedGroundPosition);
|
||||
|
||||
const targetRotationY = Math.atan2(
|
||||
this.movementDelta.x,
|
||||
this.movementDelta.z,
|
||||
);
|
||||
this.rig.root.rotation.y = this.dampAngle(
|
||||
this.rig.root.rotation.y,
|
||||
targetRotationY,
|
||||
14,
|
||||
deltaSeconds,
|
||||
);
|
||||
|
||||
if (isMoving) {
|
||||
this.animateWalkCycle(elapsedSeconds);
|
||||
} else {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.rig.leftLeg.rotation.x *= 0.8;
|
||||
this.rig.rightLeg.rotation.x *= 0.8;
|
||||
this.rig.leftArm.rotation.x *= 0.8;
|
||||
this.rig.rightArm.rotation.x *= 0.8;
|
||||
}
|
||||
|
||||
private dampAngle(
|
||||
currentRadians: number,
|
||||
targetRadians: number,
|
||||
smoothingSpeed: number,
|
||||
deltaSeconds: number,
|
||||
) {
|
||||
const deltaRadians = Math.atan2(
|
||||
Math.sin(targetRadians - currentRadians),
|
||||
Math.cos(targetRadians - currentRadians),
|
||||
);
|
||||
const alpha = 1 - Math.exp(-smoothingSpeed * deltaSeconds);
|
||||
return currentRadians + deltaRadians * alpha;
|
||||
}
|
||||
|
||||
private animateWalkCycle(elapsedSeconds: number) {
|
||||
const swing = Math.sin(elapsedSeconds * 8) * 0.5;
|
||||
this.rig.leftLeg.rotation.x = swing * 2;
|
||||
this.rig.rightLeg.rotation.x = -swing * 2;
|
||||
this.rig.leftArm.rotation.x = -swing * 2;
|
||||
this.rig.rightArm.rotation.x = swing * 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { PuppetState } from "$lib/bindings";
|
||||
import * as THREE from "three";
|
||||
import type { World } from "../world";
|
||||
import { PuppetAnimation } from "./animation";
|
||||
|
||||
export type PuppetRig = {
|
||||
root: THREE.Group;
|
||||
leftArm: THREE.Group;
|
||||
rightArm: THREE.Group;
|
||||
leftLeg: THREE.Group;
|
||||
rightLeg: THREE.Group;
|
||||
};
|
||||
|
||||
export class Puppet {
|
||||
readonly root: THREE.Group;
|
||||
|
||||
private readonly rig: PuppetRig;
|
||||
private readonly animation: PuppetAnimation;
|
||||
private readonly targetGroundPosition = new THREE.Vector3();
|
||||
|
||||
constructor(readonly id: string) {
|
||||
this.rig = this.createRig();
|
||||
this.root = this.rig.root;
|
||||
this.animation = new PuppetAnimation(this.rig);
|
||||
}
|
||||
|
||||
update(
|
||||
state: PuppetState,
|
||||
world: World,
|
||||
frozen: boolean,
|
||||
deltaSeconds: number,
|
||||
elapsedSeconds: number,
|
||||
) {
|
||||
if (frozen) {
|
||||
this.animation.pause();
|
||||
return;
|
||||
}
|
||||
|
||||
world.normalizedPointToGroundPoint(
|
||||
state.position.x,
|
||||
state.position.y,
|
||||
this.targetGroundPosition,
|
||||
);
|
||||
this.animation.update(
|
||||
this.targetGroundPosition,
|
||||
state.isMoving,
|
||||
deltaSeconds,
|
||||
elapsedSeconds,
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
const disposedMaterials = new Set<THREE.Material>();
|
||||
|
||||
this.root.traverse((object) => {
|
||||
if (!(object instanceof THREE.Mesh)) return;
|
||||
|
||||
object.geometry.dispose();
|
||||
const materials = Array.isArray(object.material)
|
||||
? object.material
|
||||
: [object.material];
|
||||
for (const material of materials) {
|
||||
if (disposedMaterials.has(material)) continue;
|
||||
material.dispose();
|
||||
disposedMaterials.add(material);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private createRig(): PuppetRig {
|
||||
const root = new THREE.Group();
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: this.colorFromId(),
|
||||
});
|
||||
|
||||
const body = new THREE.Mesh(new THREE.BoxGeometry(16, 24, 8), material);
|
||||
body.position.y = 36;
|
||||
|
||||
const head = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), material);
|
||||
head.position.y = 56;
|
||||
|
||||
const leftArm = this.createLimbPivot(-12, 48, 8, 24, 8, material);
|
||||
const rightArm = this.createLimbPivot(12, 48, 8, 24, 8, material);
|
||||
const leftLeg = this.createLimbPivot(-4, 24, 8, 24, 8, material);
|
||||
const rightLeg = this.createLimbPivot(4, 24, 8, 24, 8, material);
|
||||
|
||||
root.add(body, head, leftArm, rightArm, leftLeg, rightLeg);
|
||||
|
||||
return { root, leftArm, rightArm, leftLeg, rightLeg };
|
||||
}
|
||||
|
||||
private createLimbPivot(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
depth: number,
|
||||
material: THREE.Material,
|
||||
) {
|
||||
const pivot = new THREE.Group();
|
||||
pivot.position.set(x, y, 0);
|
||||
|
||||
const limb = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(width, height, depth),
|
||||
material,
|
||||
);
|
||||
limb.position.y = -height / 2;
|
||||
pivot.add(limb);
|
||||
|
||||
return pivot;
|
||||
}
|
||||
|
||||
private colorFromId() {
|
||||
let hash = 0;
|
||||
for (const character of this.id) {
|
||||
hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
|
||||
}
|
||||
|
||||
return new THREE.Color().setHSL((hash % 360) / 360, 0.55, 0.62);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { PuppetState } from "$lib/bindings";
|
||||
import type { PuppetScreenBounds } from "../types";
|
||||
import type { World } from "../world";
|
||||
import { Puppet } from ".";
|
||||
|
||||
export class PuppetManager {
|
||||
private readonly puppets = new Map<string, Puppet>();
|
||||
|
||||
constructor(private readonly world: World) {}
|
||||
|
||||
update(
|
||||
states: readonly PuppetState[],
|
||||
frozenPuppetId: string | null,
|
||||
deltaSeconds: number,
|
||||
elapsedSeconds: number,
|
||||
) {
|
||||
this.syncPuppets(states);
|
||||
|
||||
for (const state of states) {
|
||||
this.puppets
|
||||
.get(state.id)
|
||||
?.update(
|
||||
state,
|
||||
this.world,
|
||||
state.id === frozenPuppetId,
|
||||
deltaSeconds,
|
||||
elapsedSeconds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
screenBounds(): PuppetScreenBounds[] {
|
||||
return [...this.puppets.values()].map((puppet) => ({
|
||||
id: puppet.id,
|
||||
...this.world.objectScreenBounds(puppet.root),
|
||||
}));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const puppet of this.puppets.values()) {
|
||||
this.world.removeObject(puppet.root);
|
||||
puppet.dispose();
|
||||
}
|
||||
this.puppets.clear();
|
||||
}
|
||||
|
||||
private syncPuppets(states: readonly PuppetState[]) {
|
||||
const nextPuppetIds = new Set(states.map((state) => state.id));
|
||||
|
||||
for (const state of states) {
|
||||
if (this.puppets.has(state.id)) continue;
|
||||
|
||||
const puppet = new Puppet(state.id);
|
||||
this.puppets.set(puppet.id, puppet);
|
||||
this.world.addObject(puppet.root);
|
||||
}
|
||||
|
||||
for (const [puppetId, puppet] of this.puppets) {
|
||||
if (nextPuppetIds.has(puppetId)) continue;
|
||||
|
||||
this.world.removeObject(puppet.root);
|
||||
puppet.dispose();
|
||||
this.puppets.delete(puppetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user