import { ISystem, System, Config, Components } from '../../../common/engine/system'; import { IEntity, State, DoSet } from '../../../common/engine/state'; import * as THREE from 'three'; import { IVoxComponent, VoxMesh } from './o3d/vox'; import { ITileComponent, TileMesh } from './o3d/tile'; const throttle = (type: string, name: string, obj?: any) => { obj = obj || window; let running = false; const func = () => { if (running) { return; } running = true; requestAnimationFrame(() => { obj.dispatchEvent(new CustomEvent(name)); running = false; }); }; obj.addEventListener(type, func); }; interface ITransformComponent { size?: number | number[]; rotation?: number[]; position?: number[]; } interface ILightComponent { color?: number; position?: number[]; intensity?: number; } interface ICameraComponent { rotation?: number[]; position?: number[]; target?: number[]; } interface IDirectionalLightComponent extends ILightComponent { target?: number[]; } const LightUpdate = (light, data) => { if (data.color !== undefined) { light.color.setHex(data.color); } if (data.intensity !== undefined) { light.intensity = data.intensity; } }; const TransformUpdate = (o3d, data) => { if (data.position) { o3d.position.fromArray(data.position.map(x => x)); } if (data.rotation) { o3d.rotation.fromArray(data.rotation.map(THREE.Math.degToRad)); } if (data.quaternion) { o3d.quaternion.fromArray(data.quaternion.map(x => x)); } }; const TargetUpdate = (o3d, data) => { TransformUpdate(o3d, data); if (data.target) { o3d.lookAt(new THREE.Vector3().fromArray(data.target)); } }; const BB_MATERIAL = new THREE.MeshBasicMaterial({ wireframe: true, color: 0x00FF00 }); @Components('directional', 'rotation', 'position', 'ambient', 'vox', 'camera', 'tilemap') export class WebGL extends System { // Entire Scene private scene = new THREE.Scene(); private entityMap: { [key: string]: THREE.Object3D } = {}; private camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); private renderer = new THREE.WebGLRenderer({ antialias: true }); private bodies: { [key: string]: any } = {}; start(state: State) { super.start(state); (window as any).scene = this.scene; throttle('resize', 'optimizedResize'); this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFShadowMap; window.addEventListener('optimizedResize', this.handleResize.bind(this)); this.handleResize(); document.body.appendChild(this.renderer.domElement); } stop() { document.body.removeChild(this.renderer.domElement); } update(entity: IEntity, component: string) { super.update(entity, component); this.updateO3D(entity); } add(entity: IEntity) { super.add(entity); this.updateO3D(entity, true); } remove(entity: IEntity) { super.remove(entity); const obj = this.entityMap[entity.id]; obj.parent.remove(obj); delete this.entityMap[entity.id]; delete this.bodies[entity.id]; } tick(delta: number) { this.renderer.render(this.scene, this.camera); } private handleResize() { this.renderer.setSize(window.innerWidth, window.innerHeight); this.camera.aspect = window.innerWidth / window.innerHeight; } // Delta updates sure would be nice private updateO3D(entity: IEntity, created = false) { const o3d = this.get(entity.id); const t: ITransformComponent = entity; if (t.position || t.rotation) { TransformUpdate(o3d, t); } const body = entity.body; if (this.config.boundingbox && body && body.size) { if (o3d.userData.body) { o3d.remove(o3d.userData.body); } const size = body.size; let geom; switch (body.type) { case 'sphere': geom = new THREE.SphereGeometry(size[0]); break; default: geom = new THREE.BoxGeometry(size[0], size[1], size[2]); } o3d.userData.body = new THREE.Mesh( geom, BB_MATERIAL ); o3d.add(o3d.userData.body); } const v: IVoxComponent = entity.vox; if (v) { if (!o3d.userData.vox) { o3d.userData.vox = new VoxMesh(v); o3d.add(o3d.userData.vox); } else { o3d.userData.vox.update(v); } } else if (o3d.userData.vox) { // Check to see if we have a vox and remove it o3d.remove(o3d.userData.vox); o3d.userData.vox.stop(); delete o3d.userData.vox; } const tilemap: ITileComponent = entity.tilemap; if(tilemap) { let tilemesh: TileMesh = o3d.userData.tilemap; if(!tilemesh) { tilemesh = o3d.userData.tilemap = new TileMesh(tilemap, (newBody) => { this.dispatch(DoSet(entity.id, 'body', Object.assign( body !== undefined ? body : {}, newBody ))); }); o3d.add(tilemesh); } } else if(o3d.userData.tilemap) { o3d.remove(o3d.userData.vox); delete o3d.userData.vox; } const ambient: ILightComponent = entity.ambient; if (ambient) { let light = o3d.userData.ambient; if (!light) { light = o3d.userData.ambient = new THREE.AmbientLight(); o3d.add(light); } LightUpdate(light, ambient); } else if (o3d.userData.ambient) { o3d.remove(o3d.userData.ambient); delete o3d.userData.ambient; } const directional: IDirectionalLightComponent = entity.directional; if (directional) { let light = o3d.userData.directional; if (!light) { light = o3d.userData.directional = new THREE.SpotLight(); light.castShadow = true; o3d.add(light); } LightUpdate(light, directional); TargetUpdate(light, directional); } const camera: ICameraComponent = entity.camera; if (camera) { let camObj = o3d.userData.camera; if (!camObj) { camObj = o3d.userData.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); this.camera = camObj; } TargetUpdate(camObj, camera); } return o3d; } private get(entityId: string) { if (this.entityMap[entityId] !== undefined) { return this.entityMap[entityId]; } const o3d = new THREE.Object3D(); o3d.name = entityId; this.entityMap[entityId] = o3d; const list = entityId.split('.'); const parent = list.length > 1 ? this.get(list.slice(0, -1).join('.')) : this.scene; parent.add(o3d); return o3d; } }