diff --git a/package-lock.json b/package-lock.json index 2cdb319..eb75609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.5", + "cannon-es": "^0.20.0", "eslint": "^8.57.0", "eslint-config-standard": "^17.1.0", "eslint-config-vuetify": "^1.0.0", @@ -2431,6 +2432,13 @@ "node": ">=6" } }, + "node_modules/cannon-es": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/cannon-es/-/cannon-es-0.20.0.tgz", + "integrity": "sha512-eZhWTZIkFOnMAJOgfXJa9+b3kVlvG+FX4mdkpePev/w/rP5V8NRquGyEozcjPfEoXUlb+p7d9SUcmDSn14prOA==", + "dev": true, + "license": "MIT" + }, "node_modules/chai": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", diff --git a/package.json b/package.json index 0e9d14e..5b92b90 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.5", + "cannon-es": "^0.20.0", "eslint": "^8.57.0", "eslint-config-standard": "^17.1.0", "eslint-config-vuetify": "^1.0.0", diff --git a/src/components/AssetsManagement/AssetPreview.vue b/src/components/AssetsManagement/AssetPreview.vue index 0d03674..27ea695 100644 --- a/src/components/AssetsManagement/AssetPreview.vue +++ b/src/components/AssetsManagement/AssetPreview.vue @@ -19,7 +19,7 @@ \ No newline at end of file diff --git a/src/lib/Dashboard.js b/src/lib/Dashboard.js new file mode 100644 index 0000000..add1964 --- /dev/null +++ b/src/lib/Dashboard.js @@ -0,0 +1,73 @@ +import { MeshBasicMaterial, TextureLoader, LinearFilter, + sRGBEncoding, + Mesh, + OrthographicCamera, + PlaneGeometry, + RGBAFormat, + Scene } from 'three'; + +import { Text } from 'troika-three-text'; + +class DashBoard { + constructor(renderer, width, height) { + + var _camera = new OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 0, 1); + + var _scene = new Scene(); + + var _params = { minFilter: LinearFilter, magFilter: LinearFilter, format: RGBAFormat, stencilBuffer: true }; + this.points = 0; + + var _texture = new TextureLoader().load('./assets/maze/x.png'); + _texture.encoding = sRGBEncoding; + + var _material = new MeshBasicMaterial({ + map: _texture, + alphaTest: .5 + }); + + // _mesh = new Mesh( new PlaneGeometry( width * 0.015, width * 0.015 ), _material ); + var _text = new Text(); + _text.font = './assets/fonts/MonomakhUnicode.otf'; + _text.text = 'Точки: 0'; + _text.anchorX = 'right'; + _text.anchorY = 'top'; + _text.fontSize = width * 0.015; + _text.position.set(width * .48, height * .47, 0); + _text.color = 0xffffff; + _text.outlineColor = 0x222222; + _text.outlineWidth = '5%'; + _text.outlineBlur = '5%'; + //_scene.add( _mesh ); + _scene.add(_text); + _text.sync(); + + this.render = function (scene, camera) { + renderer.render(_scene, _camera); + }; + + this.setSize = function (width, height) { + _camera.left = width / -2; + _camera.right = width / 2; + _camera.top = height / 2; + _camera.bottom = height / -2; + _camera.updateProjectionMatrix(); + + _text.position.set(width * .48, height * .47, 0); + }; + + this.addPoints = function (points) { + this.onpoints && this.onpoints(this.points + points, this.points); + this.points += points; + _text.text = 'точки: ' + this.points; + }; + + this.dispose = function () { + if (_mesh) _mesh.geometry.dispose(); + if (_material) _material.dispose(); + }; + + } +} + +export { DashBoard }; \ No newline at end of file diff --git a/src/lib/Hero.js b/src/lib/Hero.js new file mode 100644 index 0000000..bde4395 --- /dev/null +++ b/src/lib/Hero.js @@ -0,0 +1,183 @@ +import { AnimationMixer } from 'three'; +import { PointerControls } from './PointerControls'; +import * as THREE from 'three'; +import * as CANNON from 'cannon-es'; + +class Hero{ + + constructor(object, data){ + this.object = object; + this.data = data; + this.model = object.scene + } + + init(gameEngine){ + this.gameEngine = gameEngine; + this.mixer = new AnimationMixer(this.model); + gameEngine.mixers.push( this.mixer ); + this.actionWalk = this.mixer.clipAction( this.object.animations.find(a=>a.name=='walk') ); + this.actionIdle = this.mixer.clipAction( this.object.animations.find(a=>a.name=='idle') ); + this.actionIdle.play(); + this.activeAction = this.actionIdle; + this.pointerControls = new PointerControls(gameEngine.cameraPivot, this.model, gameEngine.renderer.domElement); + gameEngine.hero = this; + + // Character Collider + const characterCollider = new THREE.Object3D() + characterCollider.position.y = 3 + gameEngine.activeObjects.add(characterCollider) + const colliderShape = new CANNON.Sphere(0.5) + const colliderBody = new CANNON.Body({ mass: 1, material: gameEngine.phy.slipperyMaterial }) + colliderBody.addShape(colliderShape, new CANNON.Vec3(0, 0.5, 0)) + colliderBody.addShape(colliderShape, new CANNON.Vec3(0, -0.5, 0)) + colliderBody.position.set( + characterCollider.position.x, + characterCollider.position.y, + characterCollider.position.z + ) + colliderBody.linearDamping = 0.95 + colliderBody.angularFactor.set(0, 1, 0) // prevents rotation X,Z axis + gameEngine.phy.world.addBody(colliderBody) + + this.characterCollider = characterCollider; + this.colliderBody = colliderBody; + + this.clock = new THREE.Clock() + this.delta = 0 + this.inputVelocity = new THREE.Vector3() + this.velocity = new CANNON.Vec3() + this.euler = new THREE.Euler() + this.quat = new THREE.Quaternion() + this.v = new THREE.Vector3() + this.targetQuaternion = new THREE.Quaternion() + this.distance = 0 + this.canJump = true; + + this.contactNormal = new CANNON.Vec3() + this.upAxis = new CANNON.Vec3(0, 1, 0) + + colliderBody.addEventListener('collide', function (e) { + const contact = e.contact + if (contact.bi.id == this.colliderBody.id) { + contact.ni.negate(this.contactNormal) + } else { + this.contactNormal.copy(contact.ni) + } + if (this.contactNormal.dot(this.upAxis) > 0.5) { + if (!this.canJump) { + setAction(animationActions[1], true) + } + this.canJump = true + } + }.bind(this)) + + + this.ready = true; + } + + lockControls(){ + this.pointerControls.controls.lock(); + } + + setAction(toAction, loop){ + if (toAction != this.activeAction) { + let lastAction = this.activeAction; + this.activeAction = toAction + lastAction.fadeOut(0.1) + this.activeAction.reset() + this.activeAction.fadeIn(0.1) + this.activeAction.play() + if (!loop) { + this.activeAction.clampWhenFinished = true + this.activeAction.loop = THREE.LoopOnce + } + } + } + + update(){ + let { inputVelocity, velocity, euler, quat, v, targetQuaternion, clock } = this; + let pc = this.pointerControls; + pc.update(); + if (this.ready) { + if (this.canJump) { + //walking + this.mixer.update(this.distance / 100) + } else { + //were in the air + this.mixer.update(this.delta) + } + const p = this.characterCollider.position + p.y -= 1 + this.model.position.y = this.characterCollider.position.y + this.distance = this.model.position.distanceTo(p) + + const rotationMatrix = new THREE.Matrix4() + rotationMatrix.lookAt(p, this.model.position, this.model.up) + targetQuaternion.setFromRotationMatrix(rotationMatrix) + + if (!this.model.quaternion.equals(targetQuaternion)) { + this.model.quaternion.rotateTowards(targetQuaternion, this.delta * 10) + } + + if (this.canJump) { + inputVelocity.set(0, 0, 0) + + if (pc.moveForward) { + inputVelocity.z = -1 + } + if (pc.moveBackward) { + inputVelocity.z = 1 + } + + if (pc.moveLeft) { + inputVelocity.x = -1 + } + if (pc.moveRight) { + inputVelocity.x = 1 + } + + inputVelocity.setLength(this.delta * 100) + + // apply camera rotation to inputVelocity + euler.y = this.gameEngine.cameraYaw.rotation.y; + //euler.y = this.gameEngine.camera.rotation.y; + euler.order = 'XYZ' + quat.setFromEuler(euler) + inputVelocity.applyQuaternion(quat) + } + + if(inputVelocity.x || inputVelocity.z){ + this.setAction(this.actionWalk, true) + }else{ + this.setAction(this.actionIdle, true) + } + + this.model.position.lerp(this.characterCollider.position, 1) + } + velocity.set(inputVelocity.x, inputVelocity.y, inputVelocity.z) + this.colliderBody.applyImpulse(velocity) + + this.delta = Math.min(clock.getDelta(), 0.1) + this.gameEngine.phy.world.step(this.delta) + + //cannonDebugRenderer.update() + + this.characterCollider.position.set(this.colliderBody.position.x, this.colliderBody.position.y, this.colliderBody.position.z) + // boxes.forEach((b, i) => { + // boxMeshes[i].position.set(b.position.x, b.position.y, b.position.z) + // boxMeshes[i].quaternion.set(b.quaternion.x, b.quaternion.y, b.quaternion.z, b.quaternion.w) + // }) + + this.characterCollider.getWorldPosition(v) + this.gameEngine.cameraPivot.position.lerp(v, 0.1) + + // if (this.gameEngine.camera.position.distanceTo(v) > 200){ + // this.gameEngine.camera.position.set(v.x+10, 3, v.z + 30); + // } + //this.gameEngine.camera.lookAt(v.x, 3, v.z); + //this.gameEngine.camera.rotation.y = this.model.rotation.y; + } + +} + +export { Hero } \ No newline at end of file diff --git a/src/lib/PointerControls.js b/src/lib/PointerControls.js new file mode 100644 index 0000000..2d8f54c --- /dev/null +++ b/src/lib/PointerControls.js @@ -0,0 +1,168 @@ +import { + Clock, + Vector3 +} from 'three'; + +import { PointerLockControls } from 'three/examples/jsm/Addons.js'; + +class PointerControls { + constructor(camera, hero, domElement) { + this.moveForward = false; + this.moveBackward = false; + this.moveLeft = false; + this.moveRight = false; + this.moveUp = false; + this.moveDown = false; + this.rotateLeft = false; + this.rotateRight = false; + this.canJump = false; + this.velocity = new Vector3(); + this.direction = new Vector3(); + this.rotationY = 0; + this.vertex = new Vector3(); + this.rvelo = 0; + + this.camera = camera; + this.hero = hero; + this.clock = new Clock(); + this.click = false; + + this.controls = new PointerLockControls(camera, domElement); + + const onKeyDown = (event) => { + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + this.moveForward = true; + break; + case 'ArrowLeft': + case 'KeyA': + this.moveLeft = true; + this.rotateLeft = true; + break; + case 'ArrowDown': + case 'KeyS': + this.moveBackward = true; + break; + case 'ArrowRight': + case 'KeyD': + this.moveRight = true; + this.rotateRight = true; + break; + case 'KeyQ': + this.rotateLeft = true; + break; + case 'KeyE': + this.rotateRight = true; + break; + case 'KeyR': + this.moveUp = true; + break; + case 'KeyF': + this.moveDown = true; + break; + case 'Space': + if (this.canJump === true) this.velocity.y += 350; + this.canJump = false; + break; + } + }; + + const onKeyUp = (event) => { + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + this.moveForward = false; + break; + case 'ArrowLeft': + case 'KeyA': + this.moveLeft = false; + this.rotateLeft = false; + break; + case 'ArrowDown': + case 'KeyS': + this.moveBackward = false; + break; + case 'ArrowRight': + case 'KeyD': + this.moveRight = false; + this.rotateRight = false; + break; + case 'KeyQ': + this.rotateLeft = false; + break; + case 'KeyE': + this.rotateRight = false; + break; + case 'KeyR': + this.moveUp = false; + break; + case 'KeyF': + this.moveDown = false; + break; + } + }; + + document.addEventListener('keydown', onKeyDown); + document.addEventListener('keyup', onKeyUp); + + window.addEventListener("gamepadconnected", (e) => { + this.gp = navigator.getGamepads()[e.gamepad.index]; + console.log("Gamepad connected", this.gp); + }); + + domElement.addEventListener('click', () => { + this.controls.isLocked && this.clicked && this.clicked(); + }); + + domElement.addEventListener('mousedown', () => { + this.controls.isLocked && this.onpointer && this.onpointer('start'); + }); + + domElement.addEventListener('mousemove', () => { + this.controls.isLocked && this.onpointer && this.onpointer('drag'); + }); + + domElement.addEventListener('mouseup', () => { + this.controls.isLocked && this.onpointer && this.onpointer('end'); + }); + + this.update = () => { + // const delta = this.clock.getDelta() * 10; + // if (this.gp) { + // this.gp = navigator.getGamepads()[this.gp.index]; + // this.gp.pressed = this.gp.buttons[4].pressed || this.gp.buttons[5].pressed || this.gp.buttons[6].pressed || this.gp.buttons[7].pressed || this.gp.buttons[9].pressed; + // if (!this.click && this.gp.pressed) { + // this.click = true; + // this.clicked?.(); + // //console.log(this.gp.buttons.map((b,i)=>[i, b.pressed])); + // } + // if (this.click && !this.gp.pressed) this.click = false; + // } + // //this.velocity.x -= this.velocity.x * 5.0 * delta; + // this.velocity.z -= this.velocity.z * 5.0 * delta; + // this.rvelo -= this.rvelo * 5 * delta; + // // this.velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass + // this.direction.z = Number(this.moveForward) - Number(this.moveBackward) - Math.floor((this.gp && this.gp.axes[1] || 0) * 100) / 100 + (this.touchControls && this.touchControls.move || 0); + // this.rotationY = Number(this.rotateLeft) - Number(this.rotateRight) - Math.floor((this.gp && this.gp.axes[0] || 0) * 100) / 110 + (this.touchControls && this.touchControls.rotate || 0); + // //this.direction.x = Number( this.moveRight ) - Number( this.moveLeft ); + // //this.direction.normalize(); // this ensures consistent movements in all directions + // if (this.direction.z) this.velocity.z -= this.direction.z * 5.0 * delta; + // //if ( this.moveLeft || this.moveRight ) this.velocity.x -= this.direction.x * 5.0 * delta; + // if (this.rotationY) this.rvelo -= this.rotationY * 8 * delta; + + //this.velocity.x && this.controls.moveRight( - this.velocity.x * delta ); + // if (this.velocity.z) { + // this.controls.moveForward(-this.velocity.z * delta); + // this.hero.position.z += -this.velocity.z * delta; + // } + // this.controls.moveRight( this.direction.x * delta ); + // this.controls.moveForward( this.direction.z * delta ); + // this.hero.position.y += (Number(this.moveUp) - Number(this.moveDown)) * delta; + // this.hero.rotation.y += -this.rvelo * delta; + }; + + } +} + +export { PointerControls }; \ No newline at end of file diff --git a/src/lib/gameEngine.js b/src/lib/gameEngine.js index a338984..866ee7b 100644 --- a/src/lib/gameEngine.js +++ b/src/lib/gameEngine.js @@ -2,6 +2,7 @@ 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'; @@ -12,6 +13,7 @@ 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 = {}) { @@ -27,67 +29,73 @@ class GameEngine { this.frustumSize = 50; this.orthographicCamera = new THREE.OrthographicCamera( - this.frustumSize * this.aspect / - 2, - this.frustumSize * this.aspect / 2, - this.frustumSize / 2, - this.frustumSize / - 2, + 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 ); + 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 ); + 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 ); - + 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, + 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.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.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.setViewport(0, 0, this.w, this.h); renderer.autoClear = true; - this.anaglyph = new AnaglyphEffect( renderer ); - this.anaglyph.setSize( this.w, this.h ); + 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.stereo.setSize(this.w, this.h); - const controls = new OrbitControls( this.camera, renderer.domElement ); - if (opts.gizmo){ + this.activeObjects = new THREE.Group(); + scene.add(this.activeObjects); + + const controls = new OrbitControls(this.camera, renderer.domElement, this.activeObjects); + if (opts.gizmo) { const gizmo = new ViewportGizmo(this.camera, renderer, { - container:'.renderer-gizmo', + container: '.renderer-gizmo', //type:'cube' }); gizmo.attachControls(controls); @@ -97,18 +105,19 @@ class GameEngine { 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; - } ); + 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(); - mixer.update(delta); + gameEngine.hero?.update(); + gameEngine.mixers.forEach(m => m.update(delta)); gameEngine.handleXrAction(gameEngine, delta) gameEngine.render(scene, gameEngine.camera); gameEngine.gizmo?.render(); @@ -120,15 +129,11 @@ class GameEngine { this.clock = clock; this.renderer = renderer; - this.scene = scene; - this.draco = new DRACOLoader().setDecoderPath( '/3rdparty/draco/' ); + this.draco = new DRACOLoader().setDecoderPath('/3rdparty/draco/'); this.loader = new GLTFLoader(); this.loader.setDRACOLoader(this.draco); - this.mixer = mixer; - - this.activeObjects = new THREE.Group(); - scene.add(this.activeObjects); + this.mixers = [mixer]; domNode.appendChild(renderer.domElement); @@ -138,24 +143,26 @@ class GameEngine { 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); + scene.background = new THREE.Color(1, 1, 1); console.log('GameEngine started') - renderer.domElement.addEventListener('wheel', (event)=>{ + 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(); }) - if (opts.ar){ + this.initPhysics(); + + if (opts.ar) { renderer.xr.enabled = true; - document.body.appendChild( ARButton.createButton( renderer, { } ) ); + document.body.appendChild(ARButton.createButton(renderer, {})); } - if (opts.xr){ + if (opts.xr) { renderer.xr.enabled = true; - document.body.appendChild(XRButton.createButton(renderer, opts.depthSense ?{ + document.body.appendChild(XRButton.createButton(renderer, opts.depthSense ? { 'requiredFeatures': ['depth-sensing'], - 'depthSensing': { + 'depthSensing': { usagePreference: ["gpu-optimized"], dataFormatPreference: ["unsigned-short"], matchDepthView: false @@ -165,44 +172,44 @@ class GameEngine { } } - 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) ); + 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.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 ); + 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) ); + 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.addEventListener('connected', e => { c2.gamepad = e.data.gamepad; }) - this.scene.add( c2 ); + 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 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 ); + 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 ) ] ); + const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, - 1)]); - let line = new THREE.Line( geometry ); + let line = new THREE.Line(geometry); line.name = 'line'; line.scale.z = 5; @@ -210,42 +217,84 @@ class GameEngine { this.xrController2 = c2 } - handleXrAction(gameEngine, delta){ + initCameraPivot() { + const pivot = new THREE.Object3D() + pivot.position.set(0, 1, 10) + + 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){ + if (gameEngine.xrController1?.gamepad) { let gp = gameEngine.xrController1.gamepad; - if (gp.axes[3] != 0){ + if (gp.axes[3] != 0) { gameEngine.scene.position.z += gp.axes[3] * delta; } - if (gp.axes[2] != 0){ + if (gp.axes[2] != 0) { gameEngine.scene.position.x += gp.axes[2] * delta; } } - if (gameEngine.xrController2?.gamepad){ + 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){ + if (gp.axes[2] != 0) { + if (gp1.buttons[4]?.pressed) { gameEngine.scene.position.y += gp.axes[2] * delta; - }else if (gp1.buttons[5]?.pressed){ + } else if (gp1.buttons[5]?.pressed) { let sc = gameEngine.scene.scale.x * (gp.axes[2] * delta * 0.5 + 1); gameEngine.scene.scale.set(sc, sc, sc); - }else{ + } else { gameEngine.scene.rotation.y += gp.axes[2] * delta * 0.5; } } - if (gp.buttons[4]?.pressed){ + 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){ + if (gp.buttons[5]?.pressed) { // gameEngine.setCameraOrthographic(); // gameEngine.renderer.xr.updateCamera(gameEngine.orthographicCamera); let session = gameEngine.renderer.xr.getFrame().session; @@ -257,7 +306,7 @@ class GameEngine { onControllerEvent(event) { //this.handleXrAction(event, this); - event.type !=='move' && console.log(event) + event.type !== 'move' && console.log(event) // const controller = event.target; // if (controller.userData.active === false) return; // this.raycaster.setFromXRController(controller); @@ -300,10 +349,10 @@ class GameEngine { $ = THREE; - async load(url, progress){ - return new Promise((resolve, reject)=>{ - this.loader.load(url, o=>{ - o.scene.traverse(object=>{ + 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; @@ -318,9 +367,9 @@ class GameEngine { }) } - async loadTexture(url, progress){ - return new Promise((resolve, reject)=>{ - new THREE.TextureLoader().load(url, texture=>{ + 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) @@ -328,54 +377,54 @@ class GameEngine { }) } - async loadPanorama(url){ + 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)=>{ + 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.mixer.clipAction(clip, object); + playAnimation(object, clip, play = true) { + let action = this.mixers[0].clipAction(clip, object); if (play) action.play(); else action.stop(); } - stop(){ + stop() { this.renderer?.setAnimationLoop(null); } - getMouseVector(mouseEvent, domElement){ + getMouseVector(mouseEvent, domElement) { //console.log(mouseEvent, domElement) const mouse = new THREE.Vector2(); - if (this.renderType == 'VR'){ + if (this.renderType == 'VR') { let x; - if (mouseEvent.offsetX > window.innerWidth / 2){ + if (mouseEvent.offsetX > window.innerWidth / 2) { x = (mouseEvent.offsetX - window.innerWidth / 2) * 2 - }else { + } 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; + 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){ + 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=>{ + 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; } @@ -384,13 +433,13 @@ class GameEngine { return intersects; } - autoScale(object, mk = 1){ + 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); + object.scale.multiplyScalar(mk / k); } - setCamera(camera){ + setCamera(camera) { //camera.updateProjectionMatrix(); this.camera = camera; //this.transformControls.camera = camera; @@ -413,11 +462,11 @@ class GameEngine { this.camera = o; this.transformControls.camera = o; this.orbitControls.object = o; - if (this.gizmo){ + if (this.gizmo) { this.gizmo.camera = o; } } - + setCameraPerspective() { let o = this.perspectiveCamera; // const oldY = o.position.y; @@ -427,12 +476,12 @@ class GameEngine { this.camera = o; this.transformControls.camera = o; this.orbitControls.object = o; - if (this.gizmo){ + if (this.gizmo) { this.gizmo.camera = o; } } - - resize(w, h){ + + resize(w, h) { this.w = w; this.h = h; this.aspect = this.w / this.h @@ -443,21 +492,21 @@ class GameEngine { 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.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 ); + 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} \ No newline at end of file +export { GameEngine } \ No newline at end of file