Alchemist0823 / three.quarks

Three.quarks is a general purpose particle system / VFX engine for three.js
https://quarks.art
480 stars 22 forks source link

[Question] Multiple objects with the same particle system #87

Closed nrbrttth closed 3 weeks ago

nrbrttth commented 1 month ago

Hey @Alchemist0823!

First of all, neat project you have here, really like it!

I'm trying to build a tab-target game, and in need of a particle system. First thing I tried is to add damage effect to the entities when they get hit. I used the one in the marketplace with a little modification to be white: https://quarks.art/asset/cartoon-energy-explosion

When an entity get hit, I load the effect with the QuarksLoader().load method. But the more I load, the more the FPS drops, and memory usage goes up. I guess it always loads it from scratch. So I wanted to cache the loaded object in some Map, reuse that when I need to add another particle system, and also set the autoDestroy to true for systems, to clean up every effect. This works only partly, because only the "Glow" emitter visible after the first system is emitted.

In the future the entities would have the same effect on them at the same time, like healing or shield effect, so I must sort this out somehow.

My question is how would you go about this?

This is the class that handles particles: ```typescript import { Object3D, Object3DEventMap, Vector3 } from "three" import { BatchedRenderer, ParticleEmitter, ParticleSystem, QuarksLoader } from "three.quarks" import { generateUUID } from "three/src/math/MathUtils" import Game from "../Game" type EffectOptions = { name: string parent?: Object3D position?: Vector3 scale?: number } type Effect = { key: string object: Object3D systems: ParticleSystem[] } export default class ParticleRenderer { private static readonly renderer = new BatchedRenderer() private static readonly loadedEffects = new Map>() static init(): void { Game.scene.add(this.renderer) } static async addEffect(options: EffectOptions): Promise { return new Promise(async (resolve) => { let object: Object3D if (!this.loadedEffects.has(options.name)) { object = await this.loadEffect(options.name) } else { object = (this.loadedEffects.get(options.name) as Object3D).clone() } const key = generateUUID() const systems = new Array() const parent = options.parent || Game.scene object.traverse((child: Object3D) => { if (child.type === 'ParticleEmitter') { systems.push(this.addSystem(child as ParticleEmitter)) } }) if (object.type === 'ParticleEmitter') { systems.push(this.addSystem(object as ParticleEmitter)) } if (options.position) { object.position.copy(options.position) } if (options.scale) { object.scale.set(options.scale, options.scale, options.scale) } parent.add(object) resolve({ key, object, systems }) }) } static removeEffect(effect: Effect): void { effect.systems.forEach((system: ParticleSystem) => this.renderer.deleteSystem(system)) Game.scene.remove(effect.object) } static update(): void { this.renderer.update(Game.deltaTime) } private static async loadEffect(name: string): Promise> { return new Promise((resolve) => { new QuarksLoader().load(`/assets/particle-fx/${name}.json`, (object: Object3D) => { this.loadedEffects.set(name, object.clone()) resolve(object) }) }) } private static addSystem(object: ParticleEmitter): ParticleSystem { const system = object.system as ParticleSystem system.renderOrder = 1 system.autoDestroy = true this.renderer.addSystem(system) return system } } ```
Alchemist0823 commented 1 month ago

Great Questions. There are a lot of things to answer.

"This works only partly, because only the "Glow" emitter visible after the first system is emitted. In the future the entities would have the same effect on them at the same time, like healing or shield effect, so I must sort this out somehow." Can you be more explicit about the effects? what properties of particle system will make your reuse system not work?

We will put out more example projects in the future.

nrbrttth commented 1 month ago

I don't quite understand your question.

What I'm trying to achieve is if it's a first time loading a specific json, then cache the object provided by QuarksLoader().load, so I don't need to load it again. It's partly working though, because when I use the cached object to add a particle system, only the GlowEmitter is played, SparksEmitter does not, even though the object is cloned.

I'll put together an example later today, to show you my issue.

Update: @Alchemist0823 here is the codesandbox: https://codesandbox.io/p/sandbox/three-js-forked-zzcytg

Alchemist0823 commented 1 month ago

Hey @nrbrttth, This is how u reuse particle system, I added an utility class recently:

const effect = obj.clone(true);
scene.add(effect);
QuarksUtil.setAutoDestroy(effect, true);
QuarksUtil.addToBatchRenderer(effect, batchSystem);
QuarksUtil.play(effect);
sintanial commented 1 month ago

@Alchemist0823 Hi, thx for this great library, it awesome. I have a question about autoDestroy. After effect was ended, i saw that Object3D do not removed from scene. Maybe i don't understand how autoDestroy must work, but in my opinion it must remove object from scene after animation was ended.

Alchemist0823 commented 1 month ago

@Alchemist0823 Hi, thx for this great library, it awesome. I have a question about autoDestroy. After effect was ended, i saw that Object3D do not removed from scene. Maybe i don't understand how autoDestroy must work, but in my opinion it must remove object from scene after animation was ended.

It doesn't remove any Object3D without particle systems. it only removes ParticleEmitter children in the object hierarchy. I just added the addEventListener API. Now you can track the destroy event in your code to know when the particle systems are destroyed.

sintanial commented 3 weeks ago

@Alchemist0823 Wow, it's awesome, big thx for that. Because before your update, I was implement ugly hack like this ))):

export function setEffectCleaner(scene: THREE.Scene, effect: THREE.Object3D, timeout: number = 50) {
    const interval = setInterval(() => {
        let isMarkForDestroy = true;
        effect.traverse(child => {
            if (child.type != "ParticleEmitter") return;
            const c = child as any;
            if (c.system.markForDestroy === false) {
                isMarkForDestroy = false;
            }
        });

        if (isMarkForDestroy) {
            removeObjectFromScene(scene, effect);
            clearInterval(interval);
        }
    }, timeout);
}