diff --git a/package-lock.json b/package-lock.json index cf4ea74..fadd729 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "eslint-plugin-vue": "^9.27.0", "pinia": "^2.1.7", "sass": "1.77.6", + "troika-three-text": "^0.52.4", "unplugin-auto-import": "^0.17.6", "unplugin-fonts": "^1.1.1", "unplugin-vue-components": "^0.27.2", @@ -2451,6 +2452,16 @@ ], "license": "MIT" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -6461,6 +6472,16 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requireindex": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", @@ -7306,6 +7327,39 @@ "node": ">=14" } }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "dev": true, + "license": "MIT" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -8173,6 +8227,13 @@ } } }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "dev": true, + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index f7e0ae6..a60bf04 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint-plugin-vue": "^9.27.0", "pinia": "^2.1.7", "sass": "1.77.6", + "troika-three-text": "^0.52.4", "unplugin-auto-import": "^0.17.6", "unplugin-fonts": "^1.1.1", "unplugin-vue-components": "^0.27.2", diff --git a/public/static/meshes/b1.png b/public/static/meshes/b1.png new file mode 100644 index 0000000..4f91411 Binary files /dev/null and b/public/static/meshes/b1.png differ diff --git a/public/static/meshes/maze-reed.bin b/public/static/meshes/maze-reed.bin new file mode 100644 index 0000000..f4fc6c1 Binary files /dev/null and b/public/static/meshes/maze-reed.bin differ diff --git a/public/static/meshes/maze-reed.gltf b/public/static/meshes/maze-reed.gltf new file mode 100644 index 0000000..12e926f --- /dev/null +++ b/public/static/meshes/maze-reed.gltf @@ -0,0 +1,389 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.4.55", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0, + 1, + 2, + 3 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"floor", + "translation":[ + 2, + 0, + -0.6499999761581421 + ] + }, + { + "mesh":1, + "name":"tunnel" + }, + { + "mesh":2, + "name":"wall", + "rotation":[ + 0, + 0.7071068286895752, + 0, + 0.7071068286895752 + ], + "translation":[ + 0.6499999761581421, + 0, + -1.100000023841858 + ] + }, + { + "mesh":3, + "name":"door", + "translation":[ + 0, + 0, + -0.44999998807907104 + ] + } + ], + "materials":[ + { + "alphaMode":"BLEND", + "name":"Material", + "pbrMetallicRoughness":{ + "baseColorTexture":{ + "index":0 + }, + "metallicFactor":0, + "roughnessFactor":0.8999999761581421 + } + } + ], + "meshes":[ + { + "name":"Cube.001", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + }, + { + "name":"Curve.003", + "primitives":[ + { + "attributes":{ + "POSITION":4, + "NORMAL":5, + "TEXCOORD_0":6 + }, + "indices":7, + "material":0 + } + ] + }, + { + "name":"Curve.005", + "primitives":[ + { + "attributes":{ + "POSITION":8, + "NORMAL":9, + "TEXCOORD_0":10 + }, + "indices":11, + "material":0 + } + ] + }, + { + "name":"Curve.008", + "primitives":[ + { + "attributes":{ + "POSITION":12, + "NORMAL":13, + "TEXCOORD_0":14 + }, + "indices":15, + "material":0 + } + ] + } + ], + "textures":[ + { + "sampler":0, + "source":0 + } + ], + "images":[ + { + "mimeType":"image/png", + "name":"b1", + "uri":"b1.png" + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":24, + "max":[ + 0.6000000238418579, + 0.009999999776482582, + 0.6000000238418579 + ], + "min":[ + -0.6000000238418579, + -0.030000001192092896, + -0.6000000238418579 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":24, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":24, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":36, + "type":"SCALAR" + }, + { + "bufferView":4, + "componentType":5126, + "count":12, + "max":[ + 0.5994362831115723, + 0.6900395154953003, + 0.40000441670417786 + ], + "min":[ + -0.6005637049674988, + 0.010013699531555176, + -0.3999955654144287 + ], + "type":"VEC3" + }, + { + "bufferView":5, + "componentType":5126, + "count":12, + "type":"VEC3" + }, + { + "bufferView":6, + "componentType":5126, + "count":12, + "type":"VEC2" + }, + { + "bufferView":7, + "componentType":5123, + "count":18, + "type":"SCALAR" + }, + { + "bufferView":8, + "componentType":5126, + "count":4, + "max":[ + 0.600348174571991, + 0.7055734992027283, + -0.04999995976686478 + ], + "min":[ + -0.5996518731117249, + -0.03282167762517929, + -0.04999999329447746 + ], + "type":"VEC3" + }, + { + "bufferView":9, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":10, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":11, + "componentType":5123, + "count":6, + "type":"SCALAR" + }, + { + "bufferView":12, + "componentType":5126, + "count":40, + "max":[ + 0.6068381667137146, + 0.7768232822418213, + 0.05000004917383194 + ], + "min":[ + -0.6075304746627808, + -0.03299999609589577, + -0.050000086426734924 + ], + "type":"VEC3" + }, + { + "bufferView":13, + "componentType":5126, + "count":40, + "type":"VEC3" + }, + { + "bufferView":14, + "componentType":5126, + "count":40, + "type":"VEC2" + }, + { + "bufferView":15, + "componentType":5123, + "count":60, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":288, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":288, + "byteOffset":288, + "target":34962 + }, + { + "buffer":0, + "byteLength":192, + "byteOffset":576, + "target":34962 + }, + { + "buffer":0, + "byteLength":72, + "byteOffset":768, + "target":34963 + }, + { + "buffer":0, + "byteLength":144, + "byteOffset":840, + "target":34962 + }, + { + "buffer":0, + "byteLength":144, + "byteOffset":984, + "target":34962 + }, + { + "buffer":0, + "byteLength":96, + "byteOffset":1128, + "target":34962 + }, + { + "buffer":0, + "byteLength":36, + "byteOffset":1224, + "target":34963 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":1260, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":1308, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":1356, + "target":34962 + }, + { + "buffer":0, + "byteLength":12, + "byteOffset":1388, + "target":34963 + }, + { + "buffer":0, + "byteLength":480, + "byteOffset":1400, + "target":34962 + }, + { + "buffer":0, + "byteLength":480, + "byteOffset":1880, + "target":34962 + }, + { + "buffer":0, + "byteLength":320, + "byteOffset":2360, + "target":34962 + }, + { + "buffer":0, + "byteLength":120, + "byteOffset":2680, + "target":34963 + } + ], + "samplers":[ + { + "magFilter":9729, + "minFilter":9987 + } + ], + "buffers":[ + { + "byteLength":2800, + "uri":"maze-reed.bin" + } + ] +} diff --git a/src/components/GamePlaying/GamePlaying.vue b/src/components/GamePlaying/GamePlaying.vue index e3ddecb..5fae3b3 100644 --- a/src/components/GamePlaying/GamePlaying.vue +++ b/src/components/GamePlaying/GamePlaying.vue @@ -60,6 +60,7 @@ import { Game4 } from '@/components/InteractiveObjects/PuzzleGame4'; import { Grass } from '@/components/InteractiveObjects/Grass'; import { VideoPlayer } from '@/components/InteractiveObjects/VideoPlayer'; import { useAppStore } from '@/stores/app'; +import { MazeQuizGame } from '../InteractiveObjects/MazeQuizGame/MazeQuizGame'; const store = useAppStore(); @@ -205,22 +206,33 @@ export default { } let testGame1 = new Game1(gameEngine, '/static/textures/game1-test.jpg', 2, 3); - gameEngine.activeObjects.add(testGame1.game); - testGame1.game.position.set(0, 1, -15); + gameEngine.activeObjects.add(testGame1.object); + testGame1.object.position.set(0, 1, -15); let testGame2 = new Game2(gameEngine, '/static/textures/game2-test.jpg', 3, 3); - gameEngine.activeObjects.add(testGame2.game); - testGame2.game.position.set(0, 1, 15); - testGame2.game.rotation.y += Math.PI; + gameEngine.activeObjects.add(testGame2.object); + testGame2.object.position.set(0, 1, 15); + testGame2.object.rotation.y += Math.PI; let testGame4 = new Game4(gameEngine, '/static/feathers-game.glb', 3, 4); - gameEngine.activeObjects.add(testGame4.game); - testGame4.game.position.set(15, 1, 5); + gameEngine.activeObjects.add(testGame4.object); + testGame4.object.position.set(15, 1, 5); let vp = new VideoPlayer(gameEngine, this.$refs.videoPlayer, 16, 9); - gameEngine.activeObjects.add(vp.videoPlayer); - vp.videoPlayer.position.set(37, 5.5, 15); - vp.videoPlayer.rotation.y += -Math.PI/2; + gameEngine.activeObjects.add(vp.object); + vp.object.position.set(37, 5.5, 15); + vp.object.rotation.y += -Math.PI/2; + + let maze = new MazeQuizGame(gameEngine, {}, [ + {s: '1 + 1 = 2', h: 'Wrong answer', a: true}, + {s: '1 + 1 = 2', h: 'Wrong answer', a: true}, + {s: '1 + 1 = 2', h: 'Wrong answer', a: true}, + {s: '1 + 1 = 2', h: 'Wrong answer', a: true}, + ]) + maze.load().then(o=>{ + gameEngine.activeObjects.add(o); + o.scale.set(5,5,5); + }) new Grass(Grass.positions(1000,50,50), '/static/textures/grass01.png', 1, .5).then(mesh=>{ console.log('adding grass') diff --git a/src/components/InteractiveObjects/InteractiveObject.js b/src/components/InteractiveObjects/InteractiveObject.js new file mode 100644 index 0000000..c0a7cfe --- /dev/null +++ b/src/components/InteractiveObjects/InteractiveObject.js @@ -0,0 +1,143 @@ +import { ImageObject } from "./ImageObject"; +import { Hint } from "./Hint"; +import { Group, AnimationMixer, LoopPingPong, Vector3 } from "three"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; +import { Utils } from "./Utils"; +import { Game1 } from "./PuzzleGame1"; +import { Game2 } from "./PuzzleGame2"; +// import { Game3 } from "./games/Game3"; +import { Game4 } from "./PuzzleGame4"; +// import { Game5 } from "./games/Game5"; +// import { Game6 } from "./games/Game6"; +import { TextObject } from "./TextObject"; + +const games = {Game1, Game2, Game4}; + +class InteractiveObject { + constructor(obj, context) { + this.name = obj.name; + this.ready = new Promise((resolve, reject) => { + let mesh; + switch (obj.type) { + case 'group': + mesh = new Group(); + obj.group.forEach(g => { + let go = new InteractiveObject(g, context); + go.ready.then((gameMesh) => { + mesh.add(gameMesh); + }); + }); + resolve(mesh); + break; + case 'text': + let text = new TextObject(obj, context); + resolve(text.mesh); + break; + case 'mesh': + mesh = obj.value; + resolve(mesh); + break; + case 'image': + let imo = new ImageObject(obj, context); + mesh = imo.mesh; + resolve(mesh); + break; + case 'hint': + let hint = new Hint(obj, context); + mesh = hint.mesh; + resolve(mesh); + break; + case 'gltf': + new GLTFLoader().load(obj.value, (gltf) => { + let gltfObj = gltf.scene; + gltf.scene.traverse(function (object) { + object.frustumCulled = false; + if (obj.name && obj.name == object.name) { + gltfObj = object; + } + // object.castShadow = true; + // object.receiveShadow = true; + }); + Utils.assignMaterial(gltfObj, obj, context); + if (gltf.animations && gltf.animations.length) { + let mixer = new AnimationMixer(gltfObj); + context.mixers.push(mixer); + let action = mixer.clipAction(gltf.animations[0]); + action.setLoop(LoopPingPong); + action.play(); + } + resolve(gltfObj); + }); + break; + case 'asset': + mesh = context.assets[obj.value].clone(); + Utils.assignMaterial(mesh, obj, context); + resolve(mesh); + break; + case 'Game1': + case 'Game2': + case 'Game3': + case 'Game4': + case 'Game5': + case 'Game6': + var game = new games[obj.type](context, obj.args[0], obj.args[1], obj.args[2]); + mesh = game.game; + mesh.game = game; + resolve(mesh); + break; + } + }); + this.ready.then((mesh) => { + mesh.go = {}; + let restriction; + if (!context.disableRestrictions && obj.restriction) { + restriction = { + type: 'deny', + a: [obj.room.localToWorld(new Vector3().fromArray(obj.restriction[0])), obj.room.localToWorld(new Vector3().fromArray(obj.restriction[1]))] + }; + context.areas.push(restriction); + } + mesh.go.finish = () => { + if (obj.finish) { + var f; + if (obj.finish.nextAction) { + var next = obj.finish.nextAction; + delete obj.finish.nextAction; + f = () => { + if (next.activate) { + context.activate(next.activate); + } + }; + } + var me = obj.finish._ || {}; + delete obj.finish._; + context.motionEngine.add({ o: mesh, a: obj.finish, t: me.t || 1, f: me.f || f, d: me.d || 0 }); + } + if (restriction) context.areas.splice(context.areas.indexOf(restriction), 1); + }; + if (mesh.game) mesh.game.onfinish = mesh.go.finish; + Utils.assignParams(mesh, obj); + obj.animation && context.motionEngine.add({ + o: mesh, + a: obj.animation.motion, + r: obj.animation.repeat, + t: obj.animation.duration || 1 + }); + this.mesh = mesh; + }); + } +} + + +// function textObject(text, context){ +// const geometry = new TextGeometry( text, { +// font: context.font, +// size: .05, +// height: .01, +// curveSegments: 1 +// } ); +// return new Mesh(geometry, context.fontMaterial); +// } + + +export {InteractiveObject} \ No newline at end of file diff --git a/src/components/InteractiveObjects/MazeQuizGame/Maze.js b/src/components/InteractiveObjects/MazeQuizGame/Maze.js new file mode 100644 index 0000000..a98c1c3 --- /dev/null +++ b/src/components/InteractiveObjects/MazeQuizGame/Maze.js @@ -0,0 +1,333 @@ +import { Scene, Clock, PointLight, Group, TextGeometry, MeshStandardMaterial, MeshBasicMaterial, PlaneGeometry, Mesh, TextureLoader, sRGBEncoding, + AnimationMixer, LoopPingPong, Vector3, DirectionalLight, Matrix4, LoopRepeat, EquirectangularReflectionMapping, EquirectangularRefractionMapping } from 'three'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; +import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; +import {FontLoader} from 'three/examples/jsm/loaders/FontLoader'; +import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils'; +import { Clickable } from '../lib/Clickable'; +import { Draggable } from '../lib/Draggable'; +import { GameObject } from '../lib/GameObject'; +import { MotionEngine } from '../lib/MotionEngine'; + +class Maze { + constructor(context) { + const scene = new Scene(); + const motionEngine = new MotionEngine(); + this.context = context; + context.motionEngine = motionEngine; + context.scene = scene; + const clickable = new Clickable(2); + context.clickable = clickable; + context.draggable = new Draggable(2); + + if (context.dashboard) context.dashboard.onpoints = context.onpoints; + + context.wallSize = context.wallSize || .65; + context.tubeSize = context.tubeSize || .8; + context.wallDepth = context.wallDepth || .1; + context.fontPath = context.fontPath || './assets/fonts/ZapfChanceryC.otf'; + scene.background = new TextureLoader().load('./assets/textures/room/a3-2.jpg'); + scene.background.encoding = sRGBEncoding; + scene.background.mapping = EquirectangularRefractionMapping; + + const _tf = { + rotation: { + r: 3 * Math.PI / 2, f: 0, l: Math.PI / 2, b: Math.PI + }, + position: { + r: [-context.wallSize, context.wallSize], + f: [0, 2 * context.wallSize], + l: [context.wallSize, context.wallSize] + }, + pNext: { + r: [-context.wallSize - (context.wallSize + context.wallDepth) / 2, context.wallSize], + f: [0, 2 * context.wallSize + context.wallSize / 2], + l: [context.wallSize + (context.wallSize + context.wallDepth) / 2, context.wallSize] + } + }; + + const _l = new PointLight(0xffffff, 1, 10, 0.5); + let pLoad = []; + const mixers = []; + context.mixers = mixers; + context.assets = {}; + const clock = new Clock(); + context.activate = function (what) { + scene.traverse(o => { + if (o.name == what) { + o.visible = true; + } + }); + }; + + context.gameObject = function (name) { + let result; + scene.traverse(o => { + if (o.name == name) { + result = o; + } + }); + return result; + }; + + const areas = []; + context.areas = areas; + let mazeGeometries = []; + const cameraNear = .2; + let heroDistance = .2; + let o = {}; + var loader = new GLTFLoader().setPath(context.path); + loader.setDRACOLoader(new DRACOLoader().setDecoderPath('./lib/draco/')); + const fontLoader = new FontLoader().setPath(context.path); + pLoad.push(new Promise((resolve, reject) => { + loader.load('maze2.gltf', function (gltf) { + console.log(gltf); + gltf.scene.traverse(function (object) { + if (object.isMesh || object.isObject3D) { + //object.castShadow = true; + //object.receiveShadow = true; + } + if (object.name) { + context.assets[object.name] = object; + } + }); + ['tunnel', 'wall', 'door', 'floor'].forEach(e => { + o[e] = gltf.scene.getObjectByName(e); + }); + resolve(gltf); + }); + })); + + pLoad.push(new Promise((resolve, reject) => { + loader.setPath(context.path + 'human2/'); + loader.load('human.gltf', function (gltf) { + console.log(gltf); + gltf.scene.traverse(function (object) { + if (object.isMesh || object.isObject3D) { + //object.castShadow = true; + //object.receiveShadow = true; + object.frustumCulled = false; + } + }); + o.hero = gltf.scene; + o.hero.scale.set(.033, .033, .033); + o.hero.position.y = 0; + let mixer = new AnimationMixer(gltf.scene); + mixers.push(mixer); + o.hero.actionWalk = mixer.clipAction(gltf.animations.find(a => a.name == 'walk')); + o.hero.actionIdle = mixer.clipAction(gltf.animations.find(a => a.name == 'idle')); + o.hero.actionIdle.play(); + resolve(gltf); + }); + loader.setPath(context.path); + })); + + pLoad.push(new Promise((resolve, reject) => { + fontLoader.load('font.json', function (font) { + context.fontMaterial = new MeshBasicMaterial({ + color: 0x885e2c, + transparent: true, + opacity: 0.73 + }); + context.font = font; + resolve(font); + }); + })); + + const staticThings = new Group(); + const staticPointer = new Mesh( + new PlaneGeometry(.010, .010), + new MeshStandardMaterial({ + map: new TextureLoader().load('./assets/maze/x.png'), + alphaTest: .5, + }) + ); + staticPointer.frustumCulled = false; + staticPointer.material.map.encoding = sRGBEncoding; + staticThings.add(staticPointer); + + scene.add(staticThings); + + Promise.all(pLoad).then(function () { + loadScene(); + context.onload && context.onload(); + }); + + var that = this; + function loadScene() { + mazeObject(context.maze, scene); + scene.add(o.hero); + scene.add(new Mesh(BufferGeometryUtils.mergeBufferGeometries(mazeGeometries, false), o.tunnel.material)); + scene.add(_l); + that.ready = true; + } + + function between(a, b, x) { + return (a < b && a < x && x < b) || (b < a && b < x && x < a); + } + + function checkArea(x, z) { + let allowed = areas.filter(e => e.type != 'deny' && between(e.a[0].x, e.a[1].x, x) && between(e.a[0].z, e.a[1].z, z)); + let denied = areas.filter(e => e.type == 'deny' && between(e.a[0].x, e.a[1].x, x) && between(e.a[0].z, e.a[1].z, z)); + return { area: allowed, block: allowed.length == 0 || denied.length > 0 }; + } + + this.scene = scene; + const _vector = new Vector3(); + let heroState = 0; + let lastCamera = new Vector3(0, .5, 0.5); + this.update = function (camera) { + let currentArea = checkArea(camera.position.x, camera.position.z); + if (currentArea.block) { + if (!checkArea(camera.position.x, lastCamera.z).block) { + camera.position.z = lastCamera.z; + } else if (!checkArea(lastCamera.x, camera.position.z).block) { + camera.position.x = lastCamera.x; + } else { + camera.position.copy(lastCamera); + } + currentArea = checkArea(camera.position.x, camera.position.z); + } + _l.position.copy(camera.position); + context.onupdate && context.onupdate(); + motionEngine.update(); + let delta = clock.getDelta(); + mixers.forEach(m => m.update(delta)); + if (o.hero) { + let dst = Math.abs(camera.position.distanceTo(lastCamera)); + if (dst <= 0.005 && heroState == 1) { + o.hero.actionWalk.crossFadeTo(o.hero.actionIdle.play(), .5); + heroState = 0; + } + else if (dst <= 0.0005 && heroState == 0 && o.hero.actionWalk.isRunning()) { + o.hero.actionWalk.stop(); + } else if (dst > 0.005 && heroState == 0) { + o.hero.actionIdle.crossFadeTo(o.hero.actionWalk.reset().play(), .5); + heroState = 1; + } else if (dst > 0.007 && heroState == 1 && o.hero.actionIdle.isRunning()) { + o.hero.actionIdle.stop(); + } + _vector.setFromMatrixColumn(camera.matrix, 0); + _vector.crossVectors(camera.up, _vector); + + o.hero.position.copy(camera.position); + o.hero.position.addScaledVector(_vector, heroDistance); + o.hero.position.y = 0; + o.hero.rotation.y = camera.rotation.y - Math.PI; + + if (checkArea(o.hero.position.x, o.hero.position.z).block) { + o.hero.visible = false; + } else { + o.hero.visible = true; + } + + staticThings.position.copy(camera.position); + camera.getWorldDirection(_vector); + staticThings.position.addScaledVector(_vector, .6); + staticThings.rotation.copy(camera.rotation); + } + lastCamera.copy(camera.position); + }; + + this.onclick = function (camera, mouse, event) { + mouse = mouse || new Vector3(0, 0, 0); + clickable.update(mouse, camera, event); + }; + + this.onpointer = function (camera, mouse, action) { + mouse = mouse || new Vector3(0, 0, 0); + context.draggable.update(mouse, camera, action); + }; + + var mazeObject = function (def, room, step = 0) { + let offsetZ = 0, e; + def.len = def.len || 0; + if (step == 0) { + e = o.door.geometry.clone(); + e.rotateY(_tf.rotation.f); + mazeGeometries.push(e); + } + for (let i = 0; i < def.len; i++) { + let t = o.tunnel.geometry.clone(); + t.translate(0, 0, i * context.tubeSize + context.wallDepth / 2); + def.matrix && t.applyMatrix4(def.matrix); + + mazeGeometries.push(t); + } + offsetZ = context.wallDepth + def.len * context.tubeSize - context.tubeSize / 2; + if (!def.len) offsetZ = -.275; + areas.push({ + a: [ + room.localToWorld(new Vector3(-context.tubeSize / 2 + cameraNear, 0, -context.tubeSize / 2 - cameraNear)), + room.localToWorld(new Vector3(context.tubeSize / 2 - cameraNear, 0, offsetZ + cameraNear)) + ] + }); + if (def.type == 'area') { + def.area.forEach(ar => { + areas.push({ + a: [ + room.localToWorld(new Vector3(ar[0] + cameraNear, 0, offsetZ + ar[1] - cameraNear)), + room.localToWorld(new Vector3(ar[2] - cameraNear, 0, offsetZ + ar[3] + cameraNear)) + ] + }); + }); + } else { + if (def.noRoom) { + // e = o.wall.geometry.clone(); + // e.rotateY(_tf.rotation.f); + // e.translate(0,0,offsetZ + 0); + // def.matrix && e.applyMatrix4(def.matrix); + // mazeGeometries.push(e); + } else { + e = [o.floor.geometry.clone(), o.door.geometry.clone(), o[def.r ? 'door' : 'wall'].geometry.clone(), + o[def.f ? 'door' : 'wall'].geometry.clone(), o[def.l ? 'door' : 'wall'].geometry.clone()]; + e[0].translate(0, 0, offsetZ + context.wallSize); + + e[1].rotateY(_tf.rotation.b); + e[2].rotateY(_tf.rotation.r); + e[3].rotateY(_tf.rotation.f); + e[4].rotateY(_tf.rotation.l); + + e[1].translate(0, 0, offsetZ + 0); + e[2].translate(-context.wallSize, 0, offsetZ + context.wallSize); + e[3].translate(0, 0, offsetZ + context.wallSize * 2); + e[4].translate(context.wallSize, 0, offsetZ + context.wallSize); + + e.forEach(g => { + def.matrix && g.applyMatrix4(def.matrix); + mazeGeometries.push(g); + }); + areas.push({ + a: [ + room.localToWorld(new Vector3(-context.wallSize + cameraNear, 0, offsetZ + cameraNear)), + room.localToWorld(new Vector3(context.wallSize - cameraNear, 0, offsetZ + context.wallSize * 2 - cameraNear)) + ] + }); + } + } + + def.objects && def.objects.forEach(obj => { + obj.room = room; + let go = new GameObject(obj, context); + go.ready.then(mesh => { + room.add(mesh); + }); + }); + def.room = room; + ['r', 'f', 'l'].forEach((d, i) => { + if (!def[d]) return; + let mtx = new Matrix4(); + mtx.makeRotationY(_tf.rotation[d]); + mtx.setPosition(_tf.pNext[d][0], 0, _tf.pNext[d][1] + offsetZ); + let rr = new Group(); + scene.add(rr); + def[d].matrix = mtx.premultiply(def.matrix || new Matrix4()); + rr.applyMatrix4(def[d].matrix); + rr.updateMatrixWorld(); + mazeObject(def[d], rr, step + 1); + }); + }; + } +} + +export { Maze }; \ No newline at end of file diff --git a/src/components/InteractiveObjects/MazeQuizGame/MazeObject.js b/src/components/InteractiveObjects/MazeQuizGame/MazeObject.js new file mode 100644 index 0000000..ae44783 --- /dev/null +++ b/src/components/InteractiveObjects/MazeQuizGame/MazeObject.js @@ -0,0 +1,147 @@ +import { Group, Vector3, Matrix4, Mesh, DoubleSide } from 'three'; +import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; +import { TextObject } from '../TextObject'; + +class MazeObject { + constructor(engine, def, params = {}){ + let room = new Group(); + let scene = room; + this.object = room; + let context = {}; + context.wallSize = params.wallSize || .65; + context.tubeSize = params.tubeSize || .8; + context.wallDepth = params.wallDepth || .1; + context.fontPath = params.fontPath || '/static/fonts/ZapfChanceryC.otf'; + + const cameraNear = .2; + + this.context = context; + let _tf = { + rotation: { + r: 3 * Math.PI / 2, f: 0, l: Math.PI / 2, b: Math.PI + }, + position: { + r: [-context.wallSize, context.wallSize], + f: [0, 2 * context.wallSize], + l: [context.wallSize, context.wallSize] + }, + pNext: { + r: [-context.wallSize - (context.wallSize + context.wallDepth) / 2, context.wallSize], + f: [0, 2 * context.wallSize + context.wallSize / 2], + l: [context.wallSize + (context.wallSize + context.wallDepth) / 2, context.wallSize] + } + }; + + let o = {}; + let mazeGeometries = [], areas = []; + + this.mazeObject = function(def, room, step = 0) { + let offsetZ = 0, e; + def.len = def.len || 0; + if (step == 0) { + e = o.door.geometry.clone(); + e.rotateY(_tf.rotation.f); + mazeGeometries.push(e); + } + for (let i = 0; i < def.len; i++) { + let t = o.tunnel.geometry.clone(); + t.translate(0, 0, i * context.tubeSize + context.wallDepth / 2); + def.matrix && t.applyMatrix4(def.matrix); + + mazeGeometries.push(t); + } + offsetZ = context.wallDepth + def.len * context.tubeSize - context.tubeSize / 2; + if (!def.len) offsetZ = -.275; + areas.push({ + a: [ + room.localToWorld(new Vector3(-context.tubeSize / 2 + cameraNear, 0, -context.tubeSize / 2 - cameraNear)), + room.localToWorld(new Vector3(context.tubeSize / 2 - cameraNear, 0, offsetZ + cameraNear)) + ] + }); + if (def.type == 'area') { + def.area.forEach(ar => { + areas.push({ + a: [ + room.localToWorld(new Vector3(ar[0] + cameraNear, 0, offsetZ + ar[1] - cameraNear)), + room.localToWorld(new Vector3(ar[2] - cameraNear, 0, offsetZ + ar[3] + cameraNear)) + ] + }); + }); + } else { + if (def.noRoom) { + // e = o.wall.geometry.clone(); + // e.rotateY(_tf.rotation.f); + // e.translate(0,0,offsetZ + 0); + // def.matrix && e.applyMatrix4(def.matrix); + // mazeGeometries.push(e); + } else { + e = [o.floor.geometry.clone(), o.door.geometry.clone(), o[def.r ? 'door' : 'wall'].geometry.clone(), + o[def.f ? 'door' : 'wall'].geometry.clone(), o[def.l ? 'door' : 'wall'].geometry.clone()]; + e[0].translate(0, 0, offsetZ + context.wallSize); + + e[1].rotateY(_tf.rotation.b); + e[2].rotateY(_tf.rotation.r); + e[3].rotateY(_tf.rotation.f); + e[4].rotateY(_tf.rotation.l); + + e[1].translate(0, 0, offsetZ + 0); + e[2].translate(-context.wallSize, 0, offsetZ + context.wallSize); + e[3].translate(0, 0, offsetZ + context.wallSize * 2); + e[4].translate(context.wallSize, 0, offsetZ + context.wallSize); + + e.forEach(g => { + def.matrix && g.applyMatrix4(def.matrix); + mazeGeometries.push(g); + }); + areas.push({ + a: [ + room.localToWorld(new Vector3(-context.wallSize + cameraNear, 0, offsetZ + cameraNear)), + room.localToWorld(new Vector3(context.wallSize - cameraNear, 0, offsetZ + context.wallSize * 2 - cameraNear)) + ] + }); + } + } + + def.objects && def.objects.forEach(obj => { + obj.room = room; + // let go = new GameObject(obj, context); + let go = new TextObject(obj, context) + room.add(go.mesh); + // go.ready.then(mesh => { + // room.add(mesh); + // }); + }); + + def.room = room; + ['r', 'f', 'l'].forEach((d, i) => { + if (!def[d]) return; + let mtx = new Matrix4(); + mtx.makeRotationY(_tf.rotation[d]); + mtx.setPosition(_tf.pNext[d][0], 0, _tf.pNext[d][1] + offsetZ); + let rr = new Group(); + scene.add(rr); + def[d].matrix = mtx.premultiply(def.matrix || new Matrix4()); + rr.applyMatrix4(def[d].matrix); + rr.updateMatrixWorld(); + this.mazeObject(def[d], rr, step + 1); + }); + }; + + this.load = async function(){ + let mazeAsset = await engine.load('/static/meshes/maze-reed.gltf'); + ['tunnel', 'wall', 'door', 'floor'].forEach(e => { + o[e] = mazeAsset.scene.getObjectByName(e); + o[e].frustumCulled = false; + }); + o.tunnel.material.depthWrite = false + this.mazeObject(def, room); + mazeGeometries.forEach(mg=>{ + scene.add(new Mesh(mg, o.tunnel.material)); + }) + //scene.add(new Mesh(BufferGeometryUtils.mergeGeometries(mazeGeometries, false), o.tunnel.material)); + console.log(room); + } + } +} + +export { MazeObject } \ No newline at end of file diff --git a/src/components/InteractiveObjects/MazeQuizGame/MazeQuizGame.js b/src/components/InteractiveObjects/MazeQuizGame/MazeQuizGame.js new file mode 100644 index 0000000..718b608 --- /dev/null +++ b/src/components/InteractiveObjects/MazeQuizGame/MazeQuizGame.js @@ -0,0 +1,48 @@ +import { MazeObject } from "./MazeObject"; + +class MazeQuizGame { + constructor(engine, context, questions) { + let def = this.generate(questions); + console.log(def) + this.mazeObject = new MazeObject(engine, def) + } + + async load(){ + await this.mazeObject.load(); + this.object = this.mazeObject.object; + return this.object; + } + + generate(questions, idx = 0){ + let cq = questions[idx] + if (!cq) return {}; + let len = Math.round(Math.random()*4) + 2; + let lr = Math.round(Math.random()*4) + 2; + let lrv = Math.random() > 0.5; + return { + len, + objects:[ + { + type: 'text', text: cq.s, position:[0,.4,len], rotation:[0,Math.PI, 0] + } + ], + [lrv?'r':'l']:{ + len: 10 - lr, + [lrv?'l':'r']: { + len: lr, + objects:[ + { + type: 'text', text: cq.h, position:[0,.4,lr], rotation:[0,Math.PI, 0] + } + ] + } + }, + [lrv?'l':'r']:{ + len: lr, + [lrv?'r':'l']: this.generate(questions, idx + 1) + } + } + } + +} +export {MazeQuizGame} \ No newline at end of file diff --git a/src/components/InteractiveObjects/PuzzleGame1.js b/src/components/InteractiveObjects/PuzzleGame1.js index 0f39096..e440f13 100644 --- a/src/components/InteractiveObjects/PuzzleGame1.js +++ b/src/components/InteractiveObjects/PuzzleGame1.js @@ -4,7 +4,7 @@ import { MotionEngine } from '../../lib/MotionEngine'; class Game1 { constructor(context, image, w, h) { - this.game = new Group(); + this.object = new Group(); const aq = new MotionEngine(); const pr = [[0, -1], [0, 1], [1, 0], [-1, 0], [0, 0], [0, 2]]; let d = 1.2; @@ -40,20 +40,20 @@ class Game1 { let ri; do { ri = Math.floor(Math.random() * 6); - } while (ri == this.game.children.length); + } while (ri == this.object.children.length); mesh.rotation.set(pr[ri][0] * Math.PI / 2, pr[ri][1] * Math.PI / 2, 0); mesh._ri = ri; - this.game.add(mesh); + this.object.add(mesh); } - this.game.children[0].onBeforeRender = () => { + this.object.children[0].onBeforeRender = () => { this.update(); }; var check = () => { - if (!this.game.children.length) return false; + if (!this.object.children.length) return false; let i = 0; - for (let c of this.game.children) { + for (let c of this.object.children) { if (Math.abs(c.rotation.x - pr[i][0] * Math.PI / 2) > 0.0001 || Math.abs(c.rotation.y - pr[i][1] * Math.PI / 2) > 0.0001) return false; i++; } @@ -71,7 +71,7 @@ class Game1 { } }; - this.game.children.forEach(c => { + this.object.children.forEach(c => { context.clickable.add(c, clickFn); }); @@ -79,7 +79,7 @@ class Game1 { aq.update(); if (aq.isIdle() && !this.done && check()) { this.done = true; - this.game.children.forEach((c, i) => { + this.object.children.forEach((c, i) => { aq.add({ o: c, a: { position: { x: i % w, y: i % h } }, diff --git a/src/components/InteractiveObjects/PuzzleGame2.js b/src/components/InteractiveObjects/PuzzleGame2.js index dd696e6..bb6925d 100644 --- a/src/components/InteractiveObjects/PuzzleGame2.js +++ b/src/components/InteractiveObjects/PuzzleGame2.js @@ -17,7 +17,7 @@ class Game2 { }); let last, lidx = w - 1; - this.game = new Group(); + this.object = new Group(); let d = 1.2, p = []; function check() { @@ -60,7 +60,7 @@ class Game2 { // } p.forEach((e, i) => { let x = e % w, y = ~~(e / h); - this.game.children[i].position.set(x * d, y * d, 0); + this.object.children[i].position.set(x * d, y * d, 0); }); }; @@ -85,11 +85,11 @@ class Game2 { } let mesh = new Mesh(bg, i != lidx ? material : m2); mesh.position.set(x * d, y * d, 0); - this.game.add(mesh); + this.object.add(mesh); } - last = this.game.children[lidx]; + last = this.object.children[lidx]; - this.game.children[0].onBeforeRender = () => { + this.object.children[0].onBeforeRender = () => { this.update(); }; @@ -97,7 +97,7 @@ class Game2 { let clickFn = (i) => { if (!this.done && !aq.isActive(i.object)) { - let idx = this.game.children.indexOf(i.object); + let idx = this.object.children.indexOf(i.object); if (idx == lidx) return; //we ignore the empty cell let xc = p[idx] % w, yc = ~~(p[idx] / h); let xl = p[lidx] % w, yl = ~~(p[lidx] / h); @@ -119,7 +119,7 @@ class Game2 { } }; - this.game.children.forEach(c => { + this.object.children.forEach(c => { context.clickable.add(c, clickFn); }); @@ -127,7 +127,7 @@ class Game2 { aq.update(); if (aq.isIdle() && !this.done && check()) { this.done = true; - this.game.children.forEach((c, i) => { + this.object.children.forEach((c, i) => { last.material = material; aq.add({ o: c, diff --git a/src/components/InteractiveObjects/PuzzleGame4.js b/src/components/InteractiveObjects/PuzzleGame4.js index 64d91e9..95bc064 100644 --- a/src/components/InteractiveObjects/PuzzleGame4.js +++ b/src/components/InteractiveObjects/PuzzleGame4.js @@ -3,7 +3,7 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; import { MotionEngine } from '../../lib/MotionEngine'; var Game4 = function(context, gltf, w, h){ - this.game = new Group(); + this.object = new Group(); const aq = new MotionEngine(); const pr = []; let d = .51, c = w * h / 2, tc = w * h, m0=1, r0=.2; @@ -41,17 +41,17 @@ var Game4 = function(context, gltf, w, h){ pr.forEach((c, i)=>{ c.position.set((i % w)*d , (~~(i / w))*d); c.rotation.set(0, Math.PI, 0); - this.game.add(c); + this.object.add(c); context.clickable.add(c, clickFn); }) - this.game.children[0].onBeforeRender = ()=>{ + this.object.children[0].onBeforeRender = ()=>{ this.update(); } }); var check = ()=>{ - if (!this.game.children.length) return false; + if (!this.object.children.length) return false; return pr.filter(c=>c.$active === false).length == tc; } @@ -107,7 +107,7 @@ var Game4 = function(context, gltf, w, h){ aq.update(); if (!this.done && check()){ this.done = true; - this.game.children.forEach((c, i)=>{ + this.object.children.forEach((c, i)=>{ aq.add({ o: c, a: {material:{opacity:1}}, diff --git a/src/components/InteractiveObjects/TextObject.js b/src/components/InteractiveObjects/TextObject.js new file mode 100644 index 0000000..0658ad9 --- /dev/null +++ b/src/components/InteractiveObjects/TextObject.js @@ -0,0 +1,51 @@ +import { MeshStandardMaterial, Color, Vector3 } from "three"; +import { Text } from "troika-three-text"; +import Utils from "@/lib/utils"; + +class TextObject { + constructor(obj, params) { + const txt = new Text(); + // Set properties to configure: + txt.text = obj.text; + txt.fontSize = 0.022; + txt.lineHeight = 1.1; + txt.maxWidth = obj.width || params.wallSize * .73; + txt.textAlign = 'center'; + txt.font = params.fontPath; + txt.anchorX = 'center'; + txt.anchorY = 'bottom'; + txt.curveRadius = 0; + txt.outlineColor = 0xffffff; + txt.outlineWidth = '15%'; + txt.outlineBlur = '50%'; + Utils.assignMeshParams(txt, obj) + let m = new MeshStandardMaterial({ + roughness: .73, + metalness: .37, + }); + txt.material = m; + txt.color = new Color(0x0); + txt.sync(); + this.txt = txt; + this.mesh = txt; + if (obj.effect == 'distance') { + let dstm = .8; + var oldBR = txt.onBeforeRender; + txt.material[1].opacity = 0.01; + txt.onBeforeRender = function (renderer, scene, camera) { + oldBR && oldBR.apply(this, arguments); + var v = new Vector3(); + txt.getWorldPosition(v); + var dst = camera.position.distanceTo(v); + if (dst < dstm * 2 && dst > dstm * 1) { + txt.material[1].opacity = dstm * 1 - (dst - dstm * 1); + } + if (dst < .5 * dstm) { + txt.material[1].opacity = dstm * dst * 2; + } + }; + } + } +} + +export {TextObject} \ No newline at end of file diff --git a/src/components/InteractiveObjects/VideoPlayer.js b/src/components/InteractiveObjects/VideoPlayer.js index 0516dae..80f5ca9 100644 --- a/src/components/InteractiveObjects/VideoPlayer.js +++ b/src/components/InteractiveObjects/VideoPlayer.js @@ -13,7 +13,7 @@ class VideoPlayer { opacity: 0.5, } ); let plane = new THREE.Mesh( geometry, material ); - this.videoPlayer = plane; + this.object = plane; context.clickable.add(plane, ()=>{ material.opacity = 0.9 diff --git a/src/lib/utils.js b/src/lib/utils.js index a49b4fb..80e5fe9 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -53,5 +53,12 @@ export default { rad2deg(rad){ return rad * 180 / Math.PI; - } + }, + + assignMeshParams(mesh, params){ + ['scale', 'rotation', 'position'].forEach(p=>params[p] && mesh[p].fromArray(params[p])); + ['visible', 'name'].forEach(p=>{ + if (params[p]!==undefined) mesh[p] = params[p]; + }); + }, } \ No newline at end of file