import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/Addons.js'; import { DRACOLoader } from 'three/examples/jsm/Addons.js'; import { 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 { AnaglyphEffect } from './three/AnaglyphEffect'; 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 * as CANNON from 'cannon-es'; class GameEngine { async init(domNode, opts = {}) { this.w = domNode.clientWidth || 1200, this.h = domNode.clientHeight || 800; this.aspect = this.w / this.h const gameEngine = this; this.perspectiveCamera = new THREE.PerspectiveCamera(45, this.aspect, 0.01, 1000); 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 ); const dirLight = new THREE.DirectionalLight(0xffffff, 4); dirLight.color.setHSL(0.1, 1, 0.95); dirLight.position.set(-12, 33, 37); //dirLight.position.multiplyScalar( 20 ); scene.add(dirLight); dirLight.castShadow = true; dirLight.shadow.mapSize.width = 2048; dirLight.shadow.mapSize.height = 2048; 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: true, preserveDrawingBuffer: true //this is important for screenshots capturing //powerPreference: "high-performance", }); renderer.setPixelRatio(window.devicePixelRatio); // renderer.toneMapping = THREE.CineonToneMapping; // renderer.toneMappingExposure = 1.2; // 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; this.anaglyph = new AnaglyphEffect(renderer); this.anaglyph.setSize(this.w, this.h); this.stereo = new StereoEffect(renderer); this.stereo.setSize(this.w, this.h); 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.render(scene, gameEngine.camera); gameEngine.gizmo?.render(); } renderer.setAnimationLoop(animate); 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) => { gameEngine.camera.zoom -= event.deltaY / 1000; gameEngine.camera.zoom = Math.max(gameEngine.camera.zoom, .4); controls.rotateSpeed = 1 / gameEngine.camera.zoom; gameEngine.camera.updateProjectionMatrix(); }) this.initPhysics(); 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(); } } 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) pitch.add(this.perspectiveCamera); pitch.add(this.orthographicCamera); this.cameraPivot = pivot; this.cameraYaw = yaw; } initPhysics() { const world = new CANNON.World() world.gravity.set(0, -9.82, 0) const groundMaterial = new CANNON.Material('groundMaterial') const slipperyMaterial = new CANNON.Material('slipperyMaterial') const slippery_ground_cm = new CANNON.ContactMaterial( groundMaterial, slipperyMaterial, { friction: 0, restitution: 0.3, contactEquationStiffness: 1e8, contactEquationRelaxation: 3, } ) world.addContactMaterial(slippery_ground_cm) const planeShape = new CANNON.Plane() const planeBody = new CANNON.Body({ mass: 0, material: groundMaterial }) planeBody.addShape(planeShape) planeBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2) world.addBody(planeBody) this.phy = { world, slipperyMaterial } } 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) { // gameEngine.setCameraOrthographic(); // gameEngine.renderer.xr.updateCamera(gameEngine.orthographicCamera); let session = gameEngine.renderer.xr.getFrame().session; console.log(session); session.pauseDepthSensing(); } 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 (!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; 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, progress) { return new Promise((resolve, reject) => { this.loader.load(url, o => { o.scene.traverse(object => { // if (object.name == 'environment'){ // console.log('env recomputing') // object.material.shading = THREE.SmoothShading; // object.geometry.computeVertexNormals(true); // } object.frustumCulled = false; object.castShadow = true; object.receiveShadow = true; }); resolve(o); }, progress, reject) }) } async loadTexture(url, progress) { return new Promise((resolve, reject) => { new THREE.TextureLoader().load(url, texture => { //texture.encoding = THREE.sRGBEncoding; texture.colorSpace = THREE.SRGBColorSpace; resolve(texture) }, progress, reject) }) } async loadPanorama(url) { let t = await this.loadTexture(url); 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; } autoScale(object, mk = 1) { let bb = new THREE.Box3().setFromObject(object); let k = Math.max(bb.max.x - bb.min.x, bb.max.y - bb.min.y, bb.max.z - bb.min.z); object.scale.multiplyScalar(mk / k); } 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); } } } export { GameEngine }