Compare commits
13
Commits
023ae809ef
...
textures
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb57289f8 | ||
|
|
60c1329163 | ||
|
|
0e71c0f581 | ||
|
|
eb67a33331 | ||
|
|
39f20796ce | ||
|
|
e00165c56f | ||
|
|
02c6497700 | ||
|
|
0efd476676 | ||
|
|
237518638c | ||
|
|
fea1a98cea | ||
|
|
ecd9b3d824 | ||
|
|
9808b5d63d | ||
|
|
c13b7077a7 |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.particles-list {
|
||||
max-height: 300px;
|
||||
height: 550px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<div #rendererContainer class="renderer-container">
|
||||
<div class="plane-controls-overlay">
|
||||
<button mat-mini-fab color="primary" (click)="togglePlaneLock()"
|
||||
[matTooltip]="isPlaneLocked ? 'Unlock Plane' : 'Lock Plane'">
|
||||
<mat-icon>{{ isPlaneLocked ? 'lock' : 'lock_open' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<div *ngIf="isPlaneLocked" class="plane-orientation-buttons">
|
||||
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_ABOVE)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_ABOVE"
|
||||
matTooltip="Vertical Above">
|
||||
<mat-icon>arrow_upward</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_BELOW)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_BELOW"
|
||||
matTooltip="Vertical Below">
|
||||
<mat-icon>arrow_downward</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_FRONT)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_FRONT"
|
||||
matTooltip="Horizontal Front">
|
||||
<mat-icon>arrow_forward</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_BEHIND)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_BEHIND"
|
||||
matTooltip="Horizontal Behind">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_RIGHT)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_RIGHT"
|
||||
matTooltip="Horizontal Right">
|
||||
<mat-icon>arrow_right</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_LEFT)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_LEFT"
|
||||
matTooltip="Horizontal Left">
|
||||
<mat-icon>arrow_left</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
.renderer-container {
|
||||
height: 1000px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative; /* Added for absolute positioning of overlay */
|
||||
}
|
||||
|
||||
.plane-controls-overlay {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.plane-orientation-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.plane-orientation-buttons button {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.plane-orientation-buttons button:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.plane-orientation-buttons button.active {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RenderContainerComponent } from './render-container.component';
|
||||
|
||||
describe('RenderContainerComponent', () => {
|
||||
let component: RenderContainerComponent;
|
||||
let fixture: ComponentFixture<RenderContainerComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RenderContainerComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RenderContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild} from '@angular/core';
|
||||
import {MatMiniFabButton} from '@angular/material/button';
|
||||
import {NgIf} from '@angular/common';
|
||||
import {IntersectionPlaneService, PlaneOrientation} from '../../services/intersection-plane.service';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatTooltip} from '@angular/material/tooltip';
|
||||
import {RendererService} from '../../services/renderer.service';
|
||||
import {PlayerModelService} from '../../services/player-model.service';
|
||||
import {InputHandlerService} from '../../services/input-handler.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-render-container',
|
||||
imports: [
|
||||
MatIcon,
|
||||
MatMiniFabButton,
|
||||
MatTooltip,
|
||||
NgIf
|
||||
],
|
||||
templateUrl: './render-container.component.html',
|
||||
styleUrl: './render-container.component.scss'
|
||||
})
|
||||
export class RenderContainerComponent implements AfterViewInit, OnDestroy {
|
||||
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
|
||||
|
||||
constructor(
|
||||
private intersectionPlaneService: IntersectionPlaneService,
|
||||
private playerModelService: PlayerModelService,
|
||||
private inputHandlerService: InputHandlerService,
|
||||
private rendererService: RendererService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.initializeScene();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (this.rendererService.renderer) {
|
||||
this.inputHandlerService.cleanup(this.rendererService.renderer.domElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the 3D scene and all related components
|
||||
*/
|
||||
private initializeScene(): void {
|
||||
this.rendererService.initializeRenderer(this.rendererContainer);
|
||||
this.playerModelService.loadSkinTexture('/public/img/skins/steve.png')
|
||||
.then(() => {
|
||||
// Then create the player model with the texture applied
|
||||
this.playerModelService.createPlayerModel();
|
||||
});
|
||||
this.intersectionPlaneService.createIntersectionPlane();
|
||||
this.inputHandlerService.initializeInputHandlers(this.rendererService.renderer.domElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation loop
|
||||
*/
|
||||
private animate(): void {
|
||||
requestAnimationFrame(this.animate.bind(this));
|
||||
this.intersectionPlaneService.updatePlaneOrientation(this.rendererService.camera);
|
||||
this.rendererService.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the plane is locked
|
||||
*/
|
||||
public get isPlaneLocked(): boolean {
|
||||
return this.intersectionPlaneService.isPlaneLocked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the plane locked state
|
||||
*/
|
||||
public togglePlaneLock(): void {
|
||||
const newLockedState = !this.isPlaneLocked;
|
||||
this.intersectionPlaneService.setPlaneLocked(newLockedState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current plane orientation
|
||||
*/
|
||||
public get currentPlaneOrientation(): PlaneOrientation {
|
||||
return this.intersectionPlaneService.getCurrentOrientation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the plane orientation
|
||||
*/
|
||||
public setPlaneOrientation(orientation: PlaneOrientation): void {
|
||||
this.intersectionPlaneService.setPlaneOrientation(orientation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available plane orientations
|
||||
*/
|
||||
public get planeOrientations(): typeof PlaneOrientation {
|
||||
return PlaneOrientation;
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,7 @@
|
||||
<app-particle-properties></app-particle-properties>
|
||||
</div>
|
||||
<div class="flex middle-column">
|
||||
<div #rendererContainer class="renderer-container">
|
||||
</div>
|
||||
<app-render-container></app-render-container>
|
||||
<div class="plane-controls">
|
||||
<label>Plane Position (Z-axis):</label>
|
||||
<mat-slider [min]="minOffset" [max]="maxOffset" step="1" #planeSlider>
|
||||
|
||||
@@ -4,17 +4,6 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.renderer-container {
|
||||
height: 1000px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.side-column {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
@@ -13,17 +13,15 @@ import {MatIconModule} from '@angular/material/icon';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
|
||||
// Services
|
||||
import {RendererService} from './services/renderer.service';
|
||||
import {PlayerModelService} from './services/player-model.service';
|
||||
import {IntersectionPlaneService} from './services/intersection-plane.service';
|
||||
import {ParticleManagerService} from './services/particle-manager.service';
|
||||
import {InputHandlerService} from './services/input-handler.service';
|
||||
|
||||
// Models
|
||||
import {PropertiesComponent} from './components/properties/properties.component';
|
||||
import {ParticleComponent} from './components/particle/particle.component';
|
||||
import {FramesComponent} from './components/frames/frames.component';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {RenderContainerComponent} from './components/render-container/render-container.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-particles',
|
||||
@@ -45,66 +43,21 @@ import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
PropertiesComponent,
|
||||
ParticleComponent,
|
||||
FramesComponent,
|
||||
RenderContainerComponent,
|
||||
],
|
||||
templateUrl: './particles.component.html',
|
||||
styleUrl: './particles.component.scss'
|
||||
})
|
||||
export class ParticlesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
|
||||
export class ParticlesComponent {
|
||||
@ViewChild('planeSlider') planeSlider!: ElementRef;
|
||||
|
||||
constructor(
|
||||
private rendererService: RendererService,
|
||||
private playerModelService: PlayerModelService,
|
||||
private intersectionPlaneService: IntersectionPlaneService,
|
||||
private particleManagerService: ParticleManagerService,
|
||||
private inputHandlerService: InputHandlerService,
|
||||
private matSnackBar: MatSnackBar,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize component
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// No initialization needed here
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Three.js scene after view is initialized
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
this.initializeScene();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Clean up event listeners
|
||||
if (this.rendererService.renderer) {
|
||||
this.inputHandlerService.cleanup(this.rendererService.renderer.domElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the 3D scene and all related components
|
||||
*/
|
||||
private initializeScene(): void {
|
||||
// Initialize renderer
|
||||
this.rendererService.initializeRenderer(this.rendererContainer);
|
||||
|
||||
// Create player model
|
||||
this.playerModelService.createPlayerModel();
|
||||
|
||||
// Create intersection plane
|
||||
this.intersectionPlaneService.createIntersectionPlane();
|
||||
|
||||
// Initialize input handlers
|
||||
this.inputHandlerService.initializeInputHandlers(this.rendererService.renderer.domElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update plane position based on slider
|
||||
*/
|
||||
@@ -133,20 +86,6 @@ export class ParticlesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return this.intersectionPlaneService.getMinOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation loop
|
||||
*/
|
||||
private animate(): void {
|
||||
requestAnimationFrame(this.animate.bind(this));
|
||||
|
||||
// Update plane orientation based on camera position
|
||||
this.intersectionPlaneService.updatePlaneOrientation(this.rendererService.camera);
|
||||
|
||||
// Render the scene
|
||||
this.rendererService.render();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate JSON output
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,7 @@ import {RendererService} from './renderer.service';
|
||||
/**
|
||||
* Represents the possible orientations of the intersection plane
|
||||
*/
|
||||
enum PlaneOrientation {
|
||||
export enum PlaneOrientation {
|
||||
VERTICAL_ABOVE,
|
||||
VERTICAL_BELOW,
|
||||
HORIZONTAL_FRONT,
|
||||
@@ -24,6 +24,7 @@ export class IntersectionPlaneService {
|
||||
private intersectionPlane!: THREE.Mesh;
|
||||
private planePosition: number = 0; // Position in 1/16th of a block
|
||||
private currentOrientation: PlaneOrientation = PlaneOrientation.HORIZONTAL_FRONT;
|
||||
private planeLocked: boolean = false;
|
||||
|
||||
constructor(private rendererService: RendererService) {
|
||||
}
|
||||
@@ -45,6 +46,7 @@ export class IntersectionPlaneService {
|
||||
// Center the plane vertically with the player (player is about 2 blocks tall)
|
||||
this.intersectionPlane.position.y = 1;
|
||||
this.rendererService.scene.add(this.intersectionPlane);
|
||||
this.intersectionPlane.renderOrder = 1;
|
||||
|
||||
return this.intersectionPlane;
|
||||
}
|
||||
@@ -76,7 +78,6 @@ export class IntersectionPlaneService {
|
||||
// Determine which quadrant the camera is in with a 45-degree offset
|
||||
const quadrant = Math.floor((cameraAngle + Math.PI + Math.PI / 4) / (Math.PI / 2)) % 4;
|
||||
|
||||
// Return the appropriate orientation based on quadrant
|
||||
switch (quadrant) {
|
||||
case 0:
|
||||
return PlaneOrientation.HORIZONTAL_FRONT;
|
||||
@@ -98,42 +99,11 @@ export class IntersectionPlaneService {
|
||||
public updatePlaneOrientation(camera: THREE.Camera): void {
|
||||
if (!this.intersectionPlane) return;
|
||||
|
||||
if (!this.planeLocked) {
|
||||
this.currentOrientation = this.determinePlaneOrientation(camera);
|
||||
|
||||
// Apply rotation and material based on orientation
|
||||
switch (this.currentOrientation) {
|
||||
case PlaneOrientation.VERTICAL_ABOVE:
|
||||
this.intersectionPlane.rotation.x = -Math.PI / 2;
|
||||
this.intersectionPlane.rotation.y = 0;
|
||||
this.updatePlaneMaterial(0xAA0000);
|
||||
break;
|
||||
case PlaneOrientation.VERTICAL_BELOW:
|
||||
this.intersectionPlane.rotation.x = Math.PI / 2;
|
||||
this.intersectionPlane.rotation.y = 0;
|
||||
this.updatePlaneMaterial(0xAA0000);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = 0;
|
||||
this.updatePlaneMaterial(0x00AA00);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = Math.PI;
|
||||
this.updatePlaneMaterial(0x00AA00);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = Math.PI / 2;
|
||||
this.updatePlaneMaterial(0x0000AA);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = -Math.PI / 2;
|
||||
this.updatePlaneMaterial(0x0000AA);
|
||||
break;
|
||||
}
|
||||
|
||||
this.updateIntersectionPlaneOrientation()
|
||||
|
||||
//Restrict plane position to the new bounds and update it
|
||||
this.planePosition = Math.max(this.getMinOffset(), Math.min(this.getMaxOffset(), this.planePosition));
|
||||
@@ -218,4 +188,69 @@ export class IntersectionPlaneService {
|
||||
public getMinOffset(): number {
|
||||
return this.getMaxOffset() * -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current plane orientation
|
||||
*/
|
||||
public getCurrentOrientation(): PlaneOrientation {
|
||||
return this.currentOrientation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the plane orientation manually
|
||||
*/
|
||||
public setPlaneOrientation(orientation: PlaneOrientation): void {
|
||||
this.currentOrientation = orientation;
|
||||
this.updateIntersectionPlaneOrientation();
|
||||
this.updatePlanePosition(this.planePosition);
|
||||
}
|
||||
|
||||
private updateIntersectionPlaneOrientation() {
|
||||
switch (this.currentOrientation) {
|
||||
case PlaneOrientation.VERTICAL_ABOVE:
|
||||
this.intersectionPlane.rotation.x = -Math.PI / 2;
|
||||
this.intersectionPlane.rotation.y = 0;
|
||||
this.updatePlaneMaterial(0xAA0000);
|
||||
break;
|
||||
case PlaneOrientation.VERTICAL_BELOW:
|
||||
this.intersectionPlane.rotation.x = Math.PI / 2;
|
||||
this.intersectionPlane.rotation.y = 0;
|
||||
this.updatePlaneMaterial(0xAA0000);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = 0;
|
||||
this.updatePlaneMaterial(0x00AA00);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = Math.PI;
|
||||
this.updatePlaneMaterial(0x00AA00);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = Math.PI / 2;
|
||||
this.updatePlaneMaterial(0x0000AA);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = -Math.PI / 2;
|
||||
this.updatePlaneMaterial(0x0000AA);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the plane orientation is locked
|
||||
*/
|
||||
public isPlaneLocked(): boolean {
|
||||
return this.planeLocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the plane orientation is locked
|
||||
*/
|
||||
public setPlaneLocked(locked: boolean): void {
|
||||
this.planeLocked = locked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,69 @@
|
||||
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<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});
|
||||
@@ -56,16 +101,154 @@ 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);
|
||||
|
||||
return this.playerModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the player model
|
||||
* Creates a textured player model using the Minecraft skin
|
||||
*/
|
||||
getPlayerModel(): THREE.Group {
|
||||
return this.playerModel;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ElementRef, Injectable } from '@angular/core';
|
||||
import {ElementRef, Injectable} from '@angular/core';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
/**
|
||||
* Service responsible for managing the Three.js rendering environment
|
||||
@@ -24,11 +24,11 @@ export class RendererService {
|
||||
|
||||
// Get container dimensions
|
||||
const containerWidth = container.nativeElement.clientWidth;
|
||||
const containerHeight = 400; // Fixed height as defined in CSS
|
||||
const containerHeight = container.nativeElement.clientHeight;
|
||||
|
||||
// Create camera
|
||||
this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000);
|
||||
this.camera.position.set(0, 1, 3);
|
||||
this.camera.position.set(-1, 2, 3);
|
||||
this.camera.lookAt(0, 1, 0);
|
||||
|
||||
// Create renderer
|
||||
@@ -45,8 +45,8 @@ export class RendererService {
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.05;
|
||||
this.controls.minDistance = 2;
|
||||
this.controls.maxDistance = 10;
|
||||
this.controls.minDistance = 0.5;
|
||||
this.controls.maxDistance = 4;
|
||||
this.controls.target.set(0, 1, 0);
|
||||
this.controls.update();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user