diff --git a/backend/controllers/api/GamesController.js b/backend/controllers/api/GamesController.js index c37ab04..f258f1a 100644 --- a/backend/controllers/api/GamesController.js +++ b/backend/controllers/api/GamesController.js @@ -62,7 +62,7 @@ class GamesController{ * @memberof GamesController */ router.delete('/:id', async (req, res)=>{ - await scenario.remove(req.params.id); + await game.remove(req.params.id); res.json({status: 'OK'}); }) diff --git a/package-lock.json b/package-lock.json index a5ff448..e325d6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pronature-platform", - "version": "0.0.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pronature-platform", - "version": "0.0.0", + "version": "0.2.0", "dependencies": { "@mdi/font": "7.4.47", "@vitejs/plugin-basic-ssl": "^1.1.0", @@ -25,6 +25,7 @@ "roboto-fontface": "*", "sharp": "^0.33.5", "three": "^0.169.0", + "three-viewport-gizmo": "^2.2.0", "uuid": "^11.0.2", "vue": "^3.5.13", "vuetify": "^3.7.16" @@ -6623,6 +6624,15 @@ "integrity": "sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==", "license": "MIT" }, + "node_modules/three-viewport-gizmo": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/three-viewport-gizmo/-/three-viewport-gizmo-2.2.0.tgz", + "integrity": "sha512-Jo9Liur1rUmdKk75FZumLU/+hbF+RtJHi1qsKZpntjKlCYScK6tjbYoqvJ9M+IJphrlQJF5oReFW7Sambh0N4Q==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.162.0 <1.0.0" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index 57001a1..e29da02 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "roboto-fontface": "*", "sharp": "^0.33.5", "three": "^0.169.0", + "three-viewport-gizmo": "^2.2.0", "uuid": "^11.0.2", "vue": "^3.5.13", "vuetify": "^3.7.16" diff --git a/src/components/GameDesigner/GameDesigner.vue b/src/components/GameDesigner/GameDesigner.vue index 2dc62e8..168fbe2 100644 --- a/src/components/GameDesigner/GameDesigner.vue +++ b/src/components/GameDesigner/GameDesigner.vue @@ -1,18 +1,48 @@ @@ -25,70 +55,114 @@ let gameEngine = null; export default { props:{ modelValue: Object, - scenario: Object }, data(){ return { scenesList: [], + objectsList: [], mode: 'translate', - pointerDownTime: 0 + pointerDownTime: 0, + scenario: null, + renderType: 'ST', + cameraType: 'perspective' } }, computed:{ scenes(){ - return this.scenario.scenes || []; + return this.scenario?.scenes || []; }, scene(){ return this.scenesList[0]; }, + currentObject(){ + return this.objectsList[0]; + }, + sceneObjects(){ + return this.object.scenes?.[this.scene?.data?.id]?.objects; + }, object(){ return this.modelValue; }, + objectAnimations(){ + return this.currentObject?.__g?.animations?.map(a => ({ + name: a.name, id: a.uuid, a + })); + } }, watch:{ async scene(n){ - await this.loadEnvironment(); + this.object.scenes = this.object.scenes || {} + this.object.scenes[n.data.id] = this.object.scenes[n.data.id] || {} + await this.loadEnvironment(n, this.object.scenes[n.data.id]); }, mode(n){ gameEngine.transformControls.setMode(n) + }, + async 'object.scenario'(n){ + await this.loadScenario() + }, + currentObject(n){ + gameEngine.transformControls.attach(n.__o); + gameEngine.gizmo.target = n.__o.position; + }, + renderType(v){ + gameEngine.renderType = v; + }, + cameraType(v){ + if (v == 'perspective'){ + gameEngine.setCameraPerspective(); + }else{ + gameEngine.setCameraOrthographic(); + } } }, async mounted(){ gameEngine = new GameEngine(); - this.gameEngine = gameEngine; + //this.gameEngine = gameEngine; await gameEngine.init(this.$refs.target); - gameEngine.scene.add(gameEngine.transformControls.getHelper()) + gameEngine.scene.add(gameEngine.transformControls.getHelper()); + gameEngine.scene.add(new gameEngine.$.GridHelper(100,100)); + this.resize(); + //gameEngine.setCamera(gameEngine.orthographicCamera) + //gameEngine.setCameraOrthographic(); + await this.loadScenario(); + window.addEventListener('resize', this.resize); + }, + unmounted(){ + window,removeEventListener('resize', this.resize); }, methods:{ - async loadEnvironment(){ - await this.expandScenarioData(); - if (!this.object.gd){ - this.object.gd = { - items: [] - }; + async loadScenario(){ + if (this.object.scenario){ + this.scenario = (await this.$api.scenario.load(this.object.scenario)).data; + }else{ + this.scenario = null; } + }, + async loadEnvironment(scene, target){ + //await gameEngine.loadPanorama(`/asset/default/43.webp`); + await this.expandScenarioData(scene); + target.objects = target.objects || {}; + let l = target.objects; if (this.scene.data.$environment.type == 'panorama2d'){ await gameEngine.loadPanorama(`/asset/default/${this.scene.data.$environment.asset.name}`); }else{ let env = await gameEngine.load(`/asset/default/${this.scene.data.$environment.asset.name}`); - gameEngine.autoScale(env.scene, 10); + this.setObjectAttributes(l, this.scene.data, env, 100); gameEngine.activeObjects.add(env.scene); } for (let i of this.scene.data.items) { let gltf = await gameEngine.load(`/asset/default/${i.data.$go.asset.name}`); - let o = gltf.scene; - gameEngine.autoScale(o); - gameEngine.activeObjects.add(o); - const {position, scale, rotation} = o; - i.gd = {position, scale, rotation}; - console.log(JSON.stringify(i.gd)); - window.gameEngine = gameEngine; + this.setObjectAttributes(l, i.data, gltf, 10); + gameEngine.activeObjects.add(gltf.scene); + //console.log(JSON.stringify(l)); + //window.gameEngine = gameEngine; //console.log(new gameEngine.$.Euler({"isEuler":true,"_x":0,"_y":0,"_z":0,"_order":"XYZ"})); } }, - async expandScenarioData(){ - this.scene.data.$environment = (await this.$api.gameObject.load(this.scene.data.environment)).data - for (let i of this.scene.data.items) { + async expandScenarioData(scene){ + scene.data.$environment = (await this.$api.gameObject.load(scene.data.environment)).data + for (let i of scene.data.items) { i.data.$go = (await this.$api.gameObject.load(i.data.go)).data; } }, @@ -99,13 +173,41 @@ export default { if (performance.now() - this.pointerDownTime < 200){ let intersects = gameEngine.intersect(e, this.$refs.target, gameEngine.activeObjects.children, true); if (intersects.length){ - console.log('attaching controls to', intersects[0].object) - gameEngine.transformControls.attach(intersects[0].object); + //console.log('attaching controls to', intersects[0].object) + //gameEngine.transformControls.attach(intersects[0].object); + console.log(this.sceneObjects[intersects[0].object.__pn_id]) + this.objectsList[0] = this.sceneObjects[intersects[0].object.__pn_id] }else{ gameEngine.transformControls.detach(); } } - } + }, + setObjectAttributes(l, data, o, autoScaleFactor = 1){ + if (l[data.id]){ + ['position', 'scale', 'rotation'].forEach(p=>{ + o.scene[p].copy(l[data.id][p]) + }) + }else{ + gameEngine.autoScale(o.scene, autoScaleFactor); + } + l[data.id] = l[data.id] || {}; + ['position', 'scale', 'rotation', 'visible'].forEach(p=>{ + l[data.id][p] = o.scene[p]; + }) + l[data.id].__o = o.scene; + l[data.id].__g = o; + l[data.id].__title = data.title; + o.scene.__pn_id = data.id; + }, + async toggleAnimation(animation){ + animation.playing = !animation.playing; + gameEngine.playAnimation(gameEngine.scene, animation.a, animation.playing); + }, + resize(){ + let r = this.$refs.target; + gameEngine.resize(r.clientWidth, r.clientHeight); + //this.zoom = Math.min(r.clientWidth / this.viewBox.w, r.clientHeight / this.viewBox.h); + }, } } \ No newline at end of file diff --git a/src/components/SceneDesigner/GameObject.vue b/src/components/SceneDesigner/GameObject.vue index 1f8ce36..1c0b1cf 100644 --- a/src/components/SceneDesigner/GameObject.vue +++ b/src/components/SceneDesigner/GameObject.vue @@ -19,6 +19,7 @@ + diff --git a/src/components/SceneDesigner/Scene.vue b/src/components/SceneDesigner/Scene.vue index a1129b0..68aa897 100644 --- a/src/components/SceneDesigner/Scene.vue +++ b/src/components/SceneDesigner/Scene.vue @@ -18,6 +18,7 @@ + diff --git a/src/components/SceneDesigner/Task.vue b/src/components/SceneDesigner/Task.vue index 875961c..458bb84 100644 --- a/src/components/SceneDesigner/Task.vue +++ b/src/components/SceneDesigner/Task.vue @@ -11,15 +11,17 @@ - - - - + + + + + + diff --git a/src/lib/gameEngine.js b/src/lib/gameEngine.js index e32726d..2c6a7f2 100644 --- a/src/lib/gameEngine.js +++ b/src/lib/gameEngine.js @@ -1,33 +1,42 @@ import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/Addons.js'; import { OrbitControls } from 'three/examples/jsm/Addons.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'; class GameEngine { async init(domNode) { - const width = 1200, height = 800; + this.w = domNode.clientWidth || 1200, this.h = domNode.clientHeight || 800; + this.aspect = this.w / this.h + const gameEngine = this; - const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10000); + this.perspectiveCamera = new THREE.PerspectiveCamera(50, this.aspect, 0.01, 1000); this.raycaster = new THREE.Raycaster(); - camera.position.set(1, 0, 1); + 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.light = new THREE.AmbientLight( 0x404040, 0 ); // soft white light // scene.add( this.light ); - function animate(time) { - let delta = clock.getDelta(); - mixer.update(delta); - renderer.render(scene, camera); - controls.update(); - } - const renderer = new THREE.WebGLRenderer({ antialias: true, - alpha: true, - preserveDrawingBuffer: true + // alpha: true, + // preserveDrawingBuffer: true }); renderer.setPixelRatio( window.devicePixelRatio ); // renderer.toneMapping = THREE.CineonToneMapping; @@ -35,16 +44,42 @@ class GameEngine { // renderer.shadowMap.enabled = true; // renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap renderer.outputEncoding = THREE.sRGBEncoding; - const controls = new OrbitControls( camera, renderer.domElement ); + 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 ); + + const controls = new OrbitControls( this.camera, renderer.domElement ); + 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( 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; - renderer.setSize(width, height); + + this.renderType = 'ST'; + + function animate(time) { + let delta = clock.getDelta(); + mixer.update(delta); + gameEngine.render(scene, gameEngine.camera); + gizmo.render(); + } renderer.setAnimationLoop(animate); const mixer = new THREE.AnimationMixer(this.scene); @@ -54,8 +89,6 @@ class GameEngine { this.renderer = renderer; this.scene = scene; this.loader = new GLTFLoader(); - this.camera = camera; - this.controls = controls; this.mixer = mixer; this.activeObjects = new THREE.Group(); @@ -72,10 +105,10 @@ class GameEngine { scene.background = new THREE.Color(1,1,1); console.log('GameEngine started') renderer.domElement.addEventListener('wheel', (event)=>{ - camera.zoom -= event.deltaY / 1000; - camera.zoom = Math.max(camera.zoom, .4); - controls.rotateSpeed = 1 / camera.zoom; - camera.updateProjectionMatrix(); + gameEngine.camera.zoom -= event.deltaY / 1000; + gameEngine.camera.zoom = Math.max(gameEngine.camera.zoom, .4); + controls.rotateSpeed = 1 / gameEngine.camera.zoom; + gameEngine.camera.updateProjectionMatrix(); }) } @@ -142,8 +175,7 @@ class GameEngine { intersect(mouseEvent, domElement, objects, recursive = false, returnInputObjects = true){ let mouse = this.getMouseVector(mouseEvent, domElement); this.raycaster.setFromCamera(mouse, this.camera); - console.log(objects) - let intersects = this.raycaster.intersectObjects(objects, recursive); + let intersects = this.raycaster.intersectObjects(objects.filter(o=>o.visible), recursive); if (returnInputObjects && recursive){ intersects.forEach(o=>{ while (o.object && !objects.includes(o.object)) { @@ -159,6 +191,71 @@ class GameEngine { 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; + 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; + 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} \ No newline at end of file diff --git a/src/pages/games/[[id]].vue b/src/pages/games/[[id]].vue index a478312..b020f99 100644 --- a/src/pages/games/[[id]].vue +++ b/src/pages/games/[[id]].vue @@ -1,10 +1,10 @@