Hi,
I am new to three.js and I really need help (and eventually code review)…
Thank you for considering my situation.
I have developed a 360°-image viewer with three.js on Angular.
It’s based on three.js equirectangular demo :
https://threejs.org/examples/?q=equirec#webgl_panorama_equirectangular
The main difficulty for me has been manage source switching (= texture switching).
The result is most of time correctly rendered on the computer I used to develop the app (both locally and online, with both Chrome and Firefox).
But from any other computer, the scene, geometry, material, mesh, texture … are loading (observed thanks to Three.js Developper Tool extension on Firefox) but nothing appears on screen.
The same same thing happens on my computer (the one I manage to view the panorama with when navigating on my website) sometimes for no reason AND systematically when I refresh the web page (instead of navigating to it).
Here is my Angular Typescript code :
import { CustomerService } from './../../../core/services/customer.service';
import { PanoramasService } from './../../services/panoramas.service';
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { Customer } from 'src/app/core/models/customer';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import * as THREE from 'three';
@Component({
selector: 'sacha-page-panorama',
templateUrl: './page-panorama.component.html',
styleUrls: ['./page-panorama.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PagePanoramaComponent implements OnInit, OnDestroy {
public container: HTMLElement;
public pageReloadSubscription: Subscription;
private customerSubscription: Subscription;
private panoramasSubscription: Subscription;
public operationId: string;
public panoramas: string[];
public selectedPanorama: string;
public loading: boolean;
public error: string;
// Are the panorama images sources switched to local ?
local: boolean = false;
// Paths to local panorama images sources
locals = [
'/assets/images/360.jpeg',
'/assets/images/063.jpeg'
]
private camera: any;
private scene: THREE.Scene;
private renderer: THREE.WebGLRenderer;
private texture: THREE.Texture;
private material: THREE.MeshBasicMaterial;
private mesh: THREE.Mesh;
private geometry: THREE.SphereBufferGeometry;
private onMouseDownMouseX: number;
private onMouseDownMouseY: number;
private isUserInteracting = false;
private lon = 0;
private onMouseDownLon = 0;
private lat = 0;
private onMouseDownLat = 0;
private phi = 0;
private theta = 0;
// Icons
public faSpinner = faSpinner;
constructor(private customerService: CustomerService, public panoramasService: PanoramasService, private route: ActivatedRoute, private cdr: ChangeDetectorRef, private router: Router) { }
public ngOnInit(): void {
// Get operation id from URL
this.route.params.subscribe(params => {
this.operationId = params['operationId'];
});
this.customerSubscription = this.customerService.customer$.subscribe((customer: Customer) => {
// Load server panorama images sources
this.panoramasService.subscribePanoramas(this.operationId);
});
this.panoramasSubscription = this.panoramasService.panoramas$.subscribe((panoramas: string[]) => {
// Occurs when server panorama images sources are loaded
this.panoramas = panoramas
if (this.panoramas) {
this.selectedPanorama = this.panoramas[0];
this.cleanObjects();
this.cleanScene();
this.init(this.selectedPanorama);
}
});
}
public ngOnDestroy(): void {
this.cleanObjects();
this.cleanScene();
this.customerSubscription.unsubscribe();
this.panoramasSubscription.unsubscribe();
}
public onPanoramaSelect(panorama: string): void {
this.selectedPanorama = panorama;
this.changeTexture(panorama)
}
private changeTexture(panorama: string): void {
this.cleanObjects();
this.loading = true;
this.error = null;
this.texture = new THREE.TextureLoader().load(panorama,
(texture) => {
this.loading = false;
this.geometry = new THREE.SphereBufferGeometry(500, 60, 40);
// invert the geometry on the x-axis so that all of the faces point inward
this.geometry.scale(- 1, 1, 1);
this.material = new THREE.MeshBasicMaterial({ map: this.texture });
this.material.map.needsUpdate = true;
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.scene.add(this.mesh);
this.cdr.detectChanges();
},
(xhr) => {
},
(error) => {
this.loading = false;
this.error = "Impossible de lire l'image !";
this.cdr.detectChanges();
});
}
// Init scene
private init(panorama: string): void {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000);
this.camera.target = new THREE.Vector3(0, 0, 0);
this.renderer = new THREE.WebGLRenderer();
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(window.innerWidth * 0.8, window.innerHeight * 0.7);
this.renderer.autoClear = true;
this.container = document.querySelector('#container');
if (this.container) {
this.container.appendChild(this.renderer.domElement);
}
this.changeTexture(panorama);
this.animate();
}
private animate(): void {
requestAnimationFrame(() => this.animate());
this.update();
}
private update(): void {
if (this.isUserInteracting === false) {
this.lon += 0.1;
}
this.lat = Math.max(- 85, Math.min(85, this.lat));
this.phi = THREE.MathUtils.degToRad(90 - this.lat);
this.theta = THREE.MathUtils.degToRad(this.lon);
this.camera.target.x = 500 * Math.sin(this.phi) * Math.cos(this.theta);
this.camera.target.y = 500 * Math.cos(this.phi);
this.camera.target.z = 500 * Math.sin(this.phi) * Math.sin(this.theta);
this.camera.lookAt(this.camera.target);
this.renderer.render(this.scene, this.camera);
}
private cleanScene() {
if (this.renderer) {
this.renderer.dispose();
}
if (this.scene) {
this.scene.dispose();
}
}
private cleanObjects(): void {
if (this.geometry) {
this.geometry.dispose();
}
if (this.material) {
this.material.dispose();
}
if (this.mesh) {
this.scene.remove(this.mesh);
}
if (this.texture) {
this.texture.dispose();
}
}
// FOLLOWING CODE TEMPORARLY COMMENTED AS CONSIDERED UNUSEFUL FOR DEBUGGING MY CURENT PROBLEM (ONLY LISTENERS ON PANORAMA AND WINDOW).
// public onDragOver(event: any): void {
// event.preventDefault();
// event.dataTransfer.dropEffect = 'copy';
// }
// public onDragCenter(): void {
// document.body.style.opacity = '0.5';
// }
// public onDragLeave(): void {
// document.body.style.opacity = '1';
// }
// public onPointerStart(event: any): void {
// this.isUserInteracting = true;
// let clientX = event.clientX || event.touches[0].clientX;
// let clientY = event.clientY || event.touches[0].clientY;
// this.onMouseDownMouseX = clientX;
// this.onMouseDownMouseY = clientY;
// this.onMouseDownLon = this.lon;
// this.onMouseDownLat = this.lat;
// }
// public onPointerMove(event: any): void {
// if (this.isUserInteracting === true) {
// let clientX = event.clientX || event.touches[0].clientX;
// let clientY = event.clientY || event.touches[0].clientY;
// this.lon = (this.onMouseDownMouseX - clientX) * 0.1 + this.onMouseDownLon;
// this.lat = (clientY - this.onMouseDownMouseY) * 0.1 + this.onMouseDownLat;
// }
// }
// public onPointerUp(): void {
// this.isUserInteracting = false;
// }
// public onDocumentMouseWheel(event: any): void {
// const fov = this.camera.fov + event.deltaY * 0.05;
// this.camera.fov = THREE.MathUtils.clamp(fov, 10, 75);
// this.camera.updateProjectionMatrix();
// }
// @HostListener('window:resize')
// private onWindowResize(): void {
// this.camera.aspect = window.innerWidth / window.innerHeight;
// this.camera.updateProjectionMatrix();
// this.renderer.setSize(window.innerWidth * 0.8, window.innerHeight * 0.7);
// }
}
And here is my Angular HTML :
<section class="row no-gutters bg-violet-cloudy py-5 px-0 px-sm-2 px-md-3 px-lg-4 px-xl-5 px-xxl-6">
<ng-container *ngIf="panoramas; else loadingPanoramas">
<div class="col-12 col-lg-1">
<ul *ngIf="panoramas.length > 1"
class="d-flex d-lg-inline-flex justify-content-center flex-row flex-lg-column">
<ng-container *ngIf="!local; else isLocal">
<li *ngFor="let panorama of panoramas; let index = index" (click)="onPanoramaSelect(panorama)"
class="btn text-white cursor-pointer m-2"
[ngClass]="panorama === selectedPanorama ? 'btn-primary' : 'btn-purple'">
api {{index + 1}}
</li>
</ng-container>
<ng-template #isLocal>
<li *ngFor="let local of locals; let index = index" (click)="onPanoramaSelect(local)"
class="btn text-white cursor-pointer m-2"
[ngClass]="local === selectedPanorama ? 'btn-primary' : 'btn-purple'">
local {{index + 1}}
</li>
</ng-template>
<li class="btn btn-warning text-white my-3"
(click)="local = !local; local ? onPanoramaSelect(locals[0]) : onPanoramaSelect(panoramas[0])">source</li>
</ul>
</div>
<div id="container" class="d-flex justify-content-center col-12 col-lg-11" (mousedown)="onPointerStart($event)"
(mousemove)="onPointerMove($event)" (mouseup)="onPointerUp()" (wheel)="onDocumentMouseWheel($event)"
(touchstart)="onPointerStart($event)" (touchmove)="onPointerMove($event)" (touchnd)="onPointerUp()"
(dragOver)="onDragOver($event)" (dragcenter)="onDragCenter()" (dragleave)="onDragLeave()"
(drop)="$event.preventDefault();">
<div *ngIf="error || loading" id="panoramaStatusWrap">
<fa-icon *ngIf="loading" [icon]="faSpinner" size="2x" [pulse]="true" class="panoramaStatus text-grey">
</fa-icon>
<div *ngIf="error" class="panoramaStatus bg-danger text-white p-2">{{error}}</div>
</div>
</div>
</ng-container>
<ng-template #loadingPanoramas>
<sacha-error-or-loading [observable]="panoramasService.panoramasError$"
(reload)="panoramasService.subscribePanoramas(operationId)" class="col-12 p-9">
</sacha-error-or-loading>
</ng-template>
</section>