|
import { |
|
Component, |
|
ElementRef, |
|
EventEmitter, |
|
HostListener, |
|
OnInit, |
|
Output, |
|
ViewChild, |
|
Input |
|
} from '@angular/core'; |
|
import { Object3D, TextureLoader, Group, TGALoader, MeshPhongMaterial, CanvasTexture, Vector3 } from 'three'; |
|
import * as THREE from 'three'; |
|
import './js/EnableThreeExamples'; |
|
import 'three/examples/js/loaders/OBJLoader'; |
|
import 'three/examples/js/loaders/MTLLoader'; |
|
import 'three/examples/js/loaders/TDSLoader'; |
|
import 'three/examples/js/loaders/TGALoader'; |
|
import 'three/examples/js/controls/OrbitControls'; |
|
import 'three/examples/js/controls/TrackballControls.js'; |
|
|
|
import { ThreeDModel } from '../models/3d.model'; |
|
|
|
|
|
@Component({ |
|
selector: 'app-scene', |
|
templateUrl: './app-scene.component.html', |
|
styleUrls: ['./app-scene.component.css'] |
|
}) |
|
export class AppSceneComponent implements OnInit { |
|
|
|
_selectedModel: ThreeDModel; |
|
@Input() |
|
set selectedModel(value: ThreeDModel) { |
|
this._selectedModel = value; |
|
this.changeModel(); |
|
} |
|
get selectedModel(): ThreeDModel { |
|
return this._selectedModel; |
|
} |
|
@Input() selectedLogo: ThreeDModel; |
|
@Input() imageSize: number; |
|
@Output() clickIntersect = new EventEmitter<Object3D>(); |
|
@ViewChild('canvas') private canvasRef: ElementRef; |
|
|
|
private renderer: THREE.WebGLRenderer; |
|
private camera: THREE.PerspectiveCamera; |
|
public advertCanvas: HTMLCanvasElement; |
|
public scene: THREE.Scene; |
|
public controls: THREE.OrbitControls; |
|
private currentObject: Object3D; |
|
private object: Object3D; |
|
private loader; |
|
public color: THREE.Color; |
|
public texture: THREE.CanvasTexture; |
|
private normalLoader: TextureLoader; |
|
private textureLoader: TGALoader; |
|
|
|
public fieldOfView = 60; |
|
public nearClippingPane = 1; |
|
public farClippingPane = 1100; |
|
public canvasSize = 4096; |
|
private animationTimer; |
|
backgroundScene: THREE.Scene; |
|
backgroundCamera: THREE.Camera; |
|
|
|
/* LIFECYCLE */ |
|
ngOnInit() { |
|
window['a'] = this; |
|
this.createBackground(); |
|
this.createCamera(); |
|
this.createLight(); |
|
this.startRendering(); |
|
this.addControls(); |
|
} |
|
|
|
createBackground(): any { |
|
const texture = THREE.ImageUtils.loadTexture('assets/background.jpg'); |
|
const backgroundMesh = new THREE.Mesh( |
|
new THREE.PlaneGeometry(4, 4, 0), |
|
new THREE.MeshBasicMaterial({ |
|
map: texture |
|
})); |
|
|
|
(backgroundMesh .material as any).depthTest = false; |
|
(backgroundMesh .material as any).depthWrite = false; |
|
|
|
// Create your background scene |
|
this.backgroundScene = new THREE.Scene(); |
|
this.backgroundCamera = new THREE.Camera(); |
|
this.backgroundScene.add(this.backgroundCamera); |
|
this.backgroundScene.add(backgroundMesh); |
|
} |
|
|
|
private get canvas(): HTMLCanvasElement { |
|
return this.canvasRef.nativeElement; |
|
} |
|
|
|
private changeModel() { |
|
if (!this.selectedModel) { |
|
return; |
|
} |
|
this.initNewScene(); |
|
let obj; |
|
if (obj = this.scene.getObjectByName('currObj')) { |
|
obj.parent.remove(obj); |
|
this.animationTimer = null; |
|
this.render(); |
|
} |
|
// if (this.selectedModel.name === 'Astronaut') { |
|
// this.animationTimer = null; |
|
// this.createSce(); |
|
// return; |
|
// } |
|
if (this.selectedModel.name === 'Astronaut') { |
|
this.animationTimer = null; |
|
this.createAstronaut2(); |
|
return; |
|
} |
|
if (this.selectedModel.name === 'Arian-1') { |
|
this.animationTimer = null; |
|
this.createRocket(); |
|
return; |
|
} |
|
this.createScene(); |
|
} |
|
|
|
private createRocket() { |
|
let texture; |
|
const manager = new THREE.LoadingManager(() => { }); |
|
this.textureLoader = new THREE.TextureLoader(manager); |
|
texture = this.textureLoader.load('assets/rocket1/rocket.jpg'); |
|
|
|
this.loader = new (THREE as any).MTLLoader(); |
|
this.loader.setPath(`assets/rocket1/`); |
|
this.loader.load(`12217_rocket_v1_l1.mtl`, e => { |
|
e.preload(); |
|
new (THREE as any).OBJLoader() |
|
.setPath('assets/rocket1/') |
|
.load('12217_rocket_v1_l1.obj', (object) => { |
|
this.object = object; |
|
window['r'] = this.object; |
|
this.object.position.set(0, -5.5, 0); |
|
this.object.scale.set(0.01, 0.01, 0.01); |
|
this.object.receiveShadow = false; |
|
this.animationTimer = this.startObjectAnimation(this.object); |
|
// this.camera.lookAt(this.object.position); |
|
this.object.traverse(child => { |
|
this.addCanvasTexture(child, this.object, texture); |
|
}); |
|
this.object.name = 'currObj'; |
|
this.scene.add(this.object); |
|
this.render(); |
|
}, x => { }, err => { }); |
|
}); |
|
} |
|
|
|
private createSce() { |
|
this.loader = new (THREE as any).TDSLoader(); |
|
this.loader.setResourcePath(`assets/astronaut/textures/`); |
|
this.loader.load(`assets/astronaut/astronaut.3ds`, e => this.onAstroLoading(e)); |
|
this.textureLoader = new THREE.TGALoader(); |
|
this.render(); |
|
} |
|
|
|
private createAstronaut2() { |
|
new (THREE as any).OBJLoader() |
|
.setPath('assets/astronaut2/') |
|
.load('astronaut2.obj', (object) => { |
|
this.object = object; |
|
window['e'] = this.object; |
|
this.object.traverse(child => { |
|
this.addCanvasTexture(child, this.object, texture); |
|
}); |
|
this.object.position.set(0, -7, 0); |
|
this.object.receiveShadow = false; |
|
this.object.scale.set(8, 8, 8); |
|
this.animationTimer = this.startObjectAnimation(this.object); |
|
this.object.name = 'currObj'; |
|
this.scene.add(this.object); |
|
this.render(); |
|
}, x => { }, err => { }); |
|
let texture; |
|
const manager = new THREE.LoadingManager(() => { }); |
|
this.textureLoader = new THREE.TextureLoader(manager); |
|
texture = this.textureLoader.load('assets/astronaut2/z2_Color_s.jpg'); |
|
|
|
// this.loader = new (THREE as any).MTLLoader(); |
|
// this.loader.setPath(`assets/astronaut2/`); |
|
// this.loader.load(`astronaut2.mtl`, e => { |
|
// e.preload(); |
|
|
|
// }); |
|
} |
|
|
|
startObjectAnimation(object: Object3D) { |
|
return setInterval(() => { |
|
object.rotateY(0.1); |
|
this.render(); |
|
}, 100); |
|
} |
|
|
|
private onAstroLoading(collada: Group) { |
|
collada.children.forEach((e: THREE.Mesh) => { |
|
const text = this.textureLoader.load('assets/astronaut/textures/astnt1_1.tga'); |
|
this.addCanvasTexture(e, this.object, text); |
|
}); |
|
collada.name = 'currObj'; |
|
this.scene.add(collada); |
|
} |
|
|
|
private createScene() { |
|
this.loader = new (THREE as any).TDSLoader(); |
|
this.normalLoader = new TextureLoader(); |
|
const normal = this.normalLoader.load(`assets/${this.selectedModel.path}/textures/${this.selectedModel.normal}`); |
|
|
|
this.loader.setResourcePath(`assets/${this.selectedModel.path}/textures/`); |
|
this.loader.load(`assets/${this.selectedModel.path}/${this.selectedModel.path}.3ds`, |
|
e => this.onModelLoadingCompleted(e, normal)); |
|
} |
|
|
|
private initNewScene() { |
|
this.scene = new THREE.Scene(); |
|
// this.scene.add(new THREE.AxesHelper(200)); |
|
const light = new THREE.HemisphereLight(); |
|
this.scene.add(light); |
|
if (this.camera) { |
|
this.scene.add(this.camera); |
|
} |
|
} |
|
|
|
private onModelLoadingCompleted(collada, normal) { |
|
this.currentObject = collada; |
|
this.currentObject.traverse(object => { |
|
this.addCanvasTexture(object, this.currentObject, normal, true); |
|
}); |
|
this.currentObject.receiveShadow = false; |
|
this.currentObject.name = 'currObj'; |
|
this.scene.add(this.currentObject); |
|
this.animationTimer = this.startObjectAnimation(this.currentObject); |
|
this.render(); |
|
} |
|
|
|
private setupCanvas(): void { |
|
this.advertCanvas = document.createElement('canvas'); |
|
this.advertCanvas.width = this.canvasSize; |
|
this.advertCanvas.height = this.canvasSize; |
|
this.color = new THREE.Color(); |
|
this.texture = new THREE.CanvasTexture(this.advertCanvas); |
|
} |
|
|
|
private addCanvasTexture(object, currentObject, normal, isNormal = false): void { |
|
this.setupCanvas(); |
|
const child = object as any; |
|
|
|
if (!object.isMesh && !child.material) { |
|
return; |
|
} |
|
|
|
isNormal ? child.material.normalMap = normal : child.material.map = normal; |
|
child.material.lightMap = normal; |
|
|
|
currentObject.add(new THREE.Mesh(child.geometry, new MeshPhongMaterial({ |
|
map: this.texture, |
|
alphaTest: 0.5 |
|
}))); |
|
} |
|
|
|
private createLight() { |
|
// var directionalLight = new THREE.DirectionalLight( 0xffeedd ); |
|
// directionalLight.position.set( 0, 0, 2 ); |
|
// this.scene.add( directionalLight ); |
|
} |
|
|
|
private createCamera() { |
|
const aspectRatio = this.getAspectRatio(); |
|
this.camera = new THREE.PerspectiveCamera( |
|
this.fieldOfView, |
|
aspectRatio, |
|
this.nearClippingPane, |
|
this.farClippingPane |
|
); |
|
|
|
|
|
/** |
|
* x: -13.51642127467603 |
|
y: 0.20744722870848967 |
|
z: -19.82593506593228 |
|
*/ |
|
// Set position and look at |
|
this.camera.position.x = -10; |
|
this.camera.position.y = 0.75; |
|
this.camera.position.z = -17; |
|
this.camera.rotation.x = -2; |
|
this.camera.rotation.y = 0; |
|
this.camera.rotation.z = -2; |
|
} |
|
|
|
private getAspectRatio(): number { |
|
const height = this.canvas.clientHeight; |
|
if (height === 0) { |
|
return 0; |
|
} |
|
return this.canvas.clientWidth / this.canvas.clientHeight; |
|
} |
|
|
|
private startRendering() { |
|
this.renderer = new THREE.WebGLRenderer({ |
|
canvas: this.canvas, |
|
antialias: true |
|
}); |
|
this.renderer.setPixelRatio(devicePixelRatio); |
|
this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight); |
|
|
|
this.renderer.shadowMap.enabled = true; |
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; |
|
this.renderer.setClearColor(0x1B1B1B, 1); |
|
this.renderer.autoClear = true; |
|
|
|
this.render(); |
|
} |
|
|
|
public render() { |
|
if (this.renderer && this.scene) { |
|
this.renderer.autoClear = false; |
|
this.renderer.clear(); |
|
this.renderer.render(this.backgroundScene , this.backgroundCamera); |
|
this.renderer.render(this.scene, this.camera); |
|
} |
|
} |
|
|
|
public addControls() { |
|
this.controls = new THREE.OrbitControls(this.camera); |
|
this.controls.rotateSpeed = 1.0; |
|
this.controls.zoomSpeed = 1.2; |
|
this.controls.addEventListener('change', () => this.render()); |
|
} |
|
|
|
/* EVENTS */ |
|
|
|
public onClick(event: MouseEvent) { |
|
// Example of mesh selection/pick: |
|
const raycaster = new THREE.Raycaster(); |
|
const mouse = new THREE.Vector2(); |
|
mouse.x = ((event.clientX - 300) / this.renderer.domElement.clientWidth) * 2 - 1; |
|
mouse.y = - (event.clientY / this.renderer.domElement.clientHeight) * 2 + 1; |
|
raycaster.setFromCamera(mouse, this.camera); |
|
|
|
const obj: THREE.Object3D[] = []; |
|
this.findAllObjects(obj, this.scene); |
|
const intersects = raycaster.intersectObjects(obj); |
|
// console.log('Scene has ' + obj.length + ' objects'); |
|
// console.log(intersects.length + ' intersected objects found'); |
|
|
|
if (intersects && intersects.length) { |
|
intersects.forEach((intersect, index) => { |
|
if (index > 1) { |
|
return; |
|
} |
|
|
|
// console.log(intersect); |
|
const object = intersect.object; |
|
if (intersect.uv && object && (object as any).material && (object as any).material.map instanceof CanvasTexture) { |
|
this.addCanvasObject({ |
|
offsetX: intersect.uv.x * this.canvasSize, |
|
offsetY: (1 - intersect.uv.y) * this.canvasSize |
|
}); |
|
} |
|
this.clickIntersect.emit(object); |
|
}); |
|
} |
|
} |
|
|
|
public addCanvasObject(event: { offsetX: number, offsetY: number }): void { |
|
if (!this.selectedLogo) { |
|
return; |
|
} |
|
|
|
const ctx = this.advertCanvas.getContext('2d'); |
|
const image = new Image(750, 750); |
|
image.src = this.selectedLogo.path; |
|
ctx.drawImage(image, |
|
event.offsetX - this.imageSize / 2, |
|
event.offsetY - this.imageSize / 2, |
|
this.imageSize, this.imageSize |
|
); |
|
this.texture.needsUpdate = true; |
|
this.render(); |
|
} |
|
|
|
private findAllObjects(pred: THREE.Object3D[], parent: THREE.Object3D) { |
|
// NOTE: Better to keep separate array of selected objects |
|
if (parent.children.length > 0) { |
|
parent.children.forEach((i) => { |
|
pred.push(i); |
|
this.findAllObjects(pred, i); |
|
}); |
|
} |
|
} |
|
|
|
@HostListener('window:resize', ['$event']) |
|
public onResize() { |
|
this.canvas.style.width = '100%'; |
|
this.canvas.style.height = '100%'; |
|
// console.log('onResize: ' + this.canvas.clientWidth + ', ' + this.canvas.clientHeight); |
|
|
|
this.camera.aspect = this.getAspectRatio(); |
|
this.camera.updateProjectionMatrix(); |
|
this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight); |
|
this.render(); |
|
} |
|
|
|
public splitByMaterial(geometry, materials) { |
|
const parts = []; |
|
const faces = ['a', 'b', 'c']; |
|
let geo, vMap, iMat; |
|
|
|
geometry.faces.forEach(face => { |
|
if (face.materialIndex !== iMat) { |
|
if (iMat) { |
|
this.addPart(materials, iMat, parts, geo); |
|
} |
|
|
|
geo = new THREE.Geometry(); |
|
vMap = {}; |
|
iMat = face.materialIndex; |
|
} |
|
|
|
const f = face.clone(); |
|
faces.forEach(p => { |
|
const iv = face[p]; |
|
|
|
if (!vMap.hasOwnProperty(iv)) { |
|
vMap[iv] = geo.vertices.push(geometry.vertices[iv]) - 1; |
|
} |
|
|
|
f[p] = vMap[iv]; |
|
}); |
|
geo.faces.push(f); |
|
}); |
|
|
|
this.addPart(materials, iMat, parts, geo); |
|
return parts; |
|
} |
|
|
|
private addPart(materials, iMat, parts, geo): void { |
|
const mat = materials ? Object.assign({}, materials[iMat]) : {}; |
|
mat.side = THREE.DoubleSide; |
|
parts.push(new THREE.Mesh(geo, mat)); |
|
} |
|
} |