skinview3d/src/model.ts

390 lines
12 KiB
TypeScript
Raw Normal View History

2020-01-26 20:39:29 +01:00
import { ModelType } from "skinview-utils";
import { BoxGeometry, DoubleSide, FrontSide, Group, Mesh, MeshBasicMaterial, Object3D, Texture, Vector2 } from "three";
2017-10-01 14:00:45 +02:00
2020-10-09 10:45:08 +02:00
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)
2017-10-01 14:00:45 +02:00
];
2020-10-09 10:45:08 +02:00
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);
2017-10-01 14:00:45 +02:00
2020-10-09 16:23:28 +02:00
box.faceVertexUvs[0] = [
[right[3], right[0], right[2]],
[right[0], right[1], right[2]],
[left[3], left[0], left[2]],
[left[0], left[1], left[2]],
[top[3], top[0], top[2]],
[top[0], top[1], top[2]],
[bottom[0], bottom[3], bottom[1]],
[bottom[3], bottom[2], bottom[1]],
[front[3], front[0], front[2]],
[front[0], front[1], front[2]],
[back[3], back[0], back[2]],
[back[0], back[1], back[2]]
];
2017-10-01 14:00:45 +02:00
}
2020-10-09 10:45:08 +02:00
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 {
2018-10-20 16:28:58 +02:00
constructor(
readonly innerLayer: Object3D,
readonly outerLayer: Object3D
2018-10-20 16:28:58 +02:00
) {
super();
2019-04-20 16:08:49 +02:00
innerLayer.name = "inner";
outerLayer.name = "outer";
}
}
export class SkinObject extends Group {
2018-07-17 20:49:00 +02:00
// body parts
readonly head: BodyPart;
readonly body: BodyPart;
readonly rightArm: BodyPart;
readonly leftArm: BodyPart;
readonly rightLeg: BodyPart;
readonly leftLeg: BodyPart;
2018-07-17 20:49:00 +02:00
private modelListeners: Array<() => void> = []; // called when model(slim property) is changed
2020-01-26 20:39:29 +01:00
private slim = false;
2018-07-17 20:49:00 +02:00
constructor(texture: Texture) {
2017-10-01 14:00:45 +02:00
super();
2020-08-31 01:38:19 +02:00
const layer1Material = new MeshBasicMaterial({
map: texture,
side: FrontSide
2020-08-31 01:38:19 +02:00
});
const layer2Material = new MeshBasicMaterial({
map: texture,
side: DoubleSide,
transparent: true,
2020-08-18 09:37:02 +02:00
alphaTest: 1e-5
2020-08-31 01:38:19 +02:00
});
2020-09-05 06:09:03 +02:00
const layer1MaterialBiased = layer1Material.clone();
layer1MaterialBiased.polygonOffset = true;
layer1MaterialBiased.polygonOffsetFactor = 1.0;
layer1MaterialBiased.polygonOffsetUnits = 1.0;
2020-08-31 01:38:19 +02:00
const layer2MaterialBiased = layer2Material.clone();
layer2MaterialBiased.polygonOffset = true;
layer2MaterialBiased.polygonOffsetFactor = 1.0;
layer2MaterialBiased.polygonOffsetUnits = 1.0;
2017-10-01 14:00:45 +02:00
// Head
2020-08-31 01:56:13 +02:00
const headBox = new BoxGeometry(8, 8, 8);
2020-10-09 10:45:08 +02:00
setSkinUVs(headBox, 0, 0, 8, 8, 8);
const headMesh = new Mesh(headBox, layer1Material);
2017-10-01 14:00:45 +02:00
2020-08-31 01:56:13 +02:00
const head2Box = new BoxGeometry(9, 9, 9);
2020-10-09 10:45:08 +02:00
setSkinUVs(head2Box, 32, 0, 8, 8, 8);
const head2Mesh = new Mesh(head2Box, layer2Material);
2017-10-01 14:00:45 +02:00
this.head = new BodyPart(headMesh, head2Mesh);
2019-04-20 16:08:49 +02:00
this.head.name = "head";
this.head.add(headMesh, head2Mesh);
this.head.position.y = 4;
2017-10-01 14:00:45 +02:00
this.add(this.head);
// Body
2020-08-31 01:56:13 +02:00
const bodyBox = new BoxGeometry(8, 12, 4);
2020-10-09 10:45:08 +02:00
setSkinUVs(bodyBox, 16, 16, 8, 12, 4);
2020-08-31 01:38:19 +02:00
const bodyMesh = new Mesh(bodyBox, layer1Material);
2017-10-01 14:00:45 +02:00
2020-10-03 14:42:22 +02:00
const body2Box = new BoxGeometry(8.5, 12.5, 4.5);
2020-10-09 10:45:08 +02:00
setSkinUVs(body2Box, 16, 32, 8, 12, 4);
2020-08-31 01:38:19 +02:00
const body2Mesh = new Mesh(body2Box, layer2Material);
2017-10-01 14:00:45 +02:00
this.body = new BodyPart(bodyMesh, body2Mesh);
2019-04-20 16:08:49 +02:00
this.body.name = "body";
this.body.add(bodyMesh, body2Mesh);
this.body.position.y = -6;
2017-10-01 14:00:45 +02:00
this.add(this.body);
// Right Arm
2020-08-31 01:56:13 +02:00
const rightArmBox = new BoxGeometry();
const rightArmMesh = new Mesh(rightArmBox, layer1Material);
2018-07-02 09:46:48 +02:00
this.modelListeners.push(() => {
rightArmMesh.scale.x = this.slim ? 3 : 4;
rightArmMesh.scale.y = 12;
rightArmMesh.scale.z = 4;
2020-10-09 10:45:08 +02:00
setSkinUVs(rightArmBox, 40, 16, this.slim ? 3 : 4, 12, 4);
2018-07-02 09:46:48 +02:00
rightArmBox.uvsNeedUpdate = true;
rightArmBox.elementsNeedUpdate = true;
});
2020-08-31 01:56:13 +02:00
const rightArm2Box = new BoxGeometry();
2020-08-31 01:38:19 +02:00
const rightArm2Mesh = new Mesh(rightArm2Box, layer2MaterialBiased);
2018-07-02 09:46:48 +02:00
this.modelListeners.push(() => {
2020-10-03 14:42:22 +02:00
rightArm2Mesh.scale.x = this.slim ? 3.5 : 4.5;
rightArm2Mesh.scale.y = 12.5;
rightArm2Mesh.scale.z = 4.5;
2020-10-09 10:45:08 +02:00
setSkinUVs(rightArm2Box, 40, 32, this.slim ? 3 : 4, 12, 4);
2018-07-02 09:46:48 +02:00
rightArm2Box.uvsNeedUpdate = true;
rightArm2Box.elementsNeedUpdate = true;
});
2017-10-01 14:00:45 +02:00
const rightArmPivot = new Group();
rightArmPivot.add(rightArmMesh, rightArm2Mesh);
2020-10-03 18:43:21 +02:00
this.modelListeners.push(() => {
rightArmPivot.position.x = this.slim ? -.5 : -1;
});
2020-05-24 06:05:53 +02:00
rightArmPivot.position.y = -4;
this.rightArm = new BodyPart(rightArmMesh, rightArm2Mesh);
2019-04-20 16:08:49 +02:00
this.rightArm.name = "rightArm";
2017-10-01 14:00:45 +02:00
this.rightArm.add(rightArmPivot);
2020-10-03 18:43:21 +02:00
this.rightArm.position.x = -5;
this.rightArm.position.y = -2;
2017-10-01 14:00:45 +02:00
this.add(this.rightArm);
// Left Arm
2020-08-31 01:56:13 +02:00
const leftArmBox = new BoxGeometry();
const leftArmMesh = new Mesh(leftArmBox, layer1Material);
2018-07-02 09:46:48 +02:00
this.modelListeners.push(() => {
leftArmMesh.scale.x = this.slim ? 3 : 4;
leftArmMesh.scale.y = 12;
leftArmMesh.scale.z = 4;
2020-10-09 10:45:08 +02:00
setSkinUVs(leftArmBox, 32, 48, this.slim ? 3 : 4, 12, 4);
2018-07-02 09:46:48 +02:00
leftArmBox.uvsNeedUpdate = true;
2018-07-17 20:49:00 +02:00
leftArmBox.elementsNeedUpdate = true;
2018-07-02 09:46:48 +02:00
});
2020-08-31 01:56:13 +02:00
const leftArm2Box = new BoxGeometry();
2020-08-31 01:38:19 +02:00
const leftArm2Mesh = new Mesh(leftArm2Box, layer2MaterialBiased);
2018-07-02 09:46:48 +02:00
this.modelListeners.push(() => {
2020-10-03 14:42:22 +02:00
leftArm2Mesh.scale.x = this.slim ? 3.5 : 4.5;
leftArm2Mesh.scale.y = 12.5;
leftArm2Mesh.scale.z = 4.5;
2020-10-09 10:45:08 +02:00
setSkinUVs(leftArm2Box, 48, 48, this.slim ? 3 : 4, 12, 4);
2018-07-02 09:46:48 +02:00
leftArm2Box.uvsNeedUpdate = true;
leftArm2Box.elementsNeedUpdate = true;
});
2017-10-01 14:00:45 +02:00
const leftArmPivot = new Group();
leftArmPivot.add(leftArmMesh, leftArm2Mesh);
2020-10-03 18:43:21 +02:00
this.modelListeners.push(() => {
leftArmPivot.position.x = this.slim ? 0.5 : 1;
});
2020-05-24 06:05:53 +02:00
leftArmPivot.position.y = -4;
this.leftArm = new BodyPart(leftArmMesh, leftArm2Mesh);
2019-04-20 16:08:49 +02:00
this.leftArm.name = "leftArm";
2017-10-01 14:00:45 +02:00
this.leftArm.add(leftArmPivot);
2020-10-03 18:43:21 +02:00
this.leftArm.position.x = 5;
this.leftArm.position.y = -2;
2017-10-01 14:00:45 +02:00
this.add(this.leftArm);
// Right Leg
2020-08-31 01:56:13 +02:00
const rightLegBox = new BoxGeometry(4, 12, 4);
2020-10-09 10:45:08 +02:00
setSkinUVs(rightLegBox, 0, 16, 4, 12, 4);
2020-09-05 06:09:03 +02:00
const rightLegMesh = new Mesh(rightLegBox, layer1MaterialBiased);
2017-10-01 14:00:45 +02:00
2020-10-03 14:42:22 +02:00
const rightLeg2Box = new BoxGeometry(4.5, 12.5, 4.5);
2020-10-09 10:45:08 +02:00
setSkinUVs(rightLeg2Box, 0, 32, 4, 12, 4);
2020-08-31 01:38:19 +02:00
const rightLeg2Mesh = new Mesh(rightLeg2Box, layer2MaterialBiased);
2017-10-01 14:00:45 +02:00
const rightLegPivot = new Group();
rightLegPivot.add(rightLegMesh, rightLeg2Mesh);
2017-10-01 14:00:45 +02:00
rightLegPivot.position.y = -6;
this.rightLeg = new BodyPart(rightLegMesh, rightLeg2Mesh);
2019-05-02 10:33:13 +02:00
this.rightLeg.name = "rightLeg";
2017-10-01 14:00:45 +02:00
this.rightLeg.add(rightLegPivot);
2020-10-03 18:43:21 +02:00
this.rightLeg.position.x = -1.9;
this.rightLeg.position.y = -12;
2020-10-03 18:43:21 +02:00
this.rightLeg.position.z = -.1;
2017-10-01 14:00:45 +02:00
this.add(this.rightLeg);
// Left Leg
2020-08-31 01:56:13 +02:00
const leftLegBox = new BoxGeometry(4, 12, 4);
2020-10-09 10:45:08 +02:00
setSkinUVs(leftLegBox, 16, 48, 4, 12, 4);
2020-09-05 06:09:03 +02:00
const leftLegMesh = new Mesh(leftLegBox, layer1MaterialBiased);
2017-10-01 14:00:45 +02:00
2020-10-03 14:42:22 +02:00
const leftLeg2Box = new BoxGeometry(4.5, 12.5, 4.5);
2020-10-09 10:45:08 +02:00
setSkinUVs(leftLeg2Box, 0, 48, 4, 12, 4);
2020-08-31 01:38:19 +02:00
const leftLeg2Mesh = new Mesh(leftLeg2Box, layer2MaterialBiased);
2017-10-01 14:00:45 +02:00
const leftLegPivot = new Group();
leftLegPivot.add(leftLegMesh, leftLeg2Mesh);
2017-10-01 14:00:45 +02:00
leftLegPivot.position.y = -6;
this.leftLeg = new BodyPart(leftLegMesh, leftLeg2Mesh);
2019-04-20 16:08:49 +02:00
this.leftLeg.name = "leftLeg";
2017-10-01 14:00:45 +02:00
this.leftLeg.add(leftLegPivot);
2020-10-03 18:43:21 +02:00
this.leftLeg.position.x = 1.9;
this.leftLeg.position.y = -12;
2020-10-03 18:43:21 +02:00
this.leftLeg.position.z = -.1;
2017-10-01 14:00:45 +02:00
this.add(this.leftLeg);
2018-07-02 09:46:48 +02:00
2020-01-26 20:39:29 +01:00
this.modelType = "default";
2018-07-02 09:46:48 +02:00
}
2020-01-26 20:39:29 +01:00
get modelType(): ModelType {
return this.slim ? "slim" : "default";
}
2018-07-02 09:46:48 +02:00
2020-01-26 20:39:29 +01:00
set modelType(value: ModelType) {
this.slim = value === "slim";
this.modelListeners.forEach(listener => listener());
}
2020-02-01 12:13:46 +01:00
private getBodyParts(): Array<BodyPart> {
2018-10-20 09:40:52 +02:00
return this.children.filter(it => it instanceof BodyPart) as Array<BodyPart>;
}
2020-02-01 12:13:46 +01:00
setInnerLayerVisible(value: boolean): void {
this.getBodyParts().forEach(part => part.innerLayer.visible = value);
}
2020-02-01 12:13:46 +01:00
setOuterLayerVisible(value: boolean): void {
this.getBodyParts().forEach(part => part.outerLayer.visible = value);
}
2017-10-02 15:29:41 +02:00
}
2017-10-01 14:00:45 +02:00
export class CapeObject extends Group {
2018-07-17 20:49:00 +02:00
readonly cape: Mesh;
2018-07-17 20:49:00 +02:00
constructor(texture: Texture) {
2017-10-01 14:00:45 +02:00
super();
2020-08-18 09:37:02 +02:00
const capeMaterial = new MeshBasicMaterial({
map: texture,
side: DoubleSide,
transparent: true,
alphaTest: 1e-5
});
2020-10-09 10:12:13 +02:00
// +z (front) - inside of cape
// -z (back) - outside of cape
2020-08-31 01:56:13 +02:00
const capeBox = new BoxGeometry(10, 16, 1);
2020-10-09 10:45:08 +02:00
setCapeUVs(capeBox, 0, 0, 10, 16, 1);
this.cape = new Mesh(capeBox, capeMaterial);
2017-10-01 14:00:45 +02:00
this.cape.position.y = -8;
2020-10-09 10:12:13 +02:00
this.cape.position.z = .5;
2017-10-01 14:00:45 +02:00
this.add(this.cape);
}
2017-10-02 15:29:41 +02:00
}
2017-10-01 14:00:45 +02:00
2018-09-22 17:37:23 +02:00
export class ElytraObject extends Group {
readonly leftWing: Group;
readonly rightWing: Group;
constructor(texture: Texture) {
super();
const elytraMaterial = new MeshBasicMaterial({
map: texture,
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, elytraMaterial);
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, elytraMaterial);
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 = .2617994;
this.leftWing.rotation.y = .01; // to avoid z-fighting
this.leftWing.rotation.z = .2617994;
this.updateRightWing();
}
/**
* Mirrors the position & rotation of left wing,
* and apply them to the right wing.
*/
updateRightWing(): void {
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;
}
}
export type BackEquipment = "cape" | "elytra";
export class PlayerObject extends Group {
2018-07-17 20:49:00 +02:00
2018-08-17 06:56:13 +02:00
readonly skin: SkinObject;
readonly cape: CapeObject;
2018-09-22 17:37:23 +02:00
readonly elytra: ElytraObject;
2018-07-17 20:49:00 +02:00
constructor(skinTexture: Texture, capeTexture: Texture) {
2017-10-01 14:00:45 +02:00
super();
this.skin = new SkinObject(skinTexture);
2019-04-20 16:08:49 +02:00
this.skin.name = "skin";
2017-10-01 14:00:45 +02:00
this.add(this.skin);
this.cape = new CapeObject(capeTexture);
2019-04-20 16:08:49 +02:00
this.cape.name = "cape";
2017-10-01 14:00:45 +02:00
this.cape.position.z = -2;
this.cape.rotation.x = 10.8 * Math.PI / 180;
2020-10-09 10:12:13 +02:00
this.cape.rotation.y = Math.PI;
2017-10-01 14:00:45 +02:00
this.add(this.cape);
2018-09-22 17:37:23 +02:00
this.elytra = new ElytraObject(capeTexture);
this.elytra.name = "elytra";
this.elytra.position.z = -2;
this.elytra.visible = false;
this.add(this.elytra);
2017-10-01 14:00:45 +02:00
}
get backEquipment(): BackEquipment | null {
if (this.cape.visible) {
return "cape";
} else if (this.elytra.visible) {
return "elytra";
} else {
return null;
}
}
set backEquipment(value: BackEquipment | null) {
this.cape.visible = value === "cape";
this.elytra.visible = value === "elytra";
}
2017-10-02 15:29:41 +02:00
}