add physics to interactive objects

This commit is contained in:
2025-11-08 17:08:51 +02:00
parent 168fb5b770
commit f5a08a9702
10 changed files with 67 additions and 36 deletions
@@ -20,6 +20,7 @@
<script> <script>
import { GameEngine } from '@/lib/GameEngine.js'; import { GameEngine } from '@/lib/GameEngine.js';
import { autoScale } from '@/lib/MeshUtils';
let gameEngine = null; let gameEngine = null;
export default{ export default{
@@ -75,7 +76,7 @@ export default{
this.animations = gltf.animations.map(a => ({ this.animations = gltf.animations.map(a => ({
name: a.name, id: a.uuid name: a.name, id: a.uuid
})); }));
gameEngine.autoScale(gltf.scene); autoScale(gltf.scene);
let bb = new gameEngine.$.Box3().setFromObject(gltf.scene); let bb = new gameEngine.$.Box3().setFromObject(gltf.scene);
gltf.scene.traverse(function (o) { gltf.scene.traverse(function (o) {
o.frustumCulled = false; o.frustumCulled = false;
@@ -1,10 +1,10 @@
import { getBoundingBox, getBoundingBoxCenterPoint, getBoundingBoxMaxLength } from "@/lib/MeshUtils"; import { getBoundingBox, getBoundingBoxCenterPoint, getBoundingBoxMaxLength, centerOrigin } from "@/lib/MeshUtils";
class GenericObject{ class GenericObject{
constructor(engine, data){ constructor(engine, data){
return new Promise(async(resolve, reject)=>{ return new Promise(async(resolve, reject)=>{
this.source = await engine.load(data.$go.asset.name); this.source = await engine.load(data.$go.asset.name);
this.object = this.source.scene; this.object = centerOrigin(this.source.scene)
if (!data.exclude){ if (!data.exclude){
engine.clickable.add(this.object, async e=>{ engine.clickable.add(this.object, async e=>{
@@ -3,6 +3,7 @@
<v-textarea :label="l.description" v-model="modelValue.description"></v-textarea> <v-textarea :label="l.description" v-model="modelValue.description"></v-textarea>
<v-checkbox density="compact" v-model="modelValue.hud" hide-details label="Observe in head-up display"></v-checkbox> <v-checkbox density="compact" v-model="modelValue.hud" hide-details label="Observe in head-up display"></v-checkbox>
<v-checkbox density="compact" v-model="modelValue.exclude" hide-details label="Disable interactions"></v-checkbox> <v-checkbox density="compact" v-model="modelValue.exclude" hide-details label="Disable interactions"></v-checkbox>
<v-checkbox density="compact" v-model="modelValue.noPhysics" hide-details label="Disable physics"></v-checkbox>
<v-img :src="`/asset/thumb/${modelValue.go}.webp`" /> <v-img :src="`/asset/thumb/${modelValue.go}.webp`" />
<div class="text-caption text-center">{{ modelValue.title }}</div> <div class="text-caption text-center">{{ modelValue.title }}</div>
</div> </div>
@@ -41,6 +41,7 @@ export default {
props:['modelValue'], props:['modelValue'],
mounted(){ mounted(){
this.modelValue.questions ??= []; this.modelValue.questions ??= [];
this.modelValue.noPhysics = true;
}, },
methods:{ methods:{
addQuestion(){ addQuestion(){
@@ -43,6 +43,7 @@ export default {
this.modelValue.count = 1000; this.modelValue.count = 1000;
this.modelValue.w = 50; this.modelValue.w = 50;
this.modelValue.h = 50; this.modelValue.h = 50;
this.modelValue.noPhysics = true;
} }
} }
} }
@@ -1,11 +1,12 @@
import { BoxGeometry, Mesh, MeshBasicMaterial, Group } from 'three'; import { BoxGeometry, Mesh, MeshBasicMaterial, Group } from 'three';
import { MotionEngine } from '../../lib/MotionEngine'; import { MotionEngine } from '../../lib/MotionEngine';
import { centerOrigin } from '@/lib/MeshUtils';
class PuzzleGame1 { class PuzzleGame1 {
constructor(engine, data) { constructor(engine, data) {
return new Promise(async (resolve, reject)=>{ return new Promise(async (resolve, reject)=>{
let w = data.w, h = data.h, wh = w*h; let w = data.w, h = data.h, wh = w*h;
this.object = new Group(); let container = new Group();
const aq = new MotionEngine(); const aq = new MotionEngine();
const pr = [[0, 2], [0, -1], [0, 1], [1, 0], [-1, 0], [0, 0]]; const pr = [[0, 2], [0, -1], [0, 1], [1, 0], [-1, 0], [0, 0]];
let d = 1.2; let d = 1.2;
@@ -50,16 +51,16 @@ class PuzzleGame1 {
if (idxs[4] == idxs[5]){ if (idxs[4] == idxs[5]){
mesh._dd = true; mesh._dd = true;
} }
this.object.add(mesh); container.add(mesh);
} }
this.object.children[0].onBeforeRender = () => { container.children[0].onBeforeRender = () => {
this.update(); this.update();
}; };
var check = () => { var check = () => {
let i = 0; let i = 0;
this.object.children.forEach(o=>{ container.children.forEach(o=>{
if (o._ri == 5 || o._dd && o._ri == 0) i++; if (o._ri == 5 || o._dd && o._ri == 0) i++;
}) })
return i == wh; return i == wh;
@@ -76,7 +77,7 @@ class PuzzleGame1 {
} }
}; };
this.object.children.forEach(c => { container.children.forEach(c => {
engine.clickable.add(c, clickFn); engine.clickable.add(c, clickFn);
}); });
@@ -84,18 +85,20 @@ class PuzzleGame1 {
aq.update(); aq.update();
if (aq.isIdle() && !this.done && check()) { if (aq.isIdle() && !this.done && check()) {
this.done = true; this.done = true;
this.object.children.forEach((c, i) => { container.children.forEach((c, i) => {
aq.add({ aq.add({
o: c, o: c,
a: { position: { x: i % w, y: Math.trunc(i/w)} }, a: { position: { x: i % w, y: Math.trunc(i/w)} },
t: 1, t: 1,
f: i == 0 && this.onfinish f: i == 0 && this.onfinish || undefined
}); });
}); });
//engine.dashboard.addPoints(10); //engine.dashboard.addPoints(10);
} }
}; };
this.object = centerOrigin(container);
resolve(this); resolve(this);
}) })
} }
@@ -1,5 +1,6 @@
import { BoxGeometry, Mesh, MeshBasicMaterial, Group } from 'three'; import { BoxGeometry, Mesh, MeshBasicMaterial, Group } from 'three';
import { MotionEngine } from '../../lib/MotionEngine'; import { MotionEngine } from '../../lib/MotionEngine';
import { centerOrigin } from '@/lib/MeshUtils';
class PuzzleGame2 { class PuzzleGame2 {
constructor(engine, data) { constructor(engine, data) {
@@ -17,7 +18,7 @@ class PuzzleGame2 {
}); });
let last, lidx = w - 1; let last, lidx = w - 1;
this.object = new Group(); let container = new Group();
let d = 1.2, p = []; let d = 1.2, p = [];
function check() { function check() {
@@ -60,7 +61,7 @@ class PuzzleGame2 {
// } // }
p.forEach((e, i) => { p.forEach((e, i) => {
let x = e % w, y = ~~(e / h); let x = e % w, y = ~~(e / h);
this.object.children[i].position.set(x * d, y * d, 0); container.children[i].position.set(x * d, y * d, 0);
}); });
}; };
@@ -89,11 +90,11 @@ class PuzzleGame2 {
} }
let mesh = new Mesh(bg, i != lidx ? material : m2); let mesh = new Mesh(bg, i != lidx ? material : m2);
mesh.position.set(x * d, y * d, 0); mesh.position.set(x * d, y * d, 0);
this.object.add(mesh); container.add(mesh);
} }
last = this.object.children[lidx]; last = container.children[lidx];
this.object.children[0].onBeforeRender = () => { container.children[0].onBeforeRender = () => {
this.update(); this.update();
}; };
@@ -101,7 +102,7 @@ class PuzzleGame2 {
let clickFn = (i) => { let clickFn = (i) => {
if (!this.done && !aq.isActive(i.object)) { if (!this.done && !aq.isActive(i.object)) {
let idx = this.object.children.indexOf(i.object); let idx = container.children.indexOf(i.object);
if (idx == lidx) return; //we ignore the empty cell if (idx == lidx) return; //we ignore the empty cell
let xc = p[idx] % w, yc = ~~(p[idx] / h); let xc = p[idx] % w, yc = ~~(p[idx] / h);
let xl = p[lidx] % w, yl = ~~(p[lidx] / h); let xl = p[lidx] % w, yl = ~~(p[lidx] / h);
@@ -122,7 +123,7 @@ class PuzzleGame2 {
} }
}; };
this.object.children.forEach(c => { container.children.forEach(c => {
engine.clickable.add(c, clickFn); engine.clickable.add(c, clickFn);
}); });
@@ -130,7 +131,7 @@ class PuzzleGame2 {
aq.update(); aq.update();
if (aq.isIdle() && !this.done && check()) { if (aq.isIdle() && !this.done && check()) {
this.done = true; this.done = true;
this.object.children.forEach((c, i) => { container.children.forEach((c, i) => {
last.material = material; last.material = material;
aq.add({ aq.add({
o: c, o: c,
@@ -142,6 +143,7 @@ class PuzzleGame2 {
//engine.dashboard.addPoints(10); //engine.dashboard.addPoints(10);
} }
}; };
this.object = centerOrigin(container)
resolve(this) resolve(this)
}); });
} }
+24 -6
View File
@@ -1,4 +1,4 @@
import { TextureLoader, Box3, Vector3 } from "three"; import { TextureLoader, Box3, Vector3, Group } from "three";
function assignParams(mesh, params){ function assignParams(mesh, params){
['scale', 'rotation', 'position'].forEach(p=>params[p] && mesh[p].fromArray(params[p])); ['scale', 'rotation', 'position'].forEach(p=>params[p] && mesh[p].fromArray(params[p]));
@@ -30,16 +30,22 @@ function getBoundingBox(object){
return new Box3().setFromObject(object); return new Box3().setFromObject(object);
} }
function getBoundingBoxSize(bb){
return new Vector3(bb.max.x - bb.min.x, bb.max.y - bb.min.y, bb.max.z - bb.min.z);
}
function getBoundingBoxMaxLength(bb){ function getBoundingBoxMaxLength(bb){
return Math.max(bb.max.x - bb.min.x, bb.max.y - bb.min.y, bb.max.z - bb.min.z) let size = getBoundingBoxSize(bb)
return Math.max(size.x, size.y, size.z)
} }
function getBoundingBoxCenterPoint(bb, relativeTo){ function getBoundingBoxCenterPoint(bb, relativeTo){
relativeTo = relativeTo || new Vector3(0,0,0) relativeTo = relativeTo || new Vector3(0,0,0)
let size = getBoundingBoxSize(bb)
return new Vector3( return new Vector3(
bb.min.x + (bb.max.x - bb.min.x)/2 - relativeTo.x, bb.min.x + (size.x)/2 - relativeTo.x,
bb.min.y + (bb.max.y - bb.min.y)/2 - relativeTo.y, bb.min.y + (size.y)/2 - relativeTo.y,
bb.min.z + (bb.max.z - bb.min.z)/2 - relativeTo.z bb.min.z + (size.z)/2 - relativeTo.z
) )
} }
@@ -49,4 +55,16 @@ function autoScale(object, mk = 1) {
object.scale.multiplyScalar(mk / k); object.scale.multiplyScalar(mk / k);
} }
export { assignParams, assignMaterial, autoScale, getBoundingBox, getBoundingBoxMaxLength, getBoundingBoxCenterPoint } function centerOrigin(object){
let result = new Group();
let bb = getBoundingBox(object);
let position = getBoundingBoxCenterPoint(bb, object.position).negate();
object.position.copy(position)
result.add(object);
return result;
}
export {
assignParams, assignMaterial, autoScale, centerOrigin,
getBoundingBox, getBoundingBoxSize, getBoundingBoxMaxLength, getBoundingBoxCenterPoint
}
+7 -11
View File
@@ -20,9 +20,10 @@ class Physics{
return this; return this;
} }
add = (mesh, rigidBodyType, autoAnimate = true, postPhysicsFn, colliderType, colliderSettings) => { add = (mesh, rigidBodyType, autoAnimate = true, postPhysicsFn, colliderType, colliderSettings = {}) => {
const rigidBodyDesc = RAPIER.RigidBodyDesc[rigidBodyType]() const rigidBodyDesc = RAPIER.RigidBodyDesc[rigidBodyType]()
rigidBodyDesc.setTranslation(mesh.position.x, mesh.position.y, mesh.position.z) rigidBodyDesc.setTranslation(mesh.position.x, mesh.position.y, mesh.position.z)
mesh.quaternion && rigidBodyDesc.setRotation(mesh.quaternion)
// * Responsible for collision response // * Responsible for collision response
const rigidBody = this.world.createRigidBody(rigidBodyDesc) const rigidBody = this.world.createRigidBody(rigidBodyDesc)
@@ -70,6 +71,7 @@ class Physics{
// * Responsible for collision detection // * Responsible for collision detection
const collider = this.world.createCollider(colliderDesc, rigidBody) const collider = this.world.createCollider(colliderDesc, rigidBody)
mesh.quaternion && collider.setRotationWrtParent(mesh.quaternion)
const physicsObject = { mesh, collider, rigidBody, fn: postPhysicsFn, autoAnimate } const physicsObject = { mesh, collider, rigidBody, fn: postPhysicsFn, autoAnimate }
this.physicsObjects.push(physicsObject) this.physicsObjects.push(physicsObject)
@@ -89,17 +91,11 @@ class Physics{
this.world.step(this.eventQueue) this.world.step(this.eventQueue)
for (let po of this.physicsObjects) { for (let po of this.physicsObjects) {
const autoAnimate = po.autoAnimate if (po.autoAnimate) {
po.mesh.position.copy(po.rigidBody.translation())
if (autoAnimate) { po.mesh.quaternion.copy(po.rigidBody.rotation() )
const mesh = po.mesh
const collider = po.collider
mesh.position.copy(collider.translation())
mesh.quaternion.copy(collider.rotation() )
} }
po.fn?.()
const fn = po.fn
fn && fn()
} }
this.eventQueue.drainCollisionEvents((handle1, handle2, started) => { this.eventQueue.drainCollisionEvents((handle1, handle2, started) => {
+9 -1
View File
@@ -2,7 +2,7 @@ import { InteractiveObject } from '@/components/InteractiveObjects/InteractiveOb
import { VideoPlayer } from '@/components/InteractiveObjects/VideoPlayer'; import { VideoPlayer } from '@/components/InteractiveObjects/VideoPlayer';
import { GameEngine } from '@/lib/GameEngine'; import { GameEngine } from '@/lib/GameEngine';
import { Hero } from '@/lib/Hero'; import { Hero } from '@/lib/Hero';
import { autoScale } from '@/lib/MeshUtils'; import { autoScale, getBoundingBox, getBoundingBoxCenterPoint, getBoundingBoxSize } from '@/lib/MeshUtils';
let gameEngine = null; let gameEngine = null;
export default { export default {
@@ -151,6 +151,14 @@ export default {
if (io.source?.animations?.length){ if (io.source?.animations?.length){
gameEngine.playAnimation(gameEngine.scene, io.source.animations[0]); gameEngine.playAnimation(gameEngine.scene, io.source.animations[0]);
} }
if (!i.data.noPhysics){
let bb = getBoundingBox(io.object);
let bbs = getBoundingBoxSize(bb);
gameEngine.physics.add(io.object, 'fixed', false, undefined, 'capsule', {
radius: Math.max(bbs.x, bbs.z)/2, halfHeight: bbs.y/2
})
}
} }
} }
} }