import * as THREE from 'three'; import { GLTFLoader, DRACOLoader, OrbitControls } from 'three/examples/jsm/Addons.js'; //import { Controller as OrbitControls } from './3rd-party/phy/3TH/Controller.js'; import { ViewportGizmo } from "three-viewport-gizmo"; import Stats from 'three/examples/jsm/libs/stats.module'; import { AnaglyphEffect } from 'three/addons/effects/AnaglyphEffect.js'; import { StereoEffect } from 'three/addons/effects/StereoEffect.js'; import { MapControls } from 'three/addons/controls/MapControls.js'; import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'; import { TransformControls } from 'three/addons/controls/TransformControls.js'; import { ARButton } from 'three/addons/webxr/ARButton.js'; import { XRButton } from 'three/addons/webxr/XRButton.js'; import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js'; import { Physics } from './Physics.js'; import { Clickable } from './Clickable.js'; import { DashBoard } from './Dashboard.js'; import { MotionEngine } from './MotionEngine.js'; const assetPath = '/asset/default/'; class GameEngine extends THREE.EventDispatcher{ async init(domNode, opts = {}) { this.w = domNode.clientWidth || 1200, this.h = domNode.clientHeight || 800; this.aspect = this.w / this.h this.opts = opts; const gameEngine = this; this.perspectiveCamera = new THREE.PerspectiveCamera(45, this.aspect, 0.01, 200); this.raycaster = new THREE.Raycaster(); this.perspectiveCamera.position.set(0, 0, 10); this.camera = this.perspectiveCamera; this.frustumSize = 50; this.orthographicCamera = new THREE.OrthographicCamera( this.frustumSize * this.aspect / - 2, this.frustumSize * this.aspect / 2, this.frustumSize / 2, this.frustumSize / - 2, 0.01, 1000); this.orthographicCamera.position.set(0, 0, 100); const scene = new THREE.Scene(); this.scene = scene; this.initCameraPivot() // let light = new THREE.AmbientLight( 0x404040, 300 ); // soft white light // scene.add( this.light ); var hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444 ); hemiLight.position.set(0, 33, 0); scene.add( hemiLight ); const dirLight = new THREE.DirectionalLight(0xffffff, 4); dirLight.color.setHSL(0.1, 1, 0.95); dirLight.position.set(12, 33, -37); // dirLight.position.multiplyScalar( 0.20 ); // hemiLight.position.multiplyScalar( 0.20 ); scene.add(dirLight); dirLight.castShadow = true; dirLight.shadow.mapSize.width = 1024; dirLight.shadow.mapSize.height = 1024; const d = 50; dirLight.shadow.camera.left = - d; dirLight.shadow.camera.right = d; dirLight.shadow.camera.top = d; dirLight.shadow.camera.bottom = - d; dirLight.shadow.camera.far = 1000; dirLight.shadow.bias = - 0.001; const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, preserveDrawingBuffer: true, //this is important for screenshots capturing powerPreference: "high-performance", }); renderer.setPixelRatio(window.devicePixelRatio); renderer.toneMapping = THREE.CineonToneMapping; renderer.toneMappingExposure = 1.0; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap // renderer.toneMapping = THREE.ACESFilmicToneMapping; // renderer.toneMappingExposure = 1; // renderer.shadowMap.enabled = true; // renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.setSize(this.w, this.h); renderer.setViewport(0, 0, this.w, this.h); renderer.autoClear = true; renderer.alpha = true this.anaglyph = new AnaglyphEffect(renderer); this.anaglyph.setSize(this.w, this.h); this.stereo = new StereoEffect(renderer); this.stereo.setSize(this.w, this.h); const dashboard = new DashBoard(this); this.dashboard = dashboard; this.motionQueue = new MotionEngine(); this.assetPath = assetPath; this.activeObjects = new THREE.Group(); scene.add(this.activeObjects); const controls = new OrbitControls(this.camera, renderer.domElement); if (opts.gizmo) { const gizmo = new ViewportGizmo(this.camera, renderer, { container: '.renderer-gizmo', //type:'cube' }); gizmo.attachControls(controls); this.gizmo = gizmo; } this.orbitControls = controls; //controls.enableZoom = true; //const controls = new MapControls( camera, renderer.domElement ); this.transformControls = new TransformControls(this.camera, renderer.domElement); this.transformControls.addEventListener('dragging-changed', function (event) { controls.enabled = !event.value; }); // controls.enableDamping = true; // controls.screenSpacePanning = true; this.renderType = 'ST'; function animate(time) { let delta = clock.getDelta(); gameEngine.hero?.update(); gameEngine.mixers.forEach(m => m.update(delta)); gameEngine.handleXrAction(gameEngine, delta) gameEngine.dispatchEvent({type: 'beforeRender'}) this.motionQueue.update(); gameEngine.render(scene, gameEngine.camera); if (!renderer.xr.isPresenting) { gameEngine.gizmo?.render(); } // renderer.autoClear = false; // dashboard.render(); // renderer.autoClear = true; } renderer.setAnimationLoop(animate.bind(this)); const mixer = new THREE.AnimationMixer(this.scene); const clock = new THREE.Clock(); this.clock = clock; this.renderer = renderer; this.draco = new DRACOLoader().setDecoderPath('/3rdparty/draco/'); this.loader = new GLTFLoader(); this.loader.setDRACOLoader(this.draco); this.mixers = [mixer]; domNode.appendChild(renderer.domElement); let texture = await this.loadTexture('/static/textures/bck.webp', ''); // let bck = await this.loadTexture('/static/textures/bck.webp'); // bck.premultiplyAlpha = true; texture.mapping = THREE.EquirectangularReflectionMapping; // scene.background = bck; //new THREE.Color(0.7,0.7,0.7); scene.environment = texture; scene.background = new THREE.Color(1, 1, 1); //console.log('GameEngine started') renderer.domElement.addEventListener('wheel', (event) => { if (gameEngine.hero){ gameEngine.hero.characterControls.cameraZ += event.deltaY / 33; }else{ gameEngine.camera.zoom -= event.deltaY / 1000; gameEngine.camera.zoom = Math.max(gameEngine.camera.zoom, .4); controls.rotateSpeed = 1 / gameEngine.camera.zoom; gameEngine.camera.updateProjectionMatrix(); } }) await this.initPhysics(); this.stats = new Stats(); document.body.appendChild(this.stats.dom); if (opts.ar) { renderer.xr.enabled = true; document.body.appendChild(ARButton.createButton(renderer, {})); } if (opts.xr) { renderer.xr.enabled = true; document.body.appendChild(XRButton.createButton(renderer, opts.depthSense ? { 'requiredFeatures': ['depth-sensing'], 'depthSensing': { usagePreference: ["gpu-optimized"], dataFormatPreference: ["unsigned-short"], matchDepthView: false } } : {})); this.initXrControllers(); } this.clickable = new Clickable(20); } initXrControllers() { let c1 = this.renderer.xr.getController(0); c1.addEventListener('select', this.onSelect.bind(this)); c1.addEventListener('selectstart', this.onControllerEvent.bind(this)); c1.addEventListener('selectend', this.onControllerEvent.bind(this)); c1.addEventListener('move', this.onControllerEvent.bind(this)); c1.userData.active = false; c1.addEventListener('connected', e => { c1.gamepad = e.data.gamepad; // this.session = this.renderer.xr.getSession(); // this.session.addEventListener('selectstart', this.onControllerEvent.bind(this)); }) this.scene.add(c1); let c2 = this.renderer.xr.getController(1); c2.addEventListener('select', this.onSelect.bind(this)); c2.addEventListener('selectstart', this.onControllerEvent.bind(this)); c2.addEventListener('selectend', this.onControllerEvent.bind(this)); c2.addEventListener('move', this.onControllerEvent.bind(this)); c2.userData.active = true; c2.addEventListener('connected', e => { c2.gamepad = e.data.gamepad; }) this.scene.add(c2); const controllerModelFactory = new XRControllerModelFactory(); let controllerGrip1 = this.renderer.xr.getControllerGrip(0); controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1)); this.scene.add(controllerGrip1); let controllerGrip2 = this.renderer.xr.getControllerGrip(1); controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2)); this.scene.add(controllerGrip2); const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, - 1)]); let line = new THREE.Line(geometry); line.name = 'line'; line.scale.z = 5; this.controllerLine = line; this.xrController1 = c1 this.xrController2 = c2 } initCameraPivot() { const pivot = new THREE.Object3D() pivot.position.set(0, 0, 0) const yaw = new THREE.Object3D() const pitch = new THREE.Object3D() this.scene.add(pivot) pivot.add(yaw) yaw.add(pitch) this.scene.add(this.perspectiveCamera); this.scene.add(this.orthographicCamera); this.cameraPivot = pivot; this.cameraYaw = yaw; } async initPhysics() { this.physics = new Physics(this); await this.physics.init(); } handleXrAction(gameEngine, delta) { //console.log(event.type); if (gameEngine.xrController1?.gamepad) { let gp = gameEngine.xrController1.gamepad; if (gp.axes[3] != 0) { gameEngine.activeObjects.position.z += gp.axes[3] * delta; } if (gp.axes[2] != 0) { gameEngine.activeObjects.position.x += gp.axes[2] * delta; } } if (gameEngine.xrController2?.gamepad) { let gp = gameEngine.xrController2.gamepad; let gp1 = gameEngine.xrController1.gamepad; // if (gp.axes[3] != 0){ // let sc = gameEngine.scene.scale.x + gp.axes[3] * delta * 0.5; // gameEngine.scene.scale.set(sc, sc, sc); // } if (gp.axes[2] != 0) { if (gp1.buttons[4]?.pressed) { gameEngine.activeObjects.position.y += gp.axes[2] * delta; } else if (gp1.buttons[5]?.pressed) { let sc = gameEngine.activeObjects.scale.x * (gp.axes[2] * delta * 0.5 + 1); gameEngine.activeObjects.scale.set(sc, sc, sc); } else { gameEngine.activeObjects.rotation.y += gp.axes[2] * delta * 0.5; } } if (gp.buttons[4]?.pressed) { let x = performance.now() % 1000; if (x < 333) { gameEngine.transformControls.setMode('translate'); }else if (x < 666) { gameEngine.transformControls.setMode('scale'); }else{ gameEngine.transformControls.setMode('rotate'); } // let nextMode = { // 'translate': 'scale', // 'scale': 'rotate', // 'rotate': 'translate' // } // if(gameEngine.opts.designMode){; // gameEngine.transformControls.setMode(nextMode[gameEngine.transformControls.getMode()]); // } } // if (gp.buttons[5]?.pressed) { // // gameEngine.setCameraOrthographic(); // // gameEngine.renderer.xr.updateCamera(gameEngine.orthographicCamera); // let session = gameEngine.renderer.xr.getFrame().session; // console.log(session); // session.resumeDepthSensing(); // } } } onControllerEvent(event) { //this.handleXrAction(event, this); //event.type !== 'move' && console.log(event) const controller = event.target; //console.log(event) if (this.opts.designMode){ if (!controller.userData) return if (controller.userData.active === false) return; this.transformControls.getRaycaster().setFromXRController(controller); switch (event.type) { case 'selectstart': this.transformControls.pointerDown(null); break; case 'selectend': this.transformControls.pointerUp(null); break; case 'move': this.transformControls.pointerHover(null); this.transformControls.pointerMove(null); break; } } } onSelect(event) { const controller = event.target; if (this.opts.designMode){ this.xrController1.userData.active = false; this.xrController2.userData.active = false; if (controller === this.xrController1) { this.xrController1.userData.active = true; this.xrController1.add(this.controllerLine); } if (controller === this.xrController2) { this.xrController2.userData.active = true; this.xrController2.add(this.controllerLine); } this.raycaster.setFromXRController(controller); const intersects = this.raycaster.intersectObjects(this.activeObjects.children, true); intersects.forEach(o => { while (o.object && !this.activeObjects.children.includes(o.object)) { o.object = o.object.parent; } }) if (intersects.length > 0) { setTimeout(() => { this.transformControls.attach(intersects[0].object); }, 100); } } } $ = THREE; async load(url, path = assetPath, progress) { return new Promise((resolve, reject) => { this.loader.load(`${path}${url}`, o => { o.scene.traverse(object => { if (object.name == 'environment' || object.material){ //console.log('env recomputing') object.material.shading = THREE.SmoothShading; object.geometry.computeVertexNormals(true); //object.material.metalness = 0; //if (object.material.map) object.material.map.colorSpace = THREE.SRGBColorSpace; } if ( object instanceof THREE.Mesh ) { //object.material.envMap = this.scene.environment; if (object.material.map) object.material.map.colorSpace = THREE.SRGBColorSpace; //object.material.metalness = 0; } object.frustumCulled = false; object.castShadow = true; object.receiveShadow = true; }); resolve(o); }, progress, reject) }) } async loadTexture(url, path = assetPath, progress) { return new Promise((resolve, reject) => { new THREE.TextureLoader().load(`${path}${url}`, texture => { //texture.encoding = THREE.sRGBEncoding; texture.colorSpace = THREE.SRGBColorSpace; resolve(texture) }, progress, reject) }) } async loadPanorama(url, path = assetPath) { let t = await this.loadTexture(url, path); t.mapping = THREE.EquirectangularReflectionMapping; this.scene.background = t; this.scene.environment = t; } async captureScreenshot(type = 'image/webp', quality = 80) { return new Promise((resolve, reject) => { this.renderer.domElement.toBlob(resolve, type, quality) }) } playAnimation(object, clip, play = true) { let action = this.mixers[0].clipAction(clip, object); if (play) action.play(); else action.stop(); } stop() { this.renderer?.setAnimationLoop(null); } getMouseVector(mouseEvent, domElement) { //console.log(mouseEvent, domElement) const mouse = new THREE.Vector2(); if (this.renderType == 'VR') { let x; if (mouseEvent.offsetX > window.innerWidth / 2) { x = (mouseEvent.offsetX - window.innerWidth / 2) * 2 } else { x = mouseEvent.offsetX * 2; } mouse.x = (x / window.innerWidth) * 2 - 1; mouse.y = - (mouseEvent.offsetY / window.innerHeight) * 2 + 1; } else { mouse.x = (mouseEvent.offsetX / domElement.clientWidth) * 2 - 1; mouse.y = - (mouseEvent.offsetY / domElement.clientHeight) * 2 + 1; } return mouse; } intersect(mouseEvent, domElement, objects, recursive = false, returnInputObjects = true) { let mouse = this.getMouseVector(mouseEvent, domElement); this.raycaster.setFromCamera(mouse, this.camera); let intersects = this.raycaster.intersectObjects(objects.filter(o => o.visible), recursive); if (returnInputObjects && recursive) { intersects.forEach(o => { while (o.object && !objects.includes(o.object)) { o.object = o.object.parent; } }) } //console.log('intersects', intersects); return intersects; } onClick(mouseEvent, domElement){ let mouse = this.getMouseVector(mouseEvent, domElement); this.raycaster.setFromCamera(mouse, this.camera); this.clickable.update(mouse, this.camera, mouseEvent); this.hero?.characterControls?.idleReset(); } setCamera(camera) { //camera.updateProjectionMatrix(); this.camera = camera; //this.transformControls.camera = camera; this.orbitControls.object = camera; } setCameraOrthographic() { let o = this.orthographicCamera; // o.position.copy(o.position); // const distance = o.position.distanceTo(this.orbitControls.target); // const halfWidth = this.frustumWidthAtDistance(o, distance) / 2; // const halfHeight = this.frustumHeightAtDistance(o, distance) / 2; // o.top = halfHeight; // o.bottom = -halfHeight; // o.left = -halfWidth; // o.right = halfWidth; // o.zoom = 1; // o.lookAt(this.orbitControls.target); // o.updateProjectionMatrix(); this.camera = o; this.transformControls.camera = o; this.orbitControls.object = o; if (this.gizmo) { this.gizmo.camera = o; } } setCameraPerspective() { let o = this.perspectiveCamera; // const oldY = o.position.y; // o.position.copy(o.position); // o.position.y = oldY / o.zoom; // o.updateProjectionMatrix(); this.camera = o; this.transformControls.camera = o; this.orbitControls.object = o; if (this.gizmo) { this.gizmo.camera = o; } } resize(w, h) { this.w = w; this.h = h; this.aspect = this.w / this.h this.perspectiveCamera.aspect = this.aspect; this.perspectiveCamera.updateProjectionMatrix(); this.orthographicCamera.left = - this.frustumSize * this.aspect / 2; this.orthographicCamera.right = this.frustumSize * this.aspect / 2; this.orthographicCamera.top = this.frustumSize / 2; this.orthographicCamera.bottom = - this.frustumSize / 2; this.renderer.setSize(w, h); this.renderer.setViewport(0, 0, this.w, this.h); this.anaglyph.setSize(w, h); this.stereo.setSize(w, h); this.gizmo?.update(); } render(scene, camera) { if (this.renderType == 'VR') { this.stereo?.render(scene, camera); } else if (this.renderType == 'AG') { this.anaglyph?.render(scene, camera); } else { this.renderer.render(scene, camera); } this.stats?.update() } clearScene(){ this.hero?.destroy(); this.dashboard?.reset(); this.activeObjects.clear(); this.physics.clear(); this.clickable.removeAll(); this.motionQueue.clearAll(); } } export { GameEngine }