import { ModelType } from "skinview-utils"; import { BoxGeometry, DoubleSide, FrontSide, Group, Mesh, MeshBasicMaterial, Object3D, Texture, Vector2 } from "three"; function toFaceVertices(x1: number, y1: number, x2: number, y2: number, w: number, h: number): Array { return [ new Vector2(x1 / w, 1.0 - y2 / h), new Vector2(x2 / w, 1.0 - y2 / h), new Vector2(x2 / w, 1.0 - y1 / h), new Vector2(x1 / w, 1.0 - y1 / h) ]; } function toSkinVertices(x1: number, y1: number, x2: number, y2: number): Array { return toFaceVertices(x1, y1, x2, y2, 64.0, 64.0); } function toCapeVertices(x1: number, y1: number, x2: number, y2: number): Array { return toFaceVertices(x1, y1, x2, y2, 64.0, 32.0); } function toElytraVertices(x1: number, y1: number, x2: number, y2: number) { return toFaceVertices(x1 + 22, y1, x2 + 22, y2, 64.0, 32.0); } function toEarVertices(x1: number, y1: number, x2: number, y2: number): Array { return toFaceVertices(x1, y1, x2, y2, 14.0, 7.0); } function setVertices(box: BoxGeometry, top: Array, bottom: Array, left: Array, front: Array, right: Array, back: Array): void { box.faceVertexUvs[0] = []; box.faceVertexUvs[0][0] = [right[3], right[0], right[2]]; box.faceVertexUvs[0][1] = [right[0], right[1], right[2]]; box.faceVertexUvs[0][2] = [left[3], left[0], left[2]]; box.faceVertexUvs[0][3] = [left[0], left[1], left[2]]; box.faceVertexUvs[0][4] = [top[3], top[0], top[2]]; box.faceVertexUvs[0][5] = [top[0], top[1], top[2]]; box.faceVertexUvs[0][6] = [bottom[0], bottom[3], bottom[1]]; box.faceVertexUvs[0][7] = [bottom[3], bottom[2], bottom[1]]; box.faceVertexUvs[0][8] = [front[3], front[0], front[2]]; box.faceVertexUvs[0][9] = [front[0], front[1], front[2]]; box.faceVertexUvs[0][10] = [back[3], back[0], back[2]]; box.faceVertexUvs[0][11] = [back[0], back[1], back[2]]; } /** * Notice that innerLayer and outerLayer may NOT be the direct children of the Group. */ export class BodyPart extends Group { constructor( readonly innerLayer: Object3D, readonly outerLayer: Object3D ) { super(); innerLayer.name = "inner"; outerLayer.name = "outer"; } } export class SkinObject extends Group { // body parts readonly head: BodyPart; readonly body: BodyPart; readonly rightArm: BodyPart; readonly leftArm: BodyPart; readonly rightLeg: BodyPart; readonly leftLeg: BodyPart; private modelListeners: Array<() => void> = []; // called when model(slim property) is changed private slim = false; constructor(texture: Texture) { super(); const layer1Material = new MeshBasicMaterial({ map: texture, side: FrontSide }); const layer2Material = new MeshBasicMaterial({ map: texture, side: DoubleSide, transparent: true, alphaTest: 1e-5 }); const layer1MaterialBiased = layer1Material.clone(); layer1MaterialBiased.polygonOffset = true; layer1MaterialBiased.polygonOffsetFactor = 1.0; layer1MaterialBiased.polygonOffsetUnits = 1.0; const layer2MaterialBiased = layer2Material.clone(); layer2MaterialBiased.polygonOffset = true; layer2MaterialBiased.polygonOffsetFactor = 1.0; layer2MaterialBiased.polygonOffsetUnits = 1.0; // Head const headBox = new BoxGeometry(8, 8, 8); setVertices(headBox, toSkinVertices(8, 0, 16, 8), toSkinVertices(16, 0, 24, 8), toSkinVertices(0, 8, 8, 16), toSkinVertices(8, 8, 16, 16), toSkinVertices(16, 8, 24, 16), toSkinVertices(24, 8, 32, 16) ); const headMesh = new Mesh(headBox, layer1Material); const head2Box = new BoxGeometry(9, 9, 9); setVertices(head2Box, toSkinVertices(40, 0, 48, 8), toSkinVertices(48, 0, 56, 8), toSkinVertices(32, 8, 40, 16), toSkinVertices(40, 8, 48, 16), toSkinVertices(48, 8, 56, 16), toSkinVertices(56, 8, 64, 16) ); const head2Mesh = new Mesh(head2Box, layer2Material); head2Mesh.renderOrder = -1; this.head = new BodyPart(headMesh, head2Mesh); this.head.name = "head"; this.head.add(headMesh, head2Mesh); this.add(this.head); // Body const bodyBox = new BoxGeometry(8, 12, 4); setVertices(bodyBox, toSkinVertices(20, 16, 28, 20), toSkinVertices(28, 16, 36, 20), toSkinVertices(16, 20, 20, 32), toSkinVertices(20, 20, 28, 32), toSkinVertices(28, 20, 32, 32), toSkinVertices(32, 20, 40, 32) ); const bodyMesh = new Mesh(bodyBox, layer1Material); const body2Box = new BoxGeometry(9, 13.5, 4.5); setVertices(body2Box, toSkinVertices(20, 32, 28, 36), toSkinVertices(28, 32, 36, 36), toSkinVertices(16, 36, 20, 48), toSkinVertices(20, 36, 28, 48), toSkinVertices(28, 36, 32, 48), toSkinVertices(32, 36, 40, 48) ); const body2Mesh = new Mesh(body2Box, layer2Material); this.body = new BodyPart(bodyMesh, body2Mesh); this.body.name = "body"; this.body.add(bodyMesh, body2Mesh); this.body.position.y = -10; this.add(this.body); // Right Arm const rightArmBox = new BoxGeometry(); const rightArmMesh = new Mesh(rightArmBox, layer1Material); this.modelListeners.push(() => { rightArmMesh.scale.x = this.slim ? 3 : 4; rightArmMesh.scale.y = 12; rightArmMesh.scale.z = 4; if (this.slim) { setVertices(rightArmBox, toSkinVertices(44, 16, 47, 20), toSkinVertices(47, 16, 50, 20), toSkinVertices(40, 20, 44, 32), toSkinVertices(44, 20, 47, 32), toSkinVertices(47, 20, 51, 32), toSkinVertices(51, 20, 54, 32) ); } else { setVertices(rightArmBox, toSkinVertices(44, 16, 48, 20), toSkinVertices(48, 16, 52, 20), toSkinVertices(40, 20, 44, 32), toSkinVertices(44, 20, 48, 32), toSkinVertices(48, 20, 52, 32), toSkinVertices(52, 20, 56, 32) ); } rightArmBox.uvsNeedUpdate = true; rightArmBox.elementsNeedUpdate = true; }); const rightArm2Box = new BoxGeometry(); const rightArm2Mesh = new Mesh(rightArm2Box, layer2MaterialBiased); rightArm2Mesh.renderOrder = 1; this.modelListeners.push(() => { rightArm2Mesh.scale.x = this.slim ? 3.375 : 4.5; rightArm2Mesh.scale.y = 13.5; rightArm2Mesh.scale.z = 4.5; if (this.slim) { setVertices(rightArm2Box, toSkinVertices(44, 32, 47, 36), toSkinVertices(47, 32, 50, 36), toSkinVertices(40, 36, 44, 48), toSkinVertices(44, 36, 47, 48), toSkinVertices(47, 36, 51, 48), toSkinVertices(51, 36, 54, 48) ); } else { setVertices(rightArm2Box, toSkinVertices(44, 32, 48, 36), toSkinVertices(48, 32, 52, 36), toSkinVertices(40, 36, 44, 48), toSkinVertices(44, 36, 48, 48), toSkinVertices(48, 36, 52, 48), toSkinVertices(52, 36, 56, 48) ); } rightArm2Box.uvsNeedUpdate = true; rightArm2Box.elementsNeedUpdate = true; }); const rightArmPivot = new Group(); rightArmPivot.add(rightArmMesh, rightArm2Mesh); rightArmPivot.position.y = -4; this.rightArm = new BodyPart(rightArmMesh, rightArm2Mesh); this.rightArm.name = "rightArm"; this.rightArm.add(rightArmPivot); this.rightArm.position.y = -6; this.modelListeners.push(() => { this.rightArm.position.x = this.slim ? -5.5 : -6; }); this.add(this.rightArm); // Left Arm const leftArmBox = new BoxGeometry(); const leftArmMesh = new Mesh(leftArmBox, layer1Material); this.modelListeners.push(() => { leftArmMesh.scale.x = this.slim ? 3 : 4; leftArmMesh.scale.y = 12; leftArmMesh.scale.z = 4; if (this.slim) { setVertices(leftArmBox, toSkinVertices(36, 48, 39, 52), toSkinVertices(39, 48, 42, 52), toSkinVertices(32, 52, 36, 64), toSkinVertices(36, 52, 39, 64), toSkinVertices(39, 52, 43, 64), toSkinVertices(43, 52, 46, 64) ); } else { setVertices(leftArmBox, toSkinVertices(36, 48, 40, 52), toSkinVertices(40, 48, 44, 52), toSkinVertices(32, 52, 36, 64), toSkinVertices(36, 52, 40, 64), toSkinVertices(40, 52, 44, 64), toSkinVertices(44, 52, 48, 64) ); } leftArmBox.uvsNeedUpdate = true; leftArmBox.elementsNeedUpdate = true; }); const leftArm2Box = new BoxGeometry(); const leftArm2Mesh = new Mesh(leftArm2Box, layer2MaterialBiased); leftArm2Mesh.renderOrder = 1; this.modelListeners.push(() => { leftArm2Mesh.scale.x = this.slim ? 3.375 : 4.5; leftArm2Mesh.scale.y = 13.5; leftArm2Mesh.scale.z = 4.5; if (this.slim) { setVertices(leftArm2Box, toSkinVertices(52, 48, 55, 52), toSkinVertices(55, 48, 58, 52), toSkinVertices(48, 52, 52, 64), toSkinVertices(52, 52, 55, 64), toSkinVertices(55, 52, 59, 64), toSkinVertices(59, 52, 62, 64) ); } else { setVertices(leftArm2Box, toSkinVertices(52, 48, 56, 52), toSkinVertices(56, 48, 60, 52), toSkinVertices(48, 52, 52, 64), toSkinVertices(52, 52, 56, 64), toSkinVertices(56, 52, 60, 64), toSkinVertices(60, 52, 64, 64) ); } leftArm2Box.uvsNeedUpdate = true; leftArm2Box.elementsNeedUpdate = true; }); const leftArmPivot = new Group(); leftArmPivot.add(leftArmMesh, leftArm2Mesh); leftArmPivot.position.y = -4; this.leftArm = new BodyPart(leftArmMesh, leftArm2Mesh); this.leftArm.name = "leftArm"; this.leftArm.add(leftArmPivot); this.leftArm.position.y = -6; this.modelListeners.push(() => { this.leftArm.position.x = this.slim ? 5.5 : 6; }); this.add(this.leftArm); // Right Leg const rightLegBox = new BoxGeometry(4, 12, 4); setVertices(rightLegBox, toSkinVertices(4, 16, 8, 20), toSkinVertices(8, 16, 12, 20), toSkinVertices(0, 20, 4, 32), toSkinVertices(4, 20, 8, 32), toSkinVertices(8, 20, 12, 32), toSkinVertices(12, 20, 16, 32) ); const rightLegMesh = new Mesh(rightLegBox, layer1MaterialBiased); const rightLeg2Box = new BoxGeometry(4.5, 13.5, 4.5); setVertices(rightLeg2Box, toSkinVertices(4, 32, 8, 36), toSkinVertices(8, 32, 12, 36), toSkinVertices(0, 36, 4, 48), toSkinVertices(4, 36, 8, 48), toSkinVertices(8, 36, 12, 48), toSkinVertices(12, 36, 16, 48) ); const rightLeg2Mesh = new Mesh(rightLeg2Box, layer2MaterialBiased); rightLeg2Mesh.renderOrder = 1; 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.y = -16; this.rightLeg.position.x = -2; this.add(this.rightLeg); // Left Leg const leftLegBox = new BoxGeometry(4, 12, 4); setVertices(leftLegBox, toSkinVertices(20, 48, 24, 52), toSkinVertices(24, 48, 28, 52), toSkinVertices(16, 52, 20, 64), toSkinVertices(20, 52, 24, 64), toSkinVertices(24, 52, 28, 64), toSkinVertices(28, 52, 32, 64) ); const leftLegMesh = new Mesh(leftLegBox, layer1MaterialBiased); const leftLeg2Box = new BoxGeometry(4.5, 13.5, 4.5); setVertices(leftLeg2Box, toSkinVertices(4, 48, 8, 52), toSkinVertices(8, 48, 12, 52), toSkinVertices(0, 52, 4, 64), toSkinVertices(4, 52, 8, 64), toSkinVertices(8, 52, 12, 64), toSkinVertices(12, 52, 16, 64) ); const leftLeg2Mesh = new Mesh(leftLeg2Box, layer2MaterialBiased); leftLeg2Mesh.renderOrder = 1; 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.y = -16; this.leftLeg.position.x = 2; this.add(this.leftLeg); this.modelType = "default"; } get modelType(): ModelType { return this.slim ? "slim" : "default"; } set modelType(value: ModelType) { this.slim = value === "slim"; this.modelListeners.forEach(listener => listener()); } private getBodyParts(): Array { return this.children.filter(it => it instanceof BodyPart) as Array; } setInnerLayerVisible(value: boolean): void { this.getBodyParts().forEach(part => part.innerLayer.visible = value); } setOuterLayerVisible(value: boolean): void { this.getBodyParts().forEach(part => part.outerLayer.visible = value); } } export class CapeObject extends Group { readonly cape: Mesh; constructor(texture: Texture) { super(); const capeMaterial = new MeshBasicMaterial({ map: texture, side: DoubleSide, transparent: true, alphaTest: 1e-5 }); // back = outside // front = inside const capeBox = new BoxGeometry(10, 16, 1); setVertices(capeBox, toCapeVertices(11, 1, 1, 0,), toCapeVertices(21, 1, 11, 0), toCapeVertices(11, 1, 12, 17), toCapeVertices(12, 1, 22, 17), toCapeVertices(0, 1, 1, 17), toCapeVertices(1, 1, 11, 17) ); this.cape = new Mesh(capeBox, capeMaterial); this.cape.position.y = -8; this.cape.position.z = -0.5; this.add(this.cape); } } export class ElytraObject extends Group { readonly leftWing: Group; readonly rightWing: Group; constructor(texture: Texture) { super(); const elytraMaterial = new MeshBasicMaterial({ map: texture, transparent: true, opacity: 1, side: DoubleSide, alphaTest: 0.5, polygonOffset: true, polygonOffsetFactor: -1.0, polygonOffsetUnits: -4.0 }); const leftWingBox = new BoxGeometry(10, 20, 2); setVertices(leftWingBox, toElytraVertices(2, 0, 12, 2), toElytraVertices(12, 0, 22, 2), toElytraVertices(0, 2, 2, 22), toElytraVertices(2, 2, 12, 22), toElytraVertices(12, 2, 14, 22), toElytraVertices(14, 2, 24, 22) ); const leftWingMesh = new Mesh(leftWingBox, elytraMaterial); leftWingMesh.position.x = -5; leftWingMesh.position.y = -10; leftWingMesh.position.z = -1; leftWingMesh.scale.x = -1.15 leftWingMesh.scale.y = 1.15; leftWingMesh.scale.z = 1.15; this.leftWing = new Group(); this.leftWing.add(leftWingMesh); this.leftWing.position.x = 3; // Left - right this.leftWing.position.y = -1.5; //Up - down this.leftWing.rotation.x = 0.2617994; this.leftWing.rotation.y = 0; this.leftWing.rotation.z = -0.19; this.add(this.leftWing); const rightWingBox = new BoxGeometry(10, 20, 2); setVertices(rightWingBox, toElytraVertices(2, 0, 12, 2), toElytraVertices(12, 0, 22, 2), toElytraVertices(0, 2, 2, 22), toElytraVertices(2, 2, 12, 22), toElytraVertices(12, 2, 14, 22), toElytraVertices(14, 2, 24, 22) ); const rightWingMesh = new Mesh(rightWingBox, elytraMaterial); rightWingMesh.position.x = 5; rightWingMesh.position.y = -10; rightWingMesh.position.z = -1; rightWingMesh.scale.x = 1.15 rightWingMesh.scale.y = 1.15; rightWingMesh.scale.z = 1.15; this.rightWing = new Group(); this.rightWing.add(rightWingMesh); this.rightWing.position.x = -3; // Left - right this.rightWing.position.y = -1.5; //Up - down this.rightWing.rotation.x = 0.2617994; //0.2617994 this.rightWing.rotation.y = 0; this.rightWing.rotation.z = 0.19; this.add(this.rightWing); } } export class EarsObject extends Group { readonly leftEar: Mesh; readonly rightEar: Mesh; constructor(texture: Texture) { super(); const earMaterial = new MeshBasicMaterial({ map: texture, side: DoubleSide, transparent: true, alphaTest: 1e-5 }); // back = outside // front = inside const earBox = new BoxGeometry(6, 6, 1); //x1: number, y1: number, x2: number, y2: number setVertices(earBox, //from look at back toEarVertices(1, 0, 7, 1), //top toEarVertices(7, 0, 13, 1), //bottom toEarVertices(0, 1, 1, 7), //right toEarVertices(1, 1, 7, 7), //front toEarVertices(0, 1, 1, 7), //left toEarVertices(8, 1, 14, 7) //back ); this.leftEar = new Mesh(earBox, earMaterial); this.leftEar.position.x = -5.5; this.leftEar.scale.x = 1.3; this.leftEar.scale.y = 1.3; this.leftEar.scale.z = 1.3; this.add(this.leftEar); this.rightEar = new Mesh(earBox, earMaterial); this.rightEar.position.x = 5.5; this.rightEar.scale.x = 1.3; this.rightEar.scale.y = 1.3; this.rightEar.scale.z = 1.3; this.add(this.rightEar); } } export class PlayerObject extends Group { readonly skin: SkinObject; readonly cape: CapeObject; readonly elytra: ElytraObject; readonly ears: EarsObject constructor(skinTexture: Texture, capeTexture: Texture, earTexture: Texture) { super(); this.skin = new SkinObject(skinTexture); this.skin.name = "skin"; this.skin.position.y = -2 this.add(this.skin); this.cape = new CapeObject(capeTexture); this.cape.name = "cape"; this.cape.position.z = -2; this.cape.position.y = -6; this.cape.rotation.x = 10 * Math.PI / 180; this.add(this.cape); this.elytra = new ElytraObject(capeTexture); this.elytra.name = "elytra"; this.elytra.position.y = -7; this.elytra.position.z = -2; this.elytra.visible = false; this.add(this.elytra); this.ears = new EarsObject(earTexture); this.ears.name = "ears"; this.ears.position.y = 3.5; this.add(this.ears); } }