import { ModelType } from "skinview-utils"; import { BoxGeometry, DoubleSide, FrontSide, Group, Mesh, MeshBasicMaterial, Object3D, Texture, Vector2 } from "three"; function setUVs(box: BoxGeometry, u: number, v: number, width: number, height: number, depth: number, textureWidth: number, textureHeight: number): void { const toFaceVertices = (x1: number, y1: number, x2: number, y2: number) => [ 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); 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]]; } function setSkinUVs(box: BoxGeometry, u: number, v: number, width: number, height: number, depth: number): void { setUVs(box, u, v, width, height, depth, 64, 64); } function setCapeUVs(box: BoxGeometry, u: number, v: number, width: number, height: number, depth: number): void { setUVs(box, u, v, width, height, depth, 64, 32); } /** * 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); setSkinUVs(headBox, 0, 0, 8, 8, 8); const headMesh = new Mesh(headBox, layer1Material); const head2Box = new BoxGeometry(9, 9, 9); setSkinUVs(head2Box, 32, 0, 8, 8, 8); 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.head.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, layer1Material); const body2Box = new BoxGeometry(8.5, 12.5, 4.5); setSkinUVs(body2Box, 16, 32, 8, 12, 4); 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 = -6; 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; setSkinUVs(rightArmBox, 40, 16, this.slim ? 3 : 4, 12, 4); 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.5 : 4.5; rightArm2Mesh.scale.y = 12.5; rightArm2Mesh.scale.z = 4.5; setSkinUVs(rightArm2Box, 40, 32, this.slim ? 3 : 4, 12, 4); rightArm2Box.uvsNeedUpdate = true; rightArm2Box.elementsNeedUpdate = true; }); const rightArmPivot = new Group(); rightArmPivot.add(rightArmMesh, rightArm2Mesh); this.modelListeners.push(() => { rightArmPivot.position.x = this.slim ? -.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, layer1Material); 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); 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.5 : 4.5; leftArm2Mesh.scale.y = 12.5; leftArm2Mesh.scale.z = 4.5; setSkinUVs(leftArm2Box, 48, 48, this.slim ? 3 : 4, 12, 4); leftArm2Box.uvsNeedUpdate = true; leftArm2Box.elementsNeedUpdate = true; }); 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, layer1MaterialBiased); const rightLeg2Box = new BoxGeometry(4.5, 12.5, 4.5); setSkinUVs(rightLeg2Box, 0, 32, 4, 12, 4); 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.x = -1.9; this.rightLeg.position.y = -12; this.rightLeg.position.z = -.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, layer1MaterialBiased); const leftLeg2Box = new BoxGeometry(4.5, 12.5, 4.5); setSkinUVs(leftLeg2Box, 0, 48, 4, 12, 4); 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.x = 1.9; this.leftLeg.position.y = -12; this.leftLeg.position.z = -.1; 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 }); // +z (front) - inside of cape // -z (back) - outside of cape const capeBox = new BoxGeometry(10, 16, 1); setCapeUVs(capeBox, 0, 0, 10, 16, 1); this.cape = new Mesh(capeBox, capeMaterial); this.cape.position.y = -8; this.cape.position.z = .5; this.add(this.cape); } } export class PlayerObject extends Group { readonly skin: SkinObject; readonly cape: CapeObject; constructor(skinTexture: Texture, capeTexture: Texture) { super(); this.skin = new SkinObject(skinTexture); this.skin.name = "skin"; this.add(this.skin); this.cape = new CapeObject(capeTexture); this.cape.name = "cape"; this.cape.position.z = -2; this.cape.rotation.x = 10.8 * Math.PI / 180; this.cape.rotation.y = Math.PI; this.add(this.cape); } }