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 { 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.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.25, 0.7, 0.25, // 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.25, 0.7, 0.25, // 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; } }