very hot looking puppet preview and move skin btn to scene view
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import * as THREE from "three";
|
||||
import type { PuppetRig } from ".";
|
||||
import type { PuppetRig } from "./visual";
|
||||
|
||||
const MIN_IDLE_FACING_OFFSET_DEGREES = 3;
|
||||
const MAX_IDLE_FACING_OFFSET_DEGREES = 5;
|
||||
@@ -67,6 +67,10 @@ export class PuppetAnimation {
|
||||
this.rig.rightArm.rotation.x *= 0.8;
|
||||
}
|
||||
|
||||
walk(elapsedSeconds: number) {
|
||||
this.animateWalkCycle(elapsedSeconds);
|
||||
}
|
||||
|
||||
private updateIdleTarget(isMoving: boolean) {
|
||||
if (isMoving) {
|
||||
this.wasMoving = true;
|
||||
|
||||
@@ -1,52 +1,26 @@
|
||||
import type { PuppetState } from "$lib/bindings";
|
||||
import * as THREE from "three";
|
||||
import type { World } from "../world";
|
||||
import { PuppetAnimation } from "./animation";
|
||||
import { PuppetSkin } from "./skin";
|
||||
|
||||
const CAMERA_FACING_ROTATION_Y = Math.PI / 4;
|
||||
|
||||
export type PuppetRig = {
|
||||
root: THREE.Group;
|
||||
body: THREE.Mesh<THREE.BoxGeometry, THREE.Material | THREE.Material[]>;
|
||||
head: THREE.Mesh<THREE.BoxGeometry, THREE.Material | THREE.Material[]>;
|
||||
leftArm: THREE.Group;
|
||||
leftArmMesh: THREE.Mesh<THREE.BoxGeometry, THREE.Material | THREE.Material[]>;
|
||||
rightArm: THREE.Group;
|
||||
rightArmMesh: THREE.Mesh<
|
||||
THREE.BoxGeometry,
|
||||
THREE.Material | THREE.Material[]
|
||||
>;
|
||||
leftLeg: THREE.Group;
|
||||
leftLegMesh: THREE.Mesh<THREE.BoxGeometry, THREE.Material | THREE.Material[]>;
|
||||
rightLeg: THREE.Group;
|
||||
rightLegMesh: THREE.Mesh<
|
||||
THREE.BoxGeometry,
|
||||
THREE.Material | THREE.Material[]
|
||||
>;
|
||||
};
|
||||
import { PuppetVisual } from "./visual";
|
||||
|
||||
export class Puppet {
|
||||
readonly root: THREE.Group;
|
||||
|
||||
private readonly rig: PuppetRig;
|
||||
private readonly animation: PuppetAnimation;
|
||||
private readonly skin: PuppetSkin;
|
||||
private readonly visual: PuppetVisual;
|
||||
private readonly targetGroundPosition = new THREE.Vector3();
|
||||
|
||||
constructor(readonly id: string, scale: number) {
|
||||
this.rig = this.createRig();
|
||||
this.root = this.rig.root;
|
||||
this.animation = new PuppetAnimation(this.rig);
|
||||
const placeholderMaterial = this.rig.body.material;
|
||||
this.skin = new PuppetSkin(id, this.rig);
|
||||
this.setScale(scale);
|
||||
if (!Array.isArray(placeholderMaterial)) placeholderMaterial.dispose();
|
||||
constructor(
|
||||
readonly id: string,
|
||||
scale: number,
|
||||
) {
|
||||
this.visual = new PuppetVisual(id);
|
||||
this.root = this.visual.root;
|
||||
this.visual.setScale(scale);
|
||||
this.visual.setOpacity(1);
|
||||
}
|
||||
|
||||
setScale(scale: number) {
|
||||
if (this.root.scale.x === scale) return;
|
||||
this.root.scale.setScalar(scale);
|
||||
this.visual.setScale(scale);
|
||||
}
|
||||
|
||||
update(
|
||||
@@ -58,10 +32,10 @@ export class Puppet {
|
||||
skinHash: string | null,
|
||||
opacity: number,
|
||||
) {
|
||||
this.skin.update(skinHash);
|
||||
this.skin.setOpacity(opacity);
|
||||
this.visual.setSkin(skinHash);
|
||||
this.visual.setOpacity(opacity);
|
||||
if (frozen) {
|
||||
this.animation.pause();
|
||||
this.visual.pause();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,7 +44,7 @@ export class Puppet {
|
||||
state.position.y,
|
||||
this.targetGroundPosition,
|
||||
);
|
||||
this.animation.update(
|
||||
this.visual.updateMotion(
|
||||
this.targetGroundPosition,
|
||||
state.isMoving,
|
||||
deltaSeconds,
|
||||
@@ -79,101 +53,6 @@ export class Puppet {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.skin.dispose();
|
||||
|
||||
const geometries = new Set<THREE.BufferGeometry>();
|
||||
this.root.traverse((object) => {
|
||||
if (!(object instanceof THREE.Mesh)) return;
|
||||
geometries.add(object.geometry);
|
||||
});
|
||||
for (const geometry of geometries) geometry.dispose();
|
||||
}
|
||||
|
||||
private createRig(): PuppetRig {
|
||||
const root = new THREE.Group();
|
||||
root.rotation.y = CAMERA_FACING_ROTATION_Y;
|
||||
const material = new THREE.MeshStandardMaterial();
|
||||
|
||||
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 { pivot: leftArm, mesh: leftArmMesh } = this.createLimbPivot(
|
||||
-12,
|
||||
48,
|
||||
8,
|
||||
24,
|
||||
8,
|
||||
material,
|
||||
);
|
||||
const { pivot: rightArm, mesh: rightArmMesh } = this.createLimbPivot(
|
||||
12,
|
||||
48,
|
||||
8,
|
||||
24,
|
||||
8,
|
||||
material,
|
||||
);
|
||||
const { pivot: leftLeg, mesh: leftLegMesh } = this.createLimbPivot(
|
||||
-4,
|
||||
24,
|
||||
8,
|
||||
24,
|
||||
8,
|
||||
material,
|
||||
);
|
||||
const { pivot: rightLeg, mesh: rightLegMesh } = this.createLimbPivot(
|
||||
4,
|
||||
24,
|
||||
8,
|
||||
24,
|
||||
8,
|
||||
material,
|
||||
);
|
||||
|
||||
root.add(body, head, leftArm, rightArm, leftLeg, rightLeg);
|
||||
|
||||
return {
|
||||
root,
|
||||
body,
|
||||
head,
|
||||
leftArm,
|
||||
leftArmMesh,
|
||||
rightArm,
|
||||
rightArmMesh,
|
||||
leftLeg,
|
||||
leftLegMesh,
|
||||
rightLeg,
|
||||
rightLegMesh,
|
||||
};
|
||||
}
|
||||
|
||||
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, mesh: limb };
|
||||
this.visual.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import * as THREE from "three";
|
||||
import { PuppetVisual, SOUTH_WEST_ROTATION_Y } from "./visual";
|
||||
|
||||
let {
|
||||
userId,
|
||||
skinHash,
|
||||
skinSource = null,
|
||||
scale,
|
||||
opacity,
|
||||
}: {
|
||||
userId: string;
|
||||
skinHash: string | null;
|
||||
skinSource?: string | null;
|
||||
scale: number;
|
||||
opacity: number;
|
||||
} = $props();
|
||||
|
||||
let container = $state<HTMLDivElement | null>(null);
|
||||
let renderer: THREE.WebGLRenderer | null = null;
|
||||
let visual: PuppetVisual | null = null;
|
||||
let clock: THREE.Timer | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let animationFrameId = 0;
|
||||
|
||||
function animate() {
|
||||
if (!renderer || !visual || !clock) return;
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
clock.update();
|
||||
visual.setAppearance({ skinHash, skinSource, scale, opacity });
|
||||
visual.walkInPlace(SOUTH_WEST_ROTATION_Y, clock.getElapsed());
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
function resizePreview() {
|
||||
if (!container || !renderer) return;
|
||||
const { clientWidth: width, clientHeight: height } = container;
|
||||
if (width === 0 || height === 0) return;
|
||||
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(width, height);
|
||||
camera.left = width / -2;
|
||||
camera.right = width / 2;
|
||||
camera.top = height / 2;
|
||||
camera.bottom = height / -2;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = null;
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 1_000);
|
||||
camera.position.set(200, 134, 200);
|
||||
camera.lookAt(0, 34, 0);
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
scene.add(new THREE.AmbientLight("white", 0.8));
|
||||
const directionalLight = new THREE.DirectionalLight("white", 2);
|
||||
directionalLight.position.set(200, 150, 150);
|
||||
scene.add(directionalLight);
|
||||
|
||||
onMount(() => {
|
||||
if (!container) return;
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setSize(1, 1);
|
||||
container.appendChild(renderer.domElement);
|
||||
resizePreview();
|
||||
|
||||
resizeObserver = new ResizeObserver(resizePreview);
|
||||
resizeObserver.observe(container);
|
||||
|
||||
visual = new PuppetVisual(userId);
|
||||
scene.add(visual.root);
|
||||
|
||||
clock = new THREE.Timer();
|
||||
clock.connect(document);
|
||||
animate();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
resizeObserver?.disconnect();
|
||||
clock?.dispose();
|
||||
visual?.dispose();
|
||||
renderer?.dispose();
|
||||
renderer?.domElement.remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="aspect-square size-full overflow-hidden"
|
||||
aria-label="Live puppet preview"
|
||||
role="img"
|
||||
bind:this={container}
|
||||
></div>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolveSkinSource } from "$lib/skins";
|
||||
import * as THREE from "three";
|
||||
import type { PuppetRig } from ".";
|
||||
import type { PuppetRig } from "./visual";
|
||||
|
||||
type Region = {
|
||||
x: number;
|
||||
@@ -120,7 +120,7 @@ export class PuppetSkin {
|
||||
private readonly material: THREE.MeshStandardMaterial;
|
||||
private readonly depthMaterial: THREE.MeshBasicMaterial;
|
||||
private texture: THREE.Texture | null = null;
|
||||
private requestedHash: string | null | undefined;
|
||||
private requestedSkinKey: string | undefined;
|
||||
private requestId = 0;
|
||||
|
||||
constructor(
|
||||
@@ -160,11 +160,21 @@ export class PuppetSkin {
|
||||
}
|
||||
|
||||
update(skinHash: string | null) {
|
||||
if (skinHash === this.requestedHash) return;
|
||||
this.requestedHash = skinHash;
|
||||
this.updateTexture(`hash:${skinHash ?? "default"}`, () =>
|
||||
resolveSkinSource(this.userId, skinHash),
|
||||
);
|
||||
}
|
||||
|
||||
updateSource(source: string) {
|
||||
this.updateTexture(`source:${source}`, () => Promise.resolve(source));
|
||||
}
|
||||
|
||||
private updateTexture(key: string, resolveSource: () => Promise<string>) {
|
||||
if (key === this.requestedSkinKey) return;
|
||||
this.requestedSkinKey = key;
|
||||
const requestId = ++this.requestId;
|
||||
|
||||
resolveSkinSource(this.userId, skinHash)
|
||||
resolveSource()
|
||||
.then(async (source) => {
|
||||
if (requestId !== this.requestId) return;
|
||||
const texture = await loadTexture(source);
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import * as THREE from "three";
|
||||
import { PuppetAnimation } from "./animation";
|
||||
import { PuppetSkin } from "./skin";
|
||||
|
||||
export const CAMERA_FACING_ROTATION_Y = Math.PI / 4;
|
||||
export const SOUTH_WEST_ROTATION_Y = -Math.PI / 0.01;
|
||||
|
||||
export type PuppetAppearance = {
|
||||
skinHash: string | null;
|
||||
skinSource?: string | null;
|
||||
scale: number;
|
||||
opacity: number;
|
||||
};
|
||||
|
||||
export type PuppetRig = {
|
||||
root: THREE.Group;
|
||||
body: THREE.Mesh<THREE.BoxGeometry, THREE.Material | THREE.Material[]>;
|
||||
head: THREE.Mesh<THREE.BoxGeometry, THREE.Material | THREE.Material[]>;
|
||||
leftArm: THREE.Group;
|
||||
leftArmMesh: THREE.Mesh<THREE.BoxGeometry, THREE.Material | THREE.Material[]>;
|
||||
rightArm: THREE.Group;
|
||||
rightArmMesh: THREE.Mesh<
|
||||
THREE.BoxGeometry,
|
||||
THREE.Material | THREE.Material[]
|
||||
>;
|
||||
leftLeg: THREE.Group;
|
||||
leftLegMesh: THREE.Mesh<THREE.BoxGeometry, THREE.Material | THREE.Material[]>;
|
||||
rightLeg: THREE.Group;
|
||||
rightLegMesh: THREE.Mesh<
|
||||
THREE.BoxGeometry,
|
||||
THREE.Material | THREE.Material[]
|
||||
>;
|
||||
};
|
||||
|
||||
export class PuppetVisual {
|
||||
readonly root: THREE.Group;
|
||||
|
||||
private readonly rig: PuppetRig;
|
||||
private readonly animation: PuppetAnimation;
|
||||
private readonly skin: PuppetSkin;
|
||||
|
||||
constructor(userId: string) {
|
||||
this.rig = createRig();
|
||||
this.root = this.rig.root;
|
||||
this.animation = new PuppetAnimation(this.rig);
|
||||
const placeholderMaterial = this.rig.body.material;
|
||||
this.skin = new PuppetSkin(userId, this.rig);
|
||||
if (!Array.isArray(placeholderMaterial)) placeholderMaterial.dispose();
|
||||
}
|
||||
|
||||
setAppearance({ skinHash, skinSource, scale, opacity }: PuppetAppearance) {
|
||||
if (skinSource) this.setSkinSource(skinSource);
|
||||
else this.setSkin(skinHash);
|
||||
this.setScale(scale);
|
||||
this.setOpacity(opacity);
|
||||
}
|
||||
|
||||
setSkin(skinHash: string | null) {
|
||||
this.skin.update(skinHash);
|
||||
}
|
||||
|
||||
setSkinSource(source: string) {
|
||||
this.skin.updateSource(source);
|
||||
}
|
||||
|
||||
setScale(scale: number) {
|
||||
if (this.root.scale.x !== scale) this.root.scale.setScalar(scale);
|
||||
}
|
||||
|
||||
setOpacity(opacity: number) {
|
||||
this.skin.setOpacity(opacity);
|
||||
}
|
||||
|
||||
updateMotion(
|
||||
targetGroundPosition: THREE.Vector3,
|
||||
isMoving: boolean,
|
||||
deltaSeconds: number,
|
||||
elapsedSeconds: number,
|
||||
) {
|
||||
this.animation.update(
|
||||
targetGroundPosition,
|
||||
isMoving,
|
||||
deltaSeconds,
|
||||
elapsedSeconds,
|
||||
);
|
||||
}
|
||||
|
||||
walkInPlace(facingRotationY: number, elapsedSeconds: number) {
|
||||
this.root.rotation.y = facingRotationY;
|
||||
this.animation.walk(elapsedSeconds);
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.animation.pause();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.skin.dispose();
|
||||
|
||||
const geometries = new Set<THREE.BufferGeometry>();
|
||||
this.root.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) geometries.add(object.geometry);
|
||||
});
|
||||
for (const geometry of geometries) geometry.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function createRig(): PuppetRig {
|
||||
const root = new THREE.Group();
|
||||
root.rotation.y = CAMERA_FACING_ROTATION_Y;
|
||||
const material = new THREE.MeshStandardMaterial();
|
||||
|
||||
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 { pivot: leftArm, mesh: leftArmMesh } = createLimbPivot(
|
||||
-12,
|
||||
48,
|
||||
8,
|
||||
24,
|
||||
8,
|
||||
material,
|
||||
);
|
||||
const { pivot: rightArm, mesh: rightArmMesh } = createLimbPivot(
|
||||
12,
|
||||
48,
|
||||
8,
|
||||
24,
|
||||
8,
|
||||
material,
|
||||
);
|
||||
const { pivot: leftLeg, mesh: leftLegMesh } = createLimbPivot(
|
||||
-4,
|
||||
24,
|
||||
8,
|
||||
24,
|
||||
8,
|
||||
material,
|
||||
);
|
||||
const { pivot: rightLeg, mesh: rightLegMesh } = createLimbPivot(
|
||||
4,
|
||||
24,
|
||||
8,
|
||||
24,
|
||||
8,
|
||||
material,
|
||||
);
|
||||
|
||||
root.add(body, head, leftArm, rightArm, leftLeg, rightLeg);
|
||||
|
||||
return {
|
||||
root,
|
||||
body,
|
||||
head,
|
||||
leftArm,
|
||||
leftArmMesh,
|
||||
rightArm,
|
||||
rightArmMesh,
|
||||
leftLeg,
|
||||
leftLegMesh,
|
||||
rightLeg,
|
||||
rightLegMesh,
|
||||
};
|
||||
}
|
||||
|
||||
function 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, mesh: limb };
|
||||
}
|
||||
Reference in New Issue
Block a user