interactive objects integration

This commit is contained in:
2025-11-04 16:41:42 +02:00
parent 429ab07db5
commit 4236927537
13 changed files with 178 additions and 243 deletions
@@ -2,24 +2,24 @@ import { TextureLoader, MeshStandardMaterial, MeshBasicMaterial, PlaneGeometry,
import { assignParams } from "@/lib/MeshUtils";
class ImageObject {
constructor(obj, context) {
var t = new TextureLoader().setPath(context.path).load(obj.value);
constructor(obj, engine, params) {
var t = new TextureLoader().setPath(params.path).load(obj.value);
//t.encoding = sRGBEncoding;
var mp = {
map: t,
alphaTest: 0.5
};
if (obj.nm) {
mp.normalMap = new TextureLoader().setPath(context.path).load(obj.nm);
mp.normalMap = new TextureLoader().setPath(params.path).load(obj.nm);
}
if (obj.em) {
mp.emissiveMap = new TextureLoader().setPath(context.path).load(obj.em);
mp.emissiveMap = new TextureLoader().setPath(params.path).load(obj.em);
}
if (obj.am) {
mp.alphaMap = new TextureLoader().setPath(context.path).load(obj.am);
mp.alphaMap = new TextureLoader().setPath(params.path).load(obj.am);
}
obj.material && Object.assign(mp, obj.material);
let geo = new PlaneGeometry(context.wallSize * (obj.w || 1), context.wallSize * (obj.h || 1));
let geo = new PlaneGeometry(params.wallSize * (obj.w || 1), params.wallSize * (obj.h || 1));
if (obj.uv) {
var uvAttribute = geo.attributes.uv;
for (var i = 0; i < uvAttribute.count; i++) {
@@ -8,37 +8,58 @@ import { PuzzleGame4 } from "./PuzzleGame4";
import { TextObject } from "./TextObject";
import { ImageObject } from "./ImageObject";
import { VideoPlayer } from "./VideoPlayer";
import { MazeQuizGame } from "./MazeQuizGame/MazeQuizGame";
import { assignMaterial, assignParams } from "@/lib/MeshUtils";
const games = {PuzzleGame1, PuzzleGame2, PuzzleGame4};
class InteractiveObject {
constructor(obj, context) {
constructor(obj, gameEngine, params) {
this.name = obj.name;
this.ready = new Promise((resolve, reject) => {
let mesh;
this.ready = new Promise(async (resolve, reject) => {
switch (obj.type || 'GenericObject') {
case 'Group':
mesh = new Group();
obj.group.forEach(g => {
let go = new InteractiveObject(g, context);
go.ready.then((gameMesh) => {
mesh.add(gameMesh);
});
});
resolve(mesh);
this.object = new Group();
for (let g of obj.group){
let go = new InteractiveObject(g, gameEngine);
let gameMesh = await go.ready;
this.object.add(gameMesh.object);
}
break;
case 'Text':
let text = new TextObject(obj, context);
resolve(text.object);
this.source = new TextObject(obj, gameEngine, params);
this.object = this.source.object;
break;
case 'Image':
let imo = new ImageObject(obj, context);
mesh = imo.object;
resolve(mesh);
this.source = new ImageObject(obj, gameEngine, params);
this.object = this.source.object;
break;
case 'Gltf':
let gltf = await gameEngine.load(obj.value);
let gltfObj = gltf.scene;
gltfObj.traverse(function (object) {
object.frustumCulled = false;
if (obj.name && obj.name == object.name) {
gltfObj = object;
}
// object.castShadow = true;
// object.receiveShadow = true;
});
assignMaterial(gltfObj, obj, params);
if (gltf.animations && gltf.animations.length) {
let mixer = new AnimationMixer(gltfObj);
gameEngine.mixers.push(mixer);
let action = mixer.clipAction(gltf.animations[0]);
action.setLoop(LoopPingPong);
action.play();
}
this.object = gltfObj;
this.source = gltf;
break;
case 'GenericObject':
let promise = context.load(`/asset/default/${obj.$go.asset.name}`);
promise.then(resolve);
this.source = await gameEngine.load(`/asset/default/${obj.$go.asset.name}`);
this.object = this.source.scene;
break;
case 'PuzzleGame1':
case 'PuzzleGame2':
@@ -46,19 +67,23 @@ class InteractiveObject {
case 'PuzzleGame4':
case 'PuzzleGame5':
case 'PuzzleGame6':
let game = new games[obj.type](context, obj.args[0], obj.args[1], obj.args[2]);
mesh = game.object;
mesh.game = game;
resolve(mesh);
let game = new games[obj.type](gameEngine, obj.args[0], obj.args[1], obj.args[2]);
this.object = game.object;
this.object.game = game;
break;
case 'VideoPlayer':
let vp = new VideoPlayer(context, `/asset/default/${obj.$go.asset.name}`);
vp.ready.then(resolve);
let vp = new VideoPlayer(gameEngine, `/asset/default/${obj.$go.asset.name}`);
this.source = await vp.ready;
this.object = vp.object;
break;
case 'MazeQuizGame':
this.source = new MazeQuizGame(gameEngine, obj);
await this.source.load();
this.object = this.source.object;
break;
}
});
this.ready.then((mesh) => {
this.object = mesh;
assignParams(this.object, obj);
resolve(this);
});
}
}
@@ -1,6 +1,5 @@
import { Group, Vector3, Matrix4, Mesh, Quaternion, PlaneGeometry, MeshStandardMaterial, DoubleSide, CanvasTexture, SRGBColorSpace } from 'three';
import { Group, Vector3, Matrix4, Mesh, Quaternion, PlaneGeometry, MeshStandardMaterial, DoubleSide} from 'three';
import { InteractiveObject } from '../InteractiveObject';
import Utils from '@/lib/Utils';
class MazeObject {
constructor(engine, def, params = {}){
@@ -39,7 +38,7 @@ class MazeObject {
if (typeof size == 'number'){
size = { width: 0.1, height:1, depth:size/2 }
}
let po = engine.phy.add(
let po = engine.physics.add(
{position: v}, 'fixed', false, undefined, 'cuboid',
{ ...size, isSensor, userData }
)
@@ -51,13 +50,6 @@ class MazeObject {
}
function addRoom(elements, def, offsetZ){
// e = [
// o.floor.clone(),
// o.door.clone(),
// o[def.r ? 'door' : 'wall'].clone(),
// o[def.f ? 'door' : 'wall'].clone(),
// o[def.l ? 'door' : 'wall'].clone()
// ];
let e = elements.map(e=>o[e].clone())
@@ -106,9 +98,6 @@ class MazeObject {
let offsetZ = 0, e;
def.len = def.len || 0;
if (step == 0) {
// e = o.door.clone();
// e.rotateY(_tf.rotation.f);
// mazeMeshes.push(e);
addRoom(['floor', 'wall', 'wall', 'door', 'wall'], def, -context.wallSize)
}
if (def.userData?.answer !== undefined){
@@ -121,15 +110,6 @@ class MazeObject {
root.add(t);
}
offsetZ = def.len * context.tubeSize;
//if (!def.len) offsetZ = -.275;
//room.getWorldQuaternion(quat);
// const geometry = new BoxGeometry(2, 2, offsetZ);
// const cube = new Mesh(geometry, o.tunnel.material)
// cube.position.set(context.tubeSize / 2, 0.6, offsetZ/2)
// root.add(cube);
// console.log(offsetZ, room.localToWorld(new Vector3(context.tubeSize / 2, 0.6, offsetZ/2)))
addPhysics(def.matrix, [context.tubeSize / 2, 0.6, offsetZ/2], offsetZ)
addPhysics(def.matrix, [-context.tubeSize / 2, 0.6, offsetZ/2], offsetZ)
@@ -140,16 +120,12 @@ class MazeObject {
//console.log('loadingggg', def.objects)
def.objects?.forEach(async obj => {
obj.room = room;
// let go = new GameObject(obj, context);
let go = new InteractiveObject(obj, context)
let go = new InteractiveObject(obj, engine, context)
await go.ready;
go.object.scale.multiplyScalar(context.wallSize)
go.object.position.multiplyScalar(context.wallSize)
go.object.applyMatrix4(def.matrix);
root.add(go.object);
// go.ready.then(mesh => {
// room.add(mesh);
// });
});
def.room = room;
@@ -173,17 +149,8 @@ class MazeObject {
o[e] = mazeAsset.scene.getObjectByName(e);
//o[e].frustumCulled = false;
o[e].scale.set(scale, scale, scale)
// o[e].geometry.computeBoundingBox();
// console.log(e, o[e].geometry.boundingBox)
});
this.mazeObject(def, room);
// mazeMeshes.forEach(mesh=>{
// //let mesh = new Mesh(mg, o.tunnel.material)
// root.add(mesh);
// //engine.phy.add(mesh, 'fixed')
// })
//console.log(bbox, 'bbox')
const floorGeometry = new PlaneGeometry(bbox.r - bbox.l + 10*scale, bbox.f + 10*scale);
const floor = new Mesh(floorGeometry,new MeshStandardMaterial({
roughness: 0, metalness:1, color: 0x00ffff, side: DoubleSide
@@ -191,8 +158,6 @@ class MazeObject {
floor.rotation.set(Math.PI/2, 0, 0)
floor.position.set((bbox.l + bbox.r)/2, 0.3, bbox.f/2);
root.add(floor);
//scene.add(new Mesh(BufferGeometryUtils.mergeGeometries(mazeGeometries, false), o.tunnel.material));
//console.log(room);
}
}
}
@@ -3,25 +3,28 @@ import Utils from "@/lib/Utils";
const defaults = {
arrows:{
r: len => ({ type: 'image', value: '/static/textures/arrow.png', position:[-.5,.44,len+.96], rotation:[0,Math.PI, 0], scale: [0.03, 0.03, 0.03] }),
l: len => ({ type: 'image', value: '/static/textures/arrow.png', position:[.5,.44,len+.96], rotation:[0,Math.PI, Math.PI], scale: [0.03, 0.03, 0.03] }),
f: len => ({ type: 'image', value: '/static/textures/arrow.png', position:[0,.7,len+.96], rotation:[0,Math.PI, Math.PI/2], scale: [0.03, 0.03, 0.03] })
r: len => ({ type: 'Image', value: '/static/textures/arrow.png', position:[-.5,.44,len+.96], rotation:[0,Math.PI, 0], scale: [0.03, 0.03, 0.03] }),
l: len => ({ type: 'Image', value: '/static/textures/arrow.png', position:[.5,.44,len+.96], rotation:[0,Math.PI, Math.PI], scale: [0.03, 0.03, 0.03] }),
f: len => ({ type: 'Image', value: '/static/textures/arrow.png', position:[0,.7,len+.96], rotation:[0,Math.PI, Math.PI/2], scale: [0.03, 0.03, 0.03] })
},
answers:{
r: (len, text) => ({ type: 'text', width:0.5, text, fontSize:0.025, position:[-.5,.3,len+.9], rotation:[0,Math.PI, 0] }),
l: (len, text) => ({ type: 'text', width:0.5, text, fontSize:0.025, position:[.5,.3,len+.9], rotation:[0,Math.PI, 0] }),
f: (len, text) => ({ type: 'text', width:0.5, text, fontSize:0.025, position:[0,.55,len+.9], rotation:[0,Math.PI, 0] })
r: (len, text) => ({ type: 'Text', width:0.5, text, fontSize:0.025, position:[-.5,.3,len+.9], rotation:[0,Math.PI, 0] }),
l: (len, text) => ({ type: 'Text', width:0.5, text, fontSize:0.025, position:[.5,.3,len+.9], rotation:[0,Math.PI, 0] }),
f: (len, text) => ({ type: 'Text', width:0.5, text, fontSize:0.025, position:[0,.55,len+.9], rotation:[0,Math.PI, 0] })
}
}
const tl = 4;
class MazeQuizGame {
constructor(engine, context, questions) {
constructor(engine, data) {
let questions = data.shuffle ? Utils.shuffleArray(data.questions) : data.questions;
let def = this.generate(questions);
this.mazeObject = new MazeObject(engine, def)
engine.addEventListener('collision', async e=>{
let ud = engine.phy.world.getCollider(e.handle2)?.parent()?.userData;
let ud1 = engine.physics.world.getCollider(e.handle1)?.parent()?.userData,
ud2 = engine.physics.world.getCollider(e.handle2)?.parent()?.userData;
let ud = {...ud1, ...ud2}
if (ud?.finish){
if (e.started){
engine.dashboard.updateProgress(1)
@@ -53,7 +56,7 @@ class MazeQuizGame {
async load(){
await this.mazeObject.load();
this.object = this.mazeObject.object;
return this.object;
return this;
}
generate(questions, qid = 0, len){
@@ -63,7 +66,7 @@ class MazeQuizGame {
userData: { finish: true },
objects:[
{
type: 'gltf',
type: 'Gltf',
position:[0,.25,len + .52], scale: [0.037, 0.037, 0.037], rotation: [0, Math.PI/4, 0],
value: '/static/meshes/award.glb'
}
@@ -83,12 +86,12 @@ class MazeQuizGame {
len, userData: { question, qid },
objects:[
{
type: 'text', text: question.q, fontSize:0.033, width:0.5, position:[0,.33,len + .96], rotation:[0,Math.PI, 0]
type: 'Text', text: question.q, fontSize:0.033, width:0.5, position:[0,.33,len + .96], rotation:[0,Math.PI, 0]
}
]
}
question.a.forEach((a, i)=>{
question.a.filter(a=>!!a).forEach((a, i)=>{
let d = directions[i];
mo.objects.push(
defaults.arrows[d](len),
@@ -114,7 +117,7 @@ class MazeQuizGame {
len: 2,
objects:[
{
type: 'text', width:0.5, color:0xff0000, text: question.h, fontSize:0.033, position:[0,.44,2+.96], rotation:[0,Math.PI, 0]
type: 'Text', width:0.5, color:0xff0000, text: question.h, fontSize:0.033, position:[0,.44,2+.96], rotation:[0,Math.PI, 0]
}
]
}
@@ -1,11 +1,56 @@
<template>
<div>
MAZE
</div>
<v-checkbox v-model="modelValue.shuffle" label="Shuffle questions"></v-checkbox>
<v-dialog max-width="1400" scrollable>
<template v-slot:activator="{ props: activatorProps }">
<v-btn v-bind="activatorProps">Manage Questions</v-btn>
</template>
<template v-slot:default="{ isActive }">
<v-card>
<v-card-text>
<div class="d-flex flex-wrap w-100">
<v-card v-for="(n, ni) in modelValue.questions" class="v-col-6" variant="outlined" border="0">
<v-card-item>
<v-text-field density="compact" hide-details :label="`Question #${ni+1}`" v-model="n.q" class="py-2">
<template v-slot:append>
<v-btn variant="plain" color="error" @click="deleteQuestion(ni)">
<v-icon icon="mdi-delete-forever"></v-icon>
</v-btn>
</template>
</v-text-field>
<v-text-field hide-details density="compact" v-model="n.a[0]" class="pb-2"
label="Correct answer" icon-color="success" prepend-icon="mdi-check"></v-text-field>
<v-text-field hide-details density="compact" v-model="n.a[1]" class="pb-2"
label="Wrong answer #1" icon-color="error" prepend-icon="mdi-close"></v-text-field>
<v-text-field hide-details density="compact" v-model="n.a[2]" class="pb-2"
label="Wrong answer #2" v-if="n.a[1]" icon-color="error" prepend-icon="mdi-close"></v-text-field>
<v-text-field density="compact" hide-details label="Wrong answer hint" v-model="n.h" class="pb-2"></v-text-field>
</v-card-item>
<v-divider></v-divider>
</v-card>
</div>
</v-card-text>
<v-btn @click="addQuestion" block>Add question</v-btn>
</v-card>
</template>
</v-dialog>
</template>
<script>
export default {
props:['modelValue'],
mounted(){
this.modelValue.questions ??= [];
},
methods:{
addQuestion(){
this.modelValue.questions.push({
q:'', a:['', ''], h:''
})
},
deleteQuestion(idx){
this.modelValue.questions.splice(idx, 1);
}
}
}
</script>
@@ -3,7 +3,7 @@ import { Text } from "troika-three-text";
import { assignParams } from "@/lib/MeshUtils";
class TextObject {
constructor(obj, params) {
constructor(obj, engine, params) {
const txt = new Text();
// Set properties to configure:
txt.text = obj.text;
+1 -1
View File
@@ -82,7 +82,7 @@ export default {
if (e.type == 'InteractiveObject'){
this.modelValue.type = e.id;
}else{
this.modelValue.type = 'Object3D'
this.modelValue.type = 'GenericObject'
}
}
}