AltitudeWeb/frontend/src/app/pages/particles/services/player-model.service.ts
akastijn ebe66c87c0 Rework folder structure in frontend
Pages are now grouped per group they appear in on in the header (where possible)
Utilities used by multiple pages in the project are grouped in folders such as services/pipes/etc
2025-07-04 19:50:21 +02:00

255 lines
8.4 KiB
TypeScript

import {Injectable} from '@angular/core';
import * as THREE from 'three';
import {RendererService} from './renderer.service';
@Injectable({
providedIn: 'root'
})
export class PlayerModelService {
private playerModel!: THREE.Group;
private skinTexture!: THREE.Texture;
private textureLoaded = false;
constructor(private rendererService: RendererService) {
}
/**
* Loads a Minecraft skin texture from a URL
* @param textureUrl The URL of the skin texture to load
* @returns A promise that resolves when the texture is loaded
*/
loadSkinTexture(textureUrl: string): Promise<void> {
return new Promise((resolve) => {
const loader = new THREE.TextureLoader();
loader.load(textureUrl, (texture) => {
// Set texture parameters
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
this.skinTexture = texture;
this.textureLoaded = true;
// If the player model already exists, rebuild it with textures
if (this.playerModel) {
// Remove old model
this.rendererService.scene.remove(this.playerModel);
// Create new model with textures
this.createPlayerModel();
}
resolve();
});
});
}
/**
* Creates a player model with Minecraft-style textures and adds it to the scene
*/
createPlayerModel(): THREE.Group {
this.playerModel = new THREE.Group();
if (this.textureLoaded) {
// Create textured model if texture is loaded
this.createTexturedPlayerModel();
} else {
// Create simple colored model if no texture is loaded
this.createSimplePlayerModel();
}
this.playerModel.renderOrder = 0;
this.rendererService.scene.add(this.playerModel);
return this.playerModel;
}
/**
* Creates a simple colored player model (without textures)
*/
private createSimplePlayerModel(): void {
// Head
const headGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const headMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.y = 1.35;
this.playerModel.add(head);
// Body
const bodyGeometry = new THREE.BoxGeometry(0.5, 0.7, 0.25);
const bodyMaterial = new THREE.MeshLambertMaterial({color: 0x0000ff});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.75;
this.playerModel.add(body);
// Arms
const armGeometry = new THREE.BoxGeometry(0.2, 0.7, 0.25);
const armMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
const leftArm = new THREE.Mesh(armGeometry, armMaterial);
leftArm.position.set(-0.35, 0.75, 0);
this.playerModel.add(leftArm);
const rightArm = new THREE.Mesh(armGeometry, armMaterial);
rightArm.position.set(0.35, 0.75, 0);
this.playerModel.add(rightArm);
// Legs
const legGeometry = new THREE.BoxGeometry(0.25, 0.7, 0.25);
const legMaterial = new THREE.MeshLambertMaterial({color: 0x000000});
const leftLeg = new THREE.Mesh(legGeometry, legMaterial);
leftLeg.position.set(-0.125, 0.15, 0);
this.playerModel.add(leftLeg);
const rightLeg = new THREE.Mesh(legGeometry, legMaterial);
rightLeg.position.set(0.125, 0.15, 0);
this.playerModel.add(rightLeg);
}
/**
* Creates a textured player model using the Minecraft skin
*/
private createTexturedPlayerModel(): void {
// Create the player with properly mapped textures
// Head - 8x8x8 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.5, 0.5, 0.5, // width, height, depth
[
{x: 8, y: 0, w: 8, h: 8}, // top
{x: 16, y: 0, w: 8, h: 8}, // bottom
{x: 16, y: 8, w: 8, h: 8}, // right
{x: 0, y: 8, w: 8, h: 8}, // left
{x: 8, y: 8, w: 8, h: 8}, // front
{x: 24, y: 8, w: 8, h: 8} // back
],
{x: 0, y: 1.35, z: 0} // position
));
// Body - 8x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.5, 0.7, 0.25, // width, height, depth
[
{x: 20, y: 16, w: 8, h: 4}, // top
{x: 28, y: 16, w: 8, h: 4}, // bottom
{x: 28, y: 20, w: 4, h: 12}, // right
{x: 16, y: 20, w: 4, h: 12}, // left
{x: 20, y: 20, w: 8, h: 12}, // front
{x: 32, y: 20, w: 8, h: 12} // back
],
{x: 0, y: 0.75, z: 0} // position
));
// Left Arm - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.2, 0.7, 0.25, // width, height, depth
[
{x: 44, y: 16, w: 4, h: 4}, // top
{x: 48, y: 16, w: 4, h: 4}, // bottom
{x: 48, y: 20, w: 4, h: 12}, // right
{x: 40, y: 20, w: 4, h: 12}, // left
{x: 44, y: 20, w: 4, h: 12}, // front
{x: 52, y: 20, w: 4, h: 12} // back
],
{x: -0.35, y: 0.75, z: 0} // position
));
// Right Arm - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.2, 0.7, 0.25, // width, height, depth
[
{x: 44, y: 16, w: 4, h: 4}, // top - mirror of left arm
{x: 48, y: 16, w: 4, h: 4}, // bottom - mirror of left arm
{x: 40, y: 20, w: 4, h: 12}, // right - mirror of left arm's left
{x: 48, y: 20, w: 4, h: 12}, // left - mirror of left arm's right
{x: 44, y: 20, w: 4, h: 12}, // front - same as left arm
{x: 52, y: 20, w: 4, h: 12} // back - same as left arm
],
{x: 0.35, y: 0.75, z: 0} // position
));
// Left Leg - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.26, 0.7, 0.26, // width, height, depth
[
{x: 4, y: 16, w: 4, h: 4}, // top
{x: 8, y: 16, w: 4, h: 4}, // bottom
{x: 8, y: 20, w: 4, h: 12}, // right
{x: 0, y: 20, w: 4, h: 12}, // left
{x: 4, y: 20, w: 4, h: 12}, // front
{x: 12, y: 20, w: 4, h: 12} // back
],
{x: -0.125, y: 0.15, z: 0} // position
));
// Right Leg - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.26, 0.7, 0.26, // width, height, depth
[
{x: 4, y: 16, w: 4, h: 4}, // top - mirror of left leg
{x: 8, y: 16, w: 4, h: 4}, // bottom - mirror of left leg
{x: 0, y: 20, w: 4, h: 12}, // right - mirror of left leg's left
{x: 8, y: 20, w: 4, h: 12}, // left - mirror of left leg's right
{x: 4, y: 20, w: 4, h: 12}, // front - same as left leg
{x: 12, y: 20, w: 4, h: 12} // back - same as left leg
],
{x: 0.125, y: 0.15, z: 0} // position
));
}
/**
* Creates a box with proper UV mapping for a Minecraft character part
* @param width Width of the box
* @param height Height of the box
* @param depth Depth of the box
* @param uvMapping Array of UV coordinates for each face (top, bottom, right, left, front, back)
* @param position Position of the box
* @returns THREE.Mesh with properly mapped textures
*/
private createBoxWithUvMapping(
width: number,
height: number,
depth: number,
uvMapping: Array<{ x: number, y: number, w: number, h: number }>,
position: { x: number, y: number, z: number }
): THREE.Mesh {
const geometry = new THREE.BoxGeometry(width, height, depth);
// Remap the custom face order to BoxGeometry face order: px, nx, py, ny, pz, nz
const faceOrder = [2, 3, 0, 1, 4, 5]; // right, left, top, bottom, front, back
const textureWidth = 64;
const textureHeight = 64;
const uv = geometry.attributes['uv'];
for (let i = 0; i < 6; i++) {
const face = uvMapping[faceOrder[i]];
const x1 = face.x / textureWidth;
const y1 = 1 - face.y / textureHeight;
const x2 = (face.x + face.w) / textureWidth;
const y2 = 1 - (face.y + face.h) / textureHeight;
let uvs: [number, number][] = [
[x1, y1], // top-left
[x2, y1], // top-right
[x1, y2], // bottom-left
[x2, y2] // bottom-right
];
const uvOffset = i * 8;
for (let j = 0; j < 4; j++) {
uv.array[uvOffset + j * 2] = uvs[j][0];
uv.array[uvOffset + j * 2 + 1] = uvs[j][1];
}
}
uv.needsUpdate = true;
const material = new THREE.MeshBasicMaterial({
map: this.skinTexture,
transparent: true,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(position.x, position.y, position.z);
return mesh;
}
}