minecraft skins!
This commit is contained in:
@@ -2,13 +2,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";
|
||||
|
||||
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 Puppet {
|
||||
@@ -16,12 +29,16 @@ export class Puppet {
|
||||
|
||||
private readonly rig: PuppetRig;
|
||||
private readonly animation: PuppetAnimation;
|
||||
private readonly skin: PuppetSkin;
|
||||
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);
|
||||
const placeholderMaterial = this.rig.body.material;
|
||||
this.skin = new PuppetSkin(id, this.rig);
|
||||
if (!Array.isArray(placeholderMaterial)) placeholderMaterial.dispose();
|
||||
}
|
||||
|
||||
update(
|
||||
@@ -30,7 +47,9 @@ export class Puppet {
|
||||
frozen: boolean,
|
||||
deltaSeconds: number,
|
||||
elapsedSeconds: number,
|
||||
skinHash: string | null,
|
||||
) {
|
||||
this.skin.update(skinHash);
|
||||
if (frozen) {
|
||||
this.animation.pause();
|
||||
return;
|
||||
@@ -50,28 +69,18 @@ export class Puppet {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
const disposedMaterials = new Set<THREE.Material>();
|
||||
this.skin.dispose();
|
||||
|
||||
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 material = new THREE.MeshStandardMaterial();
|
||||
|
||||
const body = new THREE.Mesh(new THREE.BoxGeometry(16, 24, 8), material);
|
||||
body.position.y = 36;
|
||||
@@ -79,14 +88,54 @@ export class Puppet {
|
||||
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);
|
||||
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, leftArm, rightArm, leftLeg, rightLeg };
|
||||
return {
|
||||
root,
|
||||
body,
|
||||
head,
|
||||
leftArm,
|
||||
leftArmMesh,
|
||||
rightArm,
|
||||
rightArmMesh,
|
||||
leftLeg,
|
||||
leftLegMesh,
|
||||
rightLeg,
|
||||
rightLegMesh,
|
||||
};
|
||||
}
|
||||
|
||||
private createLimbPivot(
|
||||
@@ -107,15 +156,6 @@ export class Puppet {
|
||||
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);
|
||||
return { pivot, mesh: limb };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export class PuppetManager {
|
||||
frozenPuppetId: string | null,
|
||||
deltaSeconds: number,
|
||||
elapsedSeconds: number,
|
||||
skinHashes: ReadonlyMap<string, string | null>,
|
||||
) {
|
||||
this.syncPuppets(states);
|
||||
|
||||
@@ -25,6 +26,7 @@ export class PuppetManager {
|
||||
state.id === frozenPuppetId,
|
||||
deltaSeconds,
|
||||
elapsedSeconds,
|
||||
skinHashes.get(state.id) ?? null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { resolveSkinSource } from "$lib/skins";
|
||||
import * as THREE from "three";
|
||||
import type { PuppetRig } from ".";
|
||||
|
||||
type Region = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
flipX?: boolean;
|
||||
};
|
||||
type Faces = [Region, Region, Region, Region, Region, Region];
|
||||
type SkinPart = {
|
||||
mesh: THREE.Mesh<THREE.BoxGeometry, THREE.Material | THREE.Material[]>;
|
||||
faces: Faces;
|
||||
};
|
||||
|
||||
const ATLAS_SIZE = 64;
|
||||
const region = (
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
flipX = false,
|
||||
): Region => ({ x, y, width, height, flipX });
|
||||
const faces = (
|
||||
right: Region,
|
||||
left: Region,
|
||||
top: Region,
|
||||
bottom: Region,
|
||||
front: Region,
|
||||
back: Region,
|
||||
): Faces => [right, left, top, bottom, front, back];
|
||||
|
||||
const HEAD = faces(
|
||||
region(0, 8, 8, 8, true),
|
||||
region(16, 8, 8, 8, true),
|
||||
region(8, 0, 8, 8),
|
||||
region(16, 0, 8, 8),
|
||||
region(8, 8, 8, 8),
|
||||
region(24, 8, 8, 8),
|
||||
);
|
||||
const BODY = faces(
|
||||
region(16, 20, 4, 12, true),
|
||||
region(28, 20, 4, 12, true),
|
||||
region(20, 16, 8, 4),
|
||||
region(28, 16, 8, 4),
|
||||
region(20, 20, 8, 12),
|
||||
region(32, 20, 8, 12),
|
||||
);
|
||||
const RIGHT_ARM = faces(
|
||||
region(40, 20, 4, 12, true),
|
||||
region(48, 20, 4, 12, true),
|
||||
region(44, 16, 4, 4),
|
||||
region(48, 16, 4, 4),
|
||||
region(44, 20, 4, 12),
|
||||
region(52, 20, 4, 12),
|
||||
);
|
||||
const LEFT_ARM = faces(
|
||||
region(32, 52, 4, 12, true),
|
||||
region(40, 52, 4, 12, true),
|
||||
region(36, 48, 4, 4),
|
||||
region(40, 48, 4, 4),
|
||||
region(36, 52, 4, 12),
|
||||
region(44, 52, 4, 12),
|
||||
);
|
||||
const RIGHT_LEG = faces(
|
||||
region(0, 20, 4, 12, true),
|
||||
region(8, 20, 4, 12, true),
|
||||
region(4, 16, 4, 4),
|
||||
region(8, 16, 4, 4),
|
||||
region(4, 20, 4, 12),
|
||||
region(12, 20, 4, 12),
|
||||
);
|
||||
const LEFT_LEG = faces(
|
||||
region(16, 52, 4, 12, true),
|
||||
region(24, 52, 4, 12, true),
|
||||
region(20, 48, 4, 4),
|
||||
region(24, 48, 4, 4),
|
||||
region(20, 52, 4, 12),
|
||||
region(28, 52, 4, 12),
|
||||
);
|
||||
|
||||
function applyUvs(geometry: THREE.BoxGeometry, regions: Faces) {
|
||||
const uv = geometry.attributes.uv;
|
||||
regions.forEach((item, faceIndex) => {
|
||||
const offset = faceIndex * 4;
|
||||
const regionLeft = item.x / ATLAS_SIZE;
|
||||
const regionRight = (item.x + item.width) / ATLAS_SIZE;
|
||||
const left = item.flipX ? regionRight : regionLeft;
|
||||
const right = item.flipX ? regionLeft : regionRight;
|
||||
const top = 1 - item.y / ATLAS_SIZE;
|
||||
const bottom = 1 - (item.y + item.height) / ATLAS_SIZE;
|
||||
uv.setXY(offset, left, top);
|
||||
uv.setXY(offset + 1, right, top);
|
||||
uv.setXY(offset + 2, left, bottom);
|
||||
uv.setXY(offset + 3, right, bottom);
|
||||
});
|
||||
uv.needsUpdate = true;
|
||||
}
|
||||
|
||||
function loadTexture(url: string) {
|
||||
return new Promise<THREE.Texture>((resolve, reject) => {
|
||||
new THREE.TextureLoader().load(
|
||||
url,
|
||||
(texture) => {
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.magFilter = THREE.NearestFilter;
|
||||
texture.minFilter = THREE.NearestFilter;
|
||||
texture.generateMipmaps = false;
|
||||
resolve(texture);
|
||||
},
|
||||
undefined,
|
||||
reject,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export class PuppetSkin {
|
||||
private readonly material: THREE.MeshStandardMaterial;
|
||||
private texture: THREE.Texture | null = null;
|
||||
private requestedHash: string | null | undefined;
|
||||
private requestId = 0;
|
||||
|
||||
constructor(
|
||||
private readonly userId: string,
|
||||
rig: PuppetRig,
|
||||
) {
|
||||
this.material = new THREE.MeshStandardMaterial({
|
||||
color: "white",
|
||||
transparent: true,
|
||||
alphaTest: 0.5,
|
||||
});
|
||||
const parts: SkinPart[] = [
|
||||
{ mesh: rig.head, faces: HEAD },
|
||||
{ mesh: rig.body, faces: BODY },
|
||||
{ mesh: rig.leftArmMesh, faces: LEFT_ARM },
|
||||
{ mesh: rig.rightArmMesh, faces: RIGHT_ARM },
|
||||
{ mesh: rig.leftLegMesh, faces: LEFT_LEG },
|
||||
{ mesh: rig.rightLegMesh, faces: RIGHT_LEG },
|
||||
];
|
||||
for (const part of parts) {
|
||||
part.mesh.material = this.material;
|
||||
applyUvs(part.mesh.geometry, part.faces);
|
||||
}
|
||||
}
|
||||
|
||||
update(skinHash: string | null) {
|
||||
if (skinHash === this.requestedHash) return;
|
||||
this.requestedHash = skinHash;
|
||||
const requestId = ++this.requestId;
|
||||
|
||||
resolveSkinSource(this.userId, skinHash)
|
||||
.then(async (source) => {
|
||||
if (requestId !== this.requestId) return;
|
||||
const texture = await loadTexture(source);
|
||||
if (requestId !== this.requestId) {
|
||||
texture.dispose();
|
||||
return;
|
||||
}
|
||||
this.texture?.dispose();
|
||||
this.texture = texture;
|
||||
this.material.map = texture;
|
||||
this.material.needsUpdate = true;
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error(`Failed to load skin for ${this.userId}`, error),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.requestId++;
|
||||
this.texture?.dispose();
|
||||
this.material.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user