diff --git a/frontend/public/img/skins/akastijn.png b/frontend/public/img/skins/akastijn.png new file mode 100644 index 0000000..2b645ec Binary files /dev/null and b/frontend/public/img/skins/akastijn.png differ diff --git a/frontend/src/app/particles/components/render-container/render-container.component.ts b/frontend/src/app/particles/components/render-container/render-container.component.ts index 6fd5913..c3d5fb1 100644 --- a/frontend/src/app/particles/components/render-container/render-container.component.ts +++ b/frontend/src/app/particles/components/render-container/render-container.component.ts @@ -49,7 +49,11 @@ export class RenderContainerComponent implements AfterViewInit, OnDestroy { */ private initializeScene(): void { this.rendererService.initializeRenderer(this.rendererContainer); - this.playerModelService.createPlayerModel(); + this.playerModelService.loadSkinTexture('/public/img/skins/akastijn.png') + .then(() => { + // Then create the player model with the texture applied + this.playerModelService.createPlayerModel(); + }); this.intersectionPlaneService.createIntersectionPlane(); this.inputHandlerService.initializeInputHandlers(this.rendererService.renderer.domElement); } diff --git a/frontend/src/app/particles/services/player-model.service.ts b/frontend/src/app/particles/services/player-model.service.ts index d50112a..8f3ae77 100644 --- a/frontend/src/app/particles/services/player-model.service.ts +++ b/frontend/src/app/particles/services/player-model.service.ts @@ -1,24 +1,68 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import * as THREE from 'three'; -import { RendererService } from './renderer.service'; +import {RendererService} from './renderer.service'; -/** - * Service responsible for creating and managing the player model - */ @Injectable({ providedIn: 'root' }) export class PlayerModelService { private playerModel!: THREE.Group; + private skinTexture!: THREE.Texture; + private textureLoaded = false; - constructor(private rendererService: RendererService) {} + constructor(private rendererService: RendererService) { + } /** - * Creates a simple player model and adds it to the scene + * 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}); @@ -56,10 +100,187 @@ export class PlayerModelService { const rightLeg = new THREE.Mesh(legGeometry, legMaterial); rightLeg.position.set(0.125, 0.15, 0); this.playerModel.add(rightLeg); + } - this.rendererService.scene.add(this.playerModel); + /** + * Creates a textured player model using the Minecraft skin + */ + private createTexturedPlayerModel(): void { + // Create the player with properly mapped textures - return this.playerModel; + // 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 { + // Create box geometry + const geometry = new THREE.BoxGeometry(width, height, depth); + + // Texture dimensions (Minecraft skins are 64x64) + const textureWidth = 64; + const textureHeight = 64; + + // Calculate UV coordinates for each face + // Three.js creates 2 triangles per face, so we need to set UVs for 6 faces * 2 triangles * 3 vertices + const uvs = []; + + // Order of faces in BoxGeometry: px, nx, py, ny, pz, nz (right, left, top, bottom, front, back) + // However, our order is: top, bottom, right, left, front, back + // So we need to remap + const remappedUVs = [ + uvMapping[2], // right (px) + uvMapping[3], // left (nx) + uvMapping[0], // top (py) + uvMapping[1], // bottom (ny) + uvMapping[4], // front (pz) + uvMapping[5], // back (nz) + ]; + + // Set UVs for all faces + let faceUVs: number[][] = []; + + remappedUVs.forEach(face => { + // Calculate corner coordinates (normalized from 0-1) + 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); + + // Each face has 2 triangles with 3 vertices each + // First triangle: top-left, bottom-right, bottom-left + // Second triangle: top-left, top-right, bottom-right + + // Triangle 1 + faceUVs.push([x1, y1]); // top-left + faceUVs.push([x2, y2]); // bottom-right + faceUVs.push([x1, y2]); // bottom-left + + // Triangle 2 + faceUVs.push([x1, y1]); // top-left + faceUVs.push([x2, y1]); // top-right + faceUVs.push([x2, y2]); // bottom-right + }); + + // Flatten the UV array + const uvArray = new Float32Array(faceUVs.flat()); + + // Set the UV attribute + geometry.setAttribute('uv', new THREE.BufferAttribute(uvArray, 2)); + + // Create material with the skin texture + const material = new THREE.MeshBasicMaterial({ + map: this.skinTexture, + transparent: true, + }); + + // Create mesh and set position + const mesh = new THREE.Mesh(geometry, material); + const wireframeMaterial = new THREE.MeshBasicMaterial({ + wireframe: true, + color: 0xff0000 + }); + const wireframe = new THREE.Mesh(geometry, wireframeMaterial); + mesh.add(wireframe); + + mesh.position.set(position.x, position.y, position.z); + + return mesh; } /**