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

Trail Particle Performance on Quest #75

Closed marwie closed 2 months ago

marwie commented 6 months ago

Hello,

I recently noticed a huge performance difference on Quest using trail particles - trail particles are currently very slow on Quest to the point where I can't really use them easily in my scenario.

Could that be related to the default maxParticles size being 10x the size of a normal sprite batch? I'm also wondering why the max size is hardcoded for both of these cases? Would it be possible to expose this with the VFXBatchSettings interface? E.g. in my case i want to render a single particle trail following an arrow so I surely don't need that many particles. Also in most cases I'm dealing with particle counts < 100 or < 200

https://github.com/Alchemist0823/three.quarks/blob/dcb9136226196c75630053b66f846f97e082c97f/src/TrailBatch.ts#L38

https://github.com/Alchemist0823/three.quarks/blob/dcb9136226196c75630053b66f846f97e082c97f/src/SpriteBatch.ts#L43

Alchemist0823 commented 6 months ago

the max particle size is just a preallocated buffer. it auto-resizes with the following code:

        if (particleCount > this.maxParticles) {
            this.expandBuffers(particleCount);
        }

we can make it 1000, too. we don't want to make it too small, so we don't reallocate and copy buffer at the first few frames of the animation too often.

Alchemist0823 commented 6 months ago

You can also try to do a profiler on Quest to see why it's very slow. The trail renderer is pretty slow in general. I was working on a webGPU implementation of this. I tested I could use webGPU as compute shader API and webGL as renderer at the same time.

marwie commented 6 months ago

Regarding the preallocation and expanding: I understand that. I'm just curious if this could still be exposed (and by default if nothing is set it can still have your predefined values) - I'm just thinking of cases where we have many particle systems in a scene that may just render a small amount of particles (defined by the user). If I understand it correctly these buffers are owned by each particleSystem and not by the BatchedRenderer, right?

marwie commented 6 months ago

I will check with the profiler and let you know when I have any more information

Alchemist0823 commented 6 months ago

Regarding the preallocation and expanding: I understand that. I'm just curious if this could still be exposed (and by default if nothing is set it can still have your predefined values) - I'm just thinking of cases where we have many particle systems in a scene that may just render a small amount of particles (defined by the user). If I understand it correctly these buffers are owned by each particleSystem and not by the BatchedRenderer, right?

Only BatchedRenderer owns buffers. If the game/app is not optimized, each particle system has its own material and texture, we simulate the movement and shading properties on CPU and write them to batched GPU buffers. In the ideal case for performance, even if you have many particle systems, but many of them could share the same texture atlas and use same shader, so they could be put in a few batches.

Alchemist0823 commented 6 months ago

Batch renders are only good if the number of draw calls is the bottle neck of the application, which is the case for most WebGL based games. if particle count is the bottle neck, the batch render is not an optimization.

marwie commented 6 months ago

I think then one issue on our side could be that the BatchedRenderer is currently created per ParticleSystem instead of being created once and shared.

Alchemist0823 commented 6 months ago

check code in BatchedRenderer.ts, only those conditions are met, we batch them to a single renderer.

private static equals(a: StoredBatchSettings, b: VFXBatchSettings) {
        return (
            a.material.side === b.material.side &&
            a.material.blending === b.material.blending &&
            a.material.transparent === b.material.transparent &&
            a.material.type === b.material.type &&
            a.material.alphaTest === b.material.alphaTest &&
            (a.material as any).map === (b.material as any).map &&
            a.renderMode === b.renderMode &&
            a.uTileCount === b.uTileCount &&
            a.vTileCount === b.vTileCount &&
            a.instancingGeometry === b.instancingGeometry &&
            a.renderOrder === b.renderOrder &&
            a.layers.mask === b.layers.mask
        );
    }
marwie commented 6 months ago

You mean even when creating multiple BatchedParticleRenderers they would still automagically batch particles together if the equals check is met?

Alchemist0823 commented 6 months ago

only if when you create multiple ParticleSystems. users can not create batchedRenderer directly

marwie commented 6 months ago

I'm not exactly sure I understand it correctly. Let's say the following code runs e.g. 5 times:

        this._batchSystem = new BatchedParticleRenderer();
        this._batchSystem.name = this.gameObject.name;
        this._container.add(this._batchSystem);
        this._interface = new ParticleSystemInterface(this);
        this._particleSystem = new _ParticleSystem(this._interface);
        this._particleSystem.addBehavior(new SizeBehaviour(this));
        this._particleSystem.addBehavior(new ColorBehaviour(this));
        this._particleSystem.addBehavior(new TextureSheetAnimationBehaviour(this));
        this._particleSystem.addBehavior(new RotationBehaviour(this));
        this._particleSystem.addBehavior(new VelocityBehaviour(this));
        this._particleSystem.addBehavior(new TrailBehaviour(this));
        this._batchSystem.addSystem(this._particleSystem);

The particle mesh + material is the same for all 5 instances. Would the particles be batched together and rendered in a single drawcall despite 5x new BatchedParticleRenderer() or would this be an anti-pattern and we now loose all optimization of three.quarks and we have 5 draw calls?

Alchemist0823 commented 6 months ago

I'm not exactly sure I understand it correctly. Let's say the following code runs e.g. 5 times:

        this._batchSystem = new BatchedParticleRenderer();
        this._batchSystem.name = this.gameObject.name;
        this._container.add(this._batchSystem);
        this._interface = new ParticleSystemInterface(this);
        this._particleSystem = new _ParticleSystem(this._interface);
        this._particleSystem.addBehavior(new SizeBehaviour(this));
        this._particleSystem.addBehavior(new ColorBehaviour(this));
        this._particleSystem.addBehavior(new TextureSheetAnimationBehaviour(this));
        this._particleSystem.addBehavior(new RotationBehaviour(this));
        this._particleSystem.addBehavior(new VelocityBehaviour(this));
        this._particleSystem.addBehavior(new TrailBehaviour(this));
        this._batchSystem.addSystem(this._particleSystem);

The particle mesh + material is the same for all 5 instances. Would the particles be batched together and rendered in a single drawcall despite 5x new BatchedParticleRenderer() or would this be an anti-pattern and we now loose all optimization of three.quarks and we have 5 draw calls?

yeah only BatchedParticleRenderer batches multiple system. you should create a single batchedParticleRenderer in the entire scene.

marwie commented 6 months ago

Ok thank you.

If i want to remove all particles that belong to one particle system: how would i do that? Is removing the particle system from the renderer enough?

Alchemist0823 commented 6 months ago

Yes. just remove it from the renderer. and remove it from the scene to keep your scene clear as well

Ok thank you.

If i want to remove all particles that belong to one particle system: how would i do that? Is removing the particle system from the renderer enough?

marwie commented 6 months ago

By removing it from the scene you mean the emitter? Will the particles that this system has emitted be removed from being rendered as well when I remove the system from the BatchedRenderer?

Alchemist0823 commented 6 months ago

yes, remove the particle system emitter from the scene. the particle has emitted will be removed if you remove the system from BatchedRender.

marwie commented 6 months ago

Ok thank you for all the information!

marwie commented 6 months ago

Would it be possible to expose the registered ParticleSystems on the BatchedRenderer? Like a getter renderer.systems or getSystems(targetarray) if you're concerned about users modifying the underlying array

The reason for this is that during pre-warming I call update on the batched renderer several times - but I only want to prewarm the newly added particle system

Edit: I guess this can already be archieved by iterating the batches

marwie commented 6 months ago

Another question: I have started to refactor the particlesystem to use one shared batch renderer

When testing this in a scene that shoots arrows and each arrow spawns particles until it gets destroyed and removed from the renderer. My logs show that the batch count however stays the same even after all arrows have been destroyed

when starting the scene and shooting first arrows Capto_Capture 2024-02-27_04-20-41_AM

later when all arrows have been destroyed Capto_Capture 2024-02-27_04-18-34_AM

do you have an idea what could be causing this? I'm double checking that all particles have the same material and mesh

Or is this expected? These particles all share the same geometry (no textures) - i would have expected this would produce only one particle batch that has multiple systems assigned.

The log belows two numbers: batches with any systems and in brackets (batchesRenderer.batches.length)

Capto_Capture 2024-02-27_04-36-54_AM


EDIT: please disregard the comment above. I double checked all values here and when stepping into the method i found that the texture check was causing new batches to be created: https://github.com/Alchemist0823/three.quarks/blob/dcb9136226196c75630053b66f846f97e082c97f/src/BatchedRenderer.ts#L90 After fixing this I dont get more batches than expected anymore

Alchemist0823 commented 6 months ago

Would it be possible to expose the registered ParticleSystems on the BatchedRenderer? Like a getter renderer.systems or getSystems(targetarray) if you're concerned about users modifying the underlying array

The reason for this is that during pre-warming I call update on the batched renderer several times - but I only want to prewarm the newly added particle system

Edit: I guess this can already be archieved by iterating the batches

There is a prewarm option in Particle System. does that work in your case? let me know if doesn't. Another contributor added that. I didn't do extensive tests.