UNPKG

skin3d

Version:

A fast, customizable Minecraft skin viewer powered by Three.js. Easily render and preview Minecraft skins in 3D for your projects.

575 lines 21.1 kB
import { BoxGeometry, BufferAttribute, DoubleSide, FrontSide, Group, Mesh, MeshStandardMaterial, Object3D, Texture, Vector2, } from "three"; /** * Set the UV mapping for a box geometry. */ function setUVs(box, u, v, width, height, depth, textureWidth, textureHeight) { const toFaceVertices = (x1, y1, x2, y2) => [ new Vector2(x1 / textureWidth, 1.0 - y2 / textureHeight), new Vector2(x2 / textureWidth, 1.0 - y2 / textureHeight), new Vector2(x2 / textureWidth, 1.0 - y1 / textureHeight), new Vector2(x1 / textureWidth, 1.0 - y1 / textureHeight), ]; const top = toFaceVertices(u + depth, v, u + width + depth, v + depth); const bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth); const left = toFaceVertices(u, v + depth, u + depth, v + depth + height); const front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height); const right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth); const back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth); const uvAttr = box.attributes.uv; const uvRight = [right[3], right[2], right[0], right[1]]; const uvLeft = [left[3], left[2], left[0], left[1]]; const uvTop = [top[3], top[2], top[0], top[1]]; const uvBottom = [bottom[0], bottom[1], bottom[3], bottom[2]]; const uvFront = [front[3], front[2], front[0], front[1]]; const uvBack = [back[3], back[2], back[0], back[1]]; const newUVData = []; for (const uvArray of [uvRight, uvLeft, uvTop, uvBottom, uvFront, uvBack]) { for (const uv of uvArray) { newUVData.push(uv.x, uv.y); } } uvAttr.set(new Float32Array(newUVData)); uvAttr.needsUpdate = true; } /** Set UVs for a skin box (64x64 texture). */ function setSkinUVs(box, u, v, width, height, depth) { setUVs(box, u, v, width, height, depth, 64, 64); } /** Set UVs for a cape box (64x32 texture). */ function setCapeUVs(box, u, v, width, height, depth) { setUVs(box, u, v, width, height, depth, 64, 32); } /** * Represents a body part with an inner and outer layer. */ export class BodyPart extends Group { constructor(innerLayer, outerLayer) { super(); Object.defineProperty(this, "innerLayer", { enumerable: true, configurable: true, writable: true, value: innerLayer }); Object.defineProperty(this, "outerLayer", { enumerable: true, configurable: true, writable: true, value: outerLayer }); innerLayer.name = "inner"; outerLayer.name = "outer"; } } /** * Represents the player's skin model, including all body parts. */ export class SkinObject extends Group { constructor() { super(); Object.defineProperty(this, "head", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "body", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "rightArm", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "leftArm", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "rightLeg", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "leftLeg", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "modelListeners", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "slim", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "_map", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "layer1Material", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "layer1MaterialBiased", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "layer2Material", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "layer2MaterialBiased", { enumerable: true, configurable: true, writable: true, value: void 0 }); // Main materials for skin layers this.layer1Material = new MeshStandardMaterial({ side: FrontSide }); this.layer2Material = new MeshStandardMaterial({ side: DoubleSide, transparent: true, alphaTest: 1e-5 }); this.layer1MaterialBiased = this.layer1Material.clone(); this.layer1MaterialBiased.polygonOffset = true; this.layer1MaterialBiased.polygonOffsetFactor = 1.0; this.layer1MaterialBiased.polygonOffsetUnits = 1.0; this.layer2MaterialBiased = this.layer2Material.clone(); this.layer2MaterialBiased.polygonOffset = true; this.layer2MaterialBiased.polygonOffsetFactor = 1.0; this.layer2MaterialBiased.polygonOffsetUnits = 1.0; // Head const headBox = new BoxGeometry(8, 8, 8); setSkinUVs(headBox, 0, 0, 8, 8, 8); const headMesh = new Mesh(headBox, this.layer1Material); const head2Box = new BoxGeometry(9, 9, 9); setSkinUVs(head2Box, 32, 0, 8, 8, 8); const head2Mesh = new Mesh(head2Box, this.layer2Material); this.head = new BodyPart(headMesh, head2Mesh); this.head.name = "head"; this.head.add(headMesh, head2Mesh); headMesh.position.y = 4; head2Mesh.position.y = 4; this.add(this.head); // Body const bodyBox = new BoxGeometry(8, 12, 4); setSkinUVs(bodyBox, 16, 16, 8, 12, 4); const bodyMesh = new Mesh(bodyBox, this.layer1Material); const body2Box = new BoxGeometry(8.5, 12.5, 4.5); setSkinUVs(body2Box, 16, 32, 8, 12, 4); const body2Mesh = new Mesh(body2Box, this.layer2Material); this.body = new BodyPart(bodyMesh, body2Mesh); this.body.name = "body"; this.body.add(bodyMesh, body2Mesh); this.body.position.y = -6; this.add(this.body); // Right Arm const rightArmBox = new BoxGeometry(); const rightArmMesh = new Mesh(rightArmBox, this.layer1MaterialBiased); this.modelListeners.push(() => { rightArmMesh.scale.x = this.slim ? 3 : 4; rightArmMesh.scale.y = 12; rightArmMesh.scale.z = 4; setSkinUVs(rightArmBox, 40, 16, this.slim ? 3 : 4, 12, 4); }); const rightArm2Box = new BoxGeometry(); const rightArm2Mesh = new Mesh(rightArm2Box, this.layer2MaterialBiased); this.modelListeners.push(() => { rightArm2Mesh.scale.x = this.slim ? 3.5 : 4.5; rightArm2Mesh.scale.y = 12.5; rightArm2Mesh.scale.z = 4.5; setSkinUVs(rightArm2Box, 40, 32, this.slim ? 3 : 4, 12, 4); }); const rightArmPivot = new Group(); rightArmPivot.add(rightArmMesh, rightArm2Mesh); this.modelListeners.push(() => { rightArmPivot.position.x = this.slim ? -0.5 : -1; }); rightArmPivot.position.y = -4; this.rightArm = new BodyPart(rightArmMesh, rightArm2Mesh); this.rightArm.name = "rightArm"; this.rightArm.add(rightArmPivot); this.rightArm.position.x = -5; this.rightArm.position.y = -2; this.add(this.rightArm); // Left Arm const leftArmBox = new BoxGeometry(); const leftArmMesh = new Mesh(leftArmBox, this.layer1MaterialBiased); this.modelListeners.push(() => { leftArmMesh.scale.x = this.slim ? 3 : 4; leftArmMesh.scale.y = 12; leftArmMesh.scale.z = 4; setSkinUVs(leftArmBox, 32, 48, this.slim ? 3 : 4, 12, 4); }); const leftArm2Box = new BoxGeometry(); const leftArm2Mesh = new Mesh(leftArm2Box, this.layer2MaterialBiased); this.modelListeners.push(() => { leftArm2Mesh.scale.x = this.slim ? 3.5 : 4.5; leftArm2Mesh.scale.y = 12.5; leftArm2Mesh.scale.z = 4.5; setSkinUVs(leftArm2Box, 48, 48, this.slim ? 3 : 4, 12, 4); }); const leftArmPivot = new Group(); leftArmPivot.add(leftArmMesh, leftArm2Mesh); this.modelListeners.push(() => { leftArmPivot.position.x = this.slim ? 0.5 : 1; }); leftArmPivot.position.y = -4; this.leftArm = new BodyPart(leftArmMesh, leftArm2Mesh); this.leftArm.name = "leftArm"; this.leftArm.add(leftArmPivot); this.leftArm.position.x = 5; this.leftArm.position.y = -2; this.add(this.leftArm); // Right Leg const rightLegBox = new BoxGeometry(4, 12, 4); setSkinUVs(rightLegBox, 0, 16, 4, 12, 4); const rightLegMesh = new Mesh(rightLegBox, this.layer1MaterialBiased); const rightLeg2Box = new BoxGeometry(4.5, 12.5, 4.5); setSkinUVs(rightLeg2Box, 0, 32, 4, 12, 4); const rightLeg2Mesh = new Mesh(rightLeg2Box, this.layer2MaterialBiased); const rightLegPivot = new Group(); rightLegPivot.add(rightLegMesh, rightLeg2Mesh); rightLegPivot.position.y = -6; this.rightLeg = new BodyPart(rightLegMesh, rightLeg2Mesh); this.rightLeg.name = "rightLeg"; this.rightLeg.add(rightLegPivot); this.rightLeg.position.x = -1.9; this.rightLeg.position.y = -12; this.rightLeg.position.z = -0.1; this.add(this.rightLeg); // Left Leg const leftLegBox = new BoxGeometry(4, 12, 4); setSkinUVs(leftLegBox, 16, 48, 4, 12, 4); const leftLegMesh = new Mesh(leftLegBox, this.layer1MaterialBiased); const leftLeg2Box = new BoxGeometry(4.5, 12.5, 4.5); setSkinUVs(leftLeg2Box, 0, 48, 4, 12, 4); const leftLeg2Mesh = new Mesh(leftLeg2Box, this.layer2MaterialBiased); const leftLegPivot = new Group(); leftLegPivot.add(leftLegMesh, leftLeg2Mesh); leftLegPivot.position.y = -6; this.leftLeg = new BodyPart(leftLegMesh, leftLeg2Mesh); this.leftLeg.name = "leftLeg"; this.leftLeg.add(leftLegPivot); this.leftLeg.position.x = 1.9; this.leftLeg.position.y = -12; this.leftLeg.position.z = -0.1; this.add(this.leftLeg); this.modelType = "default"; } /** The texture map for the skin. */ get map() { return this._map; } set map(newMap) { this._map = newMap; this.layer1Material.map = newMap; this.layer1Material.needsUpdate = true; this.layer1MaterialBiased.map = newMap; this.layer1MaterialBiased.needsUpdate = true; this.layer2Material.map = newMap; this.layer2Material.needsUpdate = true; this.layer2MaterialBiased.map = newMap; this.layer2MaterialBiased.needsUpdate = true; } /** The model type ("default" or "slim"). */ get modelType() { return this.slim ? "slim" : "default"; } set modelType(value) { this.slim = value === "slim"; this.modelListeners.forEach(listener => listener()); } /** Get all body parts in this skin. */ getBodyParts() { return this.children.filter(it => it instanceof BodyPart); } /** Show or hide the inner layer of all body parts. */ setInnerLayerVisible(value) { this.getBodyParts().forEach(part => (part.innerLayer.visible = value)); } /** Show or hide the outer layer of all body parts. */ setOuterLayerVisible(value) { this.getBodyParts().forEach(part => (part.outerLayer.visible = value)); } /** Reset all joint rotations and positions to default. */ resetJoints() { this.head.rotation.set(0, 0, 0); this.leftArm.rotation.set(0, 0, 0); this.rightArm.rotation.set(0, 0, 0); this.leftLeg.rotation.set(0, 0, 0); this.rightLeg.rotation.set(0, 0, 0); this.body.rotation.set(0, 0, 0); this.head.position.y = 0; this.body.position.y = -6; this.body.position.z = 0; this.rightArm.position.x = -5; this.rightArm.position.y = -2; this.rightArm.position.z = 0; this.leftArm.position.x = 5; this.leftArm.position.y = -2; this.leftArm.position.z = 0; this.rightLeg.position.x = -1.9; this.rightLeg.position.y = -12; this.rightLeg.position.z = -0.1; this.leftLeg.position.x = 1.9; this.leftLeg.position.y = -12; this.leftLeg.position.z = -0.1; } } /** * Represents a Minecraft-style cape. */ export class CapeObject extends Group { constructor() { super(); Object.defineProperty(this, "cape", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "material", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.material = new MeshStandardMaterial({ side: DoubleSide, transparent: true, alphaTest: 1e-5, }); const capeBox = new BoxGeometry(10, 16, 1); setCapeUVs(capeBox, 0, 0, 10, 16, 1); this.cape = new Mesh(capeBox, this.material); this.cape.position.y = -8; this.cape.position.z = 0.5; this.add(this.cape); } get map() { return this.material.map; } set map(newMap) { this.material.map = newMap; this.material.needsUpdate = true; } } /** * Represents a Minecraft-style elytra (wings). */ export class ElytraObject extends Group { constructor() { super(); Object.defineProperty(this, "leftWing", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "rightWing", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "material", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.material = new MeshStandardMaterial({ side: DoubleSide, transparent: true, alphaTest: 1e-5, }); const leftWingBox = new BoxGeometry(12, 22, 4); setCapeUVs(leftWingBox, 22, 0, 10, 20, 2); const leftWingMesh = new Mesh(leftWingBox, this.material); leftWingMesh.position.x = -5; leftWingMesh.position.y = -10; leftWingMesh.position.z = -1; this.leftWing = new Group(); this.leftWing.add(leftWingMesh); this.add(this.leftWing); const rightWingBox = new BoxGeometry(12, 22, 4); setCapeUVs(rightWingBox, 22, 0, 10, 20, 2); const rightWingMesh = new Mesh(rightWingBox, this.material); rightWingMesh.scale.x = -1; rightWingMesh.position.x = 5; rightWingMesh.position.y = -10; rightWingMesh.position.z = -1; this.rightWing = new Group(); this.rightWing.add(rightWingMesh); this.add(this.rightWing); this.leftWing.position.x = 5; this.leftWing.rotation.x = 0.2617994; this.resetJoints(); } /** Reset wing rotations to default. */ resetJoints() { this.leftWing.rotation.y = 0.01; // avoid z-fighting this.leftWing.rotation.z = 0.2617994; this.updateRightWing(); } /** * Mirror the left wing's position and rotation to the right wing. */ updateRightWing() { this.rightWing.position.x = -this.leftWing.position.x; this.rightWing.position.y = this.leftWing.position.y; this.rightWing.rotation.x = this.leftWing.rotation.x; this.rightWing.rotation.y = -this.leftWing.rotation.y; this.rightWing.rotation.z = -this.leftWing.rotation.z; } get map() { return this.material.map; } set map(newMap) { this.material.map = newMap; this.material.needsUpdate = true; } } /** * Represents a pair of ears (for skin with ears). */ export class EarsObject extends Group { constructor() { super(); Object.defineProperty(this, "rightEar", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "leftEar", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "material", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.material = new MeshStandardMaterial({ side: FrontSide }); const earBox = new BoxGeometry(8, 8, 4 / 3); setUVs(earBox, 0, 0, 6, 6, 1, 14, 7); this.rightEar = new Mesh(earBox, this.material); this.rightEar.name = "rightEar"; this.rightEar.position.x = -6; this.add(this.rightEar); this.leftEar = new Mesh(earBox, this.material); this.leftEar.name = "leftEar"; this.leftEar.position.x = 6; this.add(this.leftEar); } get map() { return this.material.map; } set map(newMap) { this.material.map = newMap; this.material.needsUpdate = true; } } const CapeDefaultAngle = (10.8 * Math.PI) / 180; /** * Represents a full player model, including skin, cape, elytra, and ears. */ export class PlayerObject extends Group { constructor() { super(); Object.defineProperty(this, "skin", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "cape", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "elytra", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "ears", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.skin = new SkinObject(); this.skin.name = "skin"; this.skin.position.y = 8; this.add(this.skin); this.cape = new CapeObject(); this.cape.name = "cape"; this.cape.position.y = 8; this.cape.position.z = -2; this.cape.rotation.x = CapeDefaultAngle; this.cape.rotation.y = Math.PI; this.add(this.cape); this.elytra = new ElytraObject(); this.elytra.name = "elytra"; this.elytra.position.y = 8; this.elytra.position.z = -2; this.elytra.visible = false; this.add(this.elytra); this.ears = new EarsObject(); this.ears.name = "ears"; this.ears.position.y = 10; this.ears.position.z = 2 / 3; this.ears.visible = false; this.skin.head.add(this.ears); } /** Which back equipment is visible ("cape", "elytra", or null). */ get backEquipment() { if (this.cape.visible) return "cape"; if (this.elytra.visible) return "elytra"; return null; } set backEquipment(value) { this.cape.visible = value === "cape"; this.elytra.visible = value === "elytra"; } /** Reset all joints and positions to default. */ resetJoints() { this.skin.resetJoints(); this.cape.rotation.x = CapeDefaultAngle; this.cape.position.y = 8; this.cape.position.z = -2; this.elytra.position.y = 8; this.elytra.position.z = -2; this.elytra.rotation.x = 0; this.elytra.resetJoints(); } } //# sourceMappingURL=model.js.map