particle system as interactive object

This commit is contained in:
2025-11-05 17:31:21 +02:00
parent 4b722750c3
commit fd3f9a6d69
11 changed files with 165 additions and 84 deletions
@@ -0,0 +1,11 @@
class GenericObject{
constructor(engine, data){
return new Promise(async(resolve, reject)=>{
this.source = await engine.load(data.$go.asset.name);
this.object = this.source.scene;
resolve(this);
})
}
}
export {GenericObject}
@@ -1,10 +1,28 @@
<template> <template>
<div>
<v-textarea :label="l.description" v-model="modelValue.description"></v-textarea>
<v-checkbox v-model="modelValue.hud" label="Observe in head-up display"></v-checkbox>
<v-img :src="`/asset/thumb/${modelValue.go}.webp`" />
<div class="text-caption text-center">{{ modelValue.title }}</div>
</div>
</template> </template>
<script> <script>
export default { export default {
//for now this will be dummy data(){
return {
active: false
}
},
mounted(){
this.active = true;
},
props:{
modelValue: Object
},
methods:{
}
} }
</script> </script>
@@ -1,65 +0,0 @@
import {
Matrix4,
Mesh,
PlaneGeometry,
MeshPhongMaterial,
Vector3,
TextureLoader
} from 'three';
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
class Grass {
constructor(positions, texture, x, y) {
return new Promise((resolve, reject) => {
var tLoader = new TextureLoader();
tLoader.load(texture, (t) => {
//t.encoding = sRGBEncoding;
// create the initial geometry
var geometry = new PlaneGeometry(x || 1, y || 1);
var material = new MeshPhongMaterial({
map: t,
alphaTest: .5,
});
geometry.applyMatrix4(new Matrix4().makeTranslation(0, geometry.parameters.height / 2, 0));
geometry.normalizeNormals();
var arr = [];
for (var i = 0; i < positions.length; i++) {
var position = positions[i];
var baseAngle = Math.PI * 2 * Math.random();
var nPlanes = 2;
for (var j = 0; j < nPlanes; j++) {
var angle = baseAngle + j * Math.PI / nPlanes;
var gg = geometry.clone();
gg.rotateY(angle);
gg.translate(position.x, position.y, position.z);
arr.push(gg);
var gg = geometry.clone();
gg.rotateY(angle + Math.PI);
gg.translate(position.x, position.y, position.z);
arr.push(gg);
}
}
var mesh = new Mesh(BufferGeometryUtils.mergeGeometries(arr, false), material);
resolve(mesh);
arr.forEach(g => g.dispose());
});
});
}
static positions(count, x, z) {
var positions = new Array(count);
for (var i = 0; i < count; i++) {
var position = new Vector3();
position.x = (Math.random() - 0.5) * x;
position.z = (Math.random() - 0.5) * z;
positions[i] = position;
}
return positions;
}
}
export { Grass }
@@ -1,4 +1,6 @@
import { Group } from "three"; import { Group } from "three";
import { GenericObject } from "./GenenricObject";
import { PuzzleGame1 } from "./PuzzleGame1"; import { PuzzleGame1 } from "./PuzzleGame1";
import { PuzzleGame2 } from "./PuzzleGame2"; import { PuzzleGame2 } from "./PuzzleGame2";
// import { Game3 } from "./games/Game3"; // import { Game3 } from "./games/Game3";
@@ -9,9 +11,10 @@ import { TextObject } from "./TextObject";
import { ImageObject } from "./ImageObject"; import { ImageObject } from "./ImageObject";
import { VideoPlayer } from "./VideoPlayer"; import { VideoPlayer } from "./VideoPlayer";
import { MazeQuizGame } from "./MazeQuizGame/MazeQuizGame"; import { MazeQuizGame } from "./MazeQuizGame/MazeQuizGame";
import { Particles } from "./Particles";
import { assignMaterial, assignParams } from "@/lib/MeshUtils"; import { assignMaterial, assignParams } from "@/lib/MeshUtils";
const games = {PuzzleGame1, PuzzleGame2, PuzzleGame4, MazeQuizGame}; const InteractiveObjectsImports = { PuzzleGame1, PuzzleGame2, PuzzleGame4, MazeQuizGame, Particles };
class InteractiveObject { class InteractiveObject {
constructor(obj, gameEngine, params) { constructor(obj, gameEngine, params) {
@@ -58,14 +61,15 @@ class InteractiveObject {
this.source = gltf; this.source = gltf;
break; break;
case 'GenericObject': case 'GenericObject':
this.source = await gameEngine.load(obj.$go.asset.name); let go = await new GenericObject(gameEngine, obj) //await gameEngine.load(obj.$go.asset.name);
this.object = this.source.scene; this.source = go.source;
this.object = go.object;
break; break;
case 'PuzzleGame3': case 'PuzzleGame3':
case 'PuzzleGame4': case 'PuzzleGame4':
case 'PuzzleGame5': case 'PuzzleGame5':
case 'PuzzleGame6': case 'PuzzleGame6':
let game = new games[obj.type](gameEngine, obj.args[0], obj.args[1], obj.args[2]); let game = new InteractiveObjectsImports[obj.type](gameEngine, obj.args[0], obj.args[1], obj.args[2]);
this.object = game.object; this.object = game.object;
this.object.game = game; this.object.game = game;
break; break;
@@ -77,7 +81,8 @@ class InteractiveObject {
case 'PuzzleGame1': case 'PuzzleGame1':
case 'PuzzleGame2': case 'PuzzleGame2':
case 'MazeQuizGame': case 'MazeQuizGame':
this.source = await new games[obj.type](gameEngine, obj); case 'Particles':
this.source = await new InteractiveObjectsImports[obj.type](gameEngine, obj);
this.object = this.source.object; this.object = this.source.object;
break; break;
} }
@@ -96,7 +101,9 @@ const InteractiveObjectTypes = [
id: 'MazeQuizGame', name: 'Maze Quiz Game' id: 'MazeQuizGame', name: 'Maze Quiz Game'
},{ },{
id: 'VideoPlayer', name: 'Video Player' id: 'VideoPlayer', name: 'Video Player'
},{
id: 'Particles', name: 'Particles'
} }
]; ];
export { InteractiveObject, InteractiveObjectTypes } export { InteractiveObject, InteractiveObjectTypes, InteractiveObjectsImports }
@@ -0,0 +1,62 @@
import {
Matrix4,
Mesh,
PlaneGeometry,
MeshPhongMaterial,
Vector3,
} from 'three';
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
class Particles {
constructor(engine, data) {
const { x, y, count, w, h } = data;
let positions = Particles.positions(count, w, h);
return new Promise(async (resolve, reject) => {
let t = await engine.loadTexture(data.$go.asset.name)
var geometry = new PlaneGeometry(x || 1, y || 1);
var material = new MeshPhongMaterial({
map: t,
alphaTest: .5,
});
geometry.applyMatrix4(new Matrix4().makeTranslation(0, geometry.parameters.height / 2, 0));
geometry.normalizeNormals();
var arr = [];
for (var i = 0; i < positions.length; i++) {
var position = positions[i];
var baseAngle = Math.PI * 2 * Math.random();
var nPlanes = 2;
for (var j = 0; j < nPlanes; j++) {
var angle = baseAngle + j * Math.PI / nPlanes;
var gg = geometry.clone();
gg.rotateY(angle);
gg.translate(position.x, position.y, position.z);
arr.push(gg);
var gg = geometry.clone();
gg.rotateY(angle + Math.PI);
gg.translate(position.x, position.y, position.z);
arr.push(gg);
}
}
this.object = new Mesh(BufferGeometryUtils.mergeGeometries(arr, false), material);
resolve(this);
arr.forEach(g => g.dispose());
});
}
static positions(count, x, z) {
let positions = new Array(count);
for (var i = 0; i < count; i++) {
var position = new Vector3();
position.x = (Math.random() - 0.5) * x;
position.z = (Math.random() - 0.5) * z;
positions[i] = position;
}
return positions;
}
}
export { Particles }
@@ -0,0 +1,49 @@
<template>
<v-card v-if="modelValue.go">
<v-card-item>
<v-number-input density="compact" :precision="null" label="Particle width" v-model="modelValue.x"></v-number-input>
<v-number-input density="compact" :precision="null" label="Particle height" v-model="modelValue.y"></v-number-input>
<v-number-input density="compact" :precision="null" label="Area width" v-model="modelValue.w"></v-number-input>
<v-number-input density="compact" :precision="null" label="Area length" v-model="modelValue.h"></v-number-input>
<v-number-input density="compact" label="Count" v-model="modelValue.count"></v-number-input>
<v-img :src="`/asset/thumb/${modelValue.go}.webp`" />
<div class="text-caption text-center">{{ modelValue.title }}</div>
</v-card-item>
</v-card>
<asset-selector @select="assignTexture" :type="['Texture']">
<template v-slot:activator="props">
<v-btn v-bind="props" prepend-icon="mdi-video-box" color="deep-orange-darken-4" block>Choose image object</v-btn>
</template>
</asset-selector>
</template>
<script>
//Grass.positions(1000,50,50), '/static/textures/grass01.png', 1, .5
export default {
data(){
return {
active: false
}
},
mounted(){
this.active = true;
},
props:{
modelValue: Object
},
methods:{
assignTexture(e){
this.modelValue.go = e.id;
this.modelValue.title = e.name
this.modelValue.x = 1;
this.modelValue.y = 1;
this.modelValue.count = 1000;
this.modelValue.w = 50;
this.modelValue.h = 50;
}
}
}
</script>
+2 -2
View File
@@ -18,7 +18,6 @@
</asset-selector> </asset-selector>
<v-form class="pt-4"> <v-form class="pt-4">
<v-text-field density="compact" :label="l.name" v-model="modelValue.title"></v-text-field> <v-text-field density="compact" :label="l.name" v-model="modelValue.title"></v-text-field>
<v-textarea :label="l.description" v-model="modelValue.description"></v-textarea>
<v-text-field density="compact" :label="l.id" v-model="modelValue.id"></v-text-field> <v-text-field density="compact" :label="l.id" v-model="modelValue.id"></v-text-field>
</v-form> </v-form>
</v-card> </v-card>
@@ -36,11 +35,12 @@ import VideoPlayer from '../InteractiveObjects/VideoPlayer.vue';
import PuzzleGame1 from '../InteractiveObjects/PuzzleGame1.vue'; import PuzzleGame1 from '../InteractiveObjects/PuzzleGame1.vue';
import PuzzleGame2 from '../InteractiveObjects/PuzzleGame2.vue'; import PuzzleGame2 from '../InteractiveObjects/PuzzleGame2.vue';
import MazeQuizGame from '../InteractiveObjects/MazeQuizGame/MazeQuizGame.vue'; import MazeQuizGame from '../InteractiveObjects/MazeQuizGame/MazeQuizGame.vue';
import Particles from '../InteractiveObjects/Particles.vue';
import GenericObject from '../InteractiveObjects/GenericObject.vue'; import GenericObject from '../InteractiveObjects/GenericObject.vue';
export default { export default {
emits:['target', 'preview'], emits:['target', 'preview'],
components: { SvgIcon, VideoPlayer, PuzzleGame1, PuzzleGame2, MazeQuizGame, GenericObject }, components: { SvgIcon, GenericObject, VideoPlayer, PuzzleGame1, PuzzleGame2, MazeQuizGame, Particles, },
data(){ data(){
return { return {
active: false active: false
+7 -9
View File
@@ -16,9 +16,8 @@ export class CharacterControls {
walkVelocity = 7 walkVelocity = 7
lerp = (x, y, a) => x * (1 - a) + y * a; lerp = (x, y, a) => x * (1 - a) + y * a;
constructor(model, mixer, animationsMap, engine, currentAction, po, pointerControls) { constructor(model, animationsMap, engine, currentAction, po, pointerControls) {
this.model = model this.model = model
this.mixer = mixer
this.animationsMap = animationsMap this.animationsMap = animationsMap
this.currentAction = currentAction this.currentAction = currentAction
this.animationsMap[currentAction].play() this.animationsMap[currentAction].play()
@@ -91,14 +90,13 @@ export class CharacterControls {
const current = this.animationsMap[this.currentAction] const current = this.animationsMap[this.currentAction]
current.fadeOut(this.fadeDuration) current.fadeOut(this.fadeDuration)
toPlay.timeScale = 0.5; //toPlay.timeScale = 1;
toPlay.reset().fadeIn(this.fadeDuration).play(); toPlay.reset().fadeIn(this.fadeDuration).play();
this.currentAction = play this.currentAction = play
this.actionStart = 0; this.actionStart = 0;
} }
this.mixer.update(delta)
this.actionStart += delta; this.actionStart += delta;
this.cameraDelta += delta * ( pointerControls.cameraLeft * -1 + pointerControls.cameraRight * 1) this.cameraDelta += delta * ( pointerControls.cameraLeft * -1 + pointerControls.cameraRight * 1)
@@ -150,11 +148,6 @@ export class CharacterControls {
cameraPosition.lerp(cameraDesiredPosition, delta*2) cameraPosition.lerp(cameraDesiredPosition, delta*2)
this.camera.position.copy(cameraPosition) this.camera.position.copy(cameraPosition)
// this.camera.lookAt(new THREE.Vector3(
// this.model.position.x,
// 2,
// this.model.position.z
// ))
this.orbitControl.target.set(this.model.position.x, 2, this.model.position.z) this.orbitControl.target.set(this.model.position.x, 2, this.model.position.z)
this.camera.lookAt(this.orbitControl.target) this.camera.lookAt(this.orbitControl.target)
} }
@@ -165,4 +158,9 @@ export class CharacterControls {
pointerControls.moveForward * 1 + pointerControls.moveBackward * -1 pointerControls.moveForward * 1 + pointerControls.moveBackward * -1
] ]
} }
idleReset(){
this.actionStart = 0;
this.currentAction = 'idle'
}
} }
+1
View File
@@ -482,6 +482,7 @@ class GameEngine extends THREE.EventDispatcher{
let mouse = this.getMouseVector(mouseEvent, domElement); let mouse = this.getMouseVector(mouseEvent, domElement);
this.raycaster.setFromCamera(mouse, this.camera); this.raycaster.setFromCamera(mouse, this.camera);
this.clickable.update(mouse, this.camera, mouseEvent); this.clickable.update(mouse, this.camera, mouseEvent);
this.hero?.characterControls?.idleReset();
} }
autoScale(object, mk = 1) { autoScale(object, mk = 1) {
+1 -1
View File
@@ -31,7 +31,7 @@ class Hero{
po.collider.setTranslationWrtParent({ x: 0, y: 2.0, z: 0 }); po.collider.setTranslationWrtParent({ x: 0, y: 2.0, z: 0 });
//po.collider.setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS); //po.collider.setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS);
this.characterControls = new CharacterControls(this.model, this.mixer, this.characterControls = new CharacterControls(this.model,
this.animationsMap, gameEngine, 'idle', po, this.pointerControls) this.animationsMap, gameEngine, 'idle', po, this.pointerControls)
this.clock = new THREE.Clock() this.clock = new THREE.Clock()