squarefeet / ShaderParticleEngine

A GLSL-heavy particle engine for THREE.js. Originally based on Stemkoski's great particle engine (see README)
http://squarefeet.github.io/ShaderParticleEngine/
MIT License
850 stars 115 forks source link

Rewrite/v2.0 General Discussion Thread #50

Open squarefeet opened 10 years ago

squarefeet commented 10 years ago

Following incredibly valid points from @usnul and @cihadturhan, in order to get this library into the state that it deserves, a rewrite would appear to be necessary. This thread is for anyone to add their 2-cents/pennies/other-currency worth of thoughts on improvements, new features, etc. The more critical, the better (IMHO) - that's one of the major ways to ensure the new version is as easy-to-use and performant as possible.

I do ask that any requests/comments should follow the same format as below: one feature per bullet-point, and grouped into either Required or Nice-to-Have (or both, if you're feeling generous).

Thanks!


Required

Nice-to-haves

Usnul commented 9 years ago

yup, looks the same. Looks awesome actually :)

Usnul commented 9 years ago

have you seen this? http://david.li/disintegration/ light/shadow related

squarefeet commented 9 years ago

I haven't... super impressive stuff. I came across his Flow stuff before, though. It's interesting how he does an incremental odd-even merge sort for the particles... Must look into that!

Usnul commented 9 years ago

There are also webworkers to assist with sorting, even mobile devices have more than 1 core. Allowing sorting to be incomplete could produce visual artifacts, but with large number of particles this may be an acceptable trade-off, considering you'd only need this when number of particles is large :)

squarefeet commented 9 years ago

Ah-ha, good call... I'll take a look at how best that could be implemented :)

squarefeet commented 9 years ago

Okie dokie, here's a quick update on the (latest) rewrite:

// Create emitter
var emitter = new Emitter();

// Set acceleration value. Behind the scenes, this sets 
// define values in the shader to 'activate' the acceleration 
// module. 
emitter.acceleration = new THREE.Vector3( 1, 2, 3 );
// Create emitter
var emitter = new Emitter();

// Add a module.
// This style allows for much easier customisation. Custom emitters 
// with custom GLSL calculations are SO easy, now!
emitter.addModule( new AccelerationModule( {
    value: new THREE.Vector3( 1, 2, 3 )
} );

// Repeat adding various modules until emitter is complete.
// Unlike the rewrite before this, no need to call `emitter.prepare();` 
// after all modules have been added.

So, I'm pretty sure I've hit a little bit of a jackpot re. combining the best bits from previous rewrites. I've just got to stabilise the emitter code, then tackle a few important internal issues:


Once I've rewritten the remaining modules into the new format, I'll definitely whack this up on Github, I promise! As ever, any questions, comments, or criticisms welcome :)

Usnul commented 9 years ago

@squarefeet

emitter.addModule( new AccelerationModule( {
     value: new THREE.Vector3( 1, 2, 3 )
} );

I like this, "module" feels a bit off though. If you already have the whole shader compiled and only defs are being manipulated, maybe consider using a flag instead:

emitter.accelerationEnabled = false

just a thought though.

How best to "group" emitters? The more I think about it, the less I'm keen on having grouping happen automatically. If it happened automatically, how would new THREE.PointCloud instances be added to the scene? One could pass in the scene as an argument to an Emitter instance, but that feels clunky and a bit silly... I'm more keen on having grouping happen manually. Much easier control for the end user, that way.

  • using a group for multiple emitters can allow for fewer draw calls (less instances of geometry), if we know span rate and we know maxAge - it's pretty easy to determine maximum pool size (i.e. how many vertices we need for geometry) Other than using grouping to get better performance for multiple emitters using same particle material - there's a possible place to group various different emitters, like say for fire effect you could have "smoke","flame" and "embers" emitters, being co-located. Just throwing ideas out there.

How best to determine when shader "combining" should happen?

  • i'm not sure what this means. Could you elaborate?

Should I use a THREE.WebGLRenderTargetCube to render 6 different scene depths in order to do much better collision detection? Failing that, would a camera looking in the opposite direction to the scene's camera and translated on the inverse z-axis (rendering just the 'back' of the scene) be enough?

  • i'd say "no", short of doing world-space simulation, you will have error always, using already existing depth buffer is 0 overhead, and thus a pure win, adding 6 more draws even just for depth is quite considerable. It could be a nice "addon", but you'd be making a very significant performance trade, and sometimes it can be justified.

Since there are issues using the spawn shader when either spawnRate or maxAge is < 1.0, I'm considering swapping to CPU spawn calculation for this. This swap will be automatic, but will require using attributes to determine whether a particle is alive or not within the other shaders. This will need to be written, but can easily be wrapped in another #ifdef to allow state-swapping to easily occur. Also considering having a user-controllable option to force CPU spawn calculation.

  • what storage type is used for these? I think it's fair enough, but the problem is probably in floating point rounding error somewhere.
squarefeet commented 9 years ago

I like this, "module" feels a bit off though. If you already have the whole shader compiled and only defs are being manipulated, maybe consider using a flag instead.

I hear that, but with the module style, it will allow for custom modules to be created really easily... Once it's up on Github and you can see the Module code, I daresay you might agree :) I do see the issue about enabling/disabling modules, though: I seem to have added that functionality without properly thinking about how it would be exposed. The flag idea seems to be a fairly good solution. I'll experiment with it and see how it feels.

...say for fire effect you could have "smoke","flame" and "embers" emitters, being co-located. Just throwing ideas out there.

Good shout. I'll think a bit more about grouping, as I daresay it will affect some of the internals quite a lot. Something to do as soon as the Module stuff is fully supported by the Emitter.

How best to determine when shader "combining" should happen? By this, I was thinking aloud re. whether to do, and if so when, the shader 'combining' should happen, i.e. combining the position and velocity shaders, or using the BigDaddy shader. I was just wondering what emitter 'states' should trigger what shaders to be used (i.e. small particle counts: use BigDaddy; larger counts: use combined shaders, huge counts: separate shaders.)

About the spawn rate issue: It'll be a lot easier to explain once the code is publicly accessible. Quite hard to explain otherwise without a huge wall of text. Needless to say, it's not a floating-point precision issue!

Usnul commented 9 years ago

@squarefeet I'be happy to read through ha wall of text, if it helps solve the problem :)

squarefeet commented 9 years ago

@Usnul Ha, braver than I! I'll try and put it up soon for you to peruse.

In the meantime, here's something else that would be good to get your opinion on:

Two constructors, both alike in dignity, in fair JavaScript where we set our scene... One constructor, the ShaderPass creates shader passes (i.e. for velocity, position, etc.), the other a Module that affects the behaviour of a particular ShaderPass (i.e. to add CurlNoise, or acceleration to a velocity pass).

A question about the implementation... which do you think is more logical?

var emitter = new Emitter();

emitter add( new ShaderPass( { 
    name: 'velocity',
    // etc...
} ) );

emitter.add( new Module( {
    name: 'acceleration',
    value: new THREE.Vector3( 1, 2, 3 ),
    affectsShaderPass: 'velocity' // Assign this module to the ShaderPass created above
} ) );

Or:

var emitter = new Emitter(),
    pass = new ShaderPass( { 
        name: 'Velocity', 
        // etc...
    } ),
    module = new Module( {
        name: 'acceleration',
        value: new THREE.Vector3( 1, 2, 3 )
    } );

pass.addModule( module );
emitter.addPass( pass );

EDIT: Quick clarification: As far as the end user goes, most people won't have to create their own ShaderPass instances or Modules. There will be predefined ones in the library (like 'VelocityPass', CurlNoise module, etc.). This is mainly for custom stuff, but these constructors will also be used to create the predefined modules/passes.

Usnul commented 9 years ago

@squarefeet Seems like a "has a" relationship, maybe i'm just not getting something. Module seems to need a pass or many passes, then module should be responsible for a pass, no? And thanks for edit, for common use-cases this does seems like a beast of an API :D

squarefeet commented 9 years ago

Yeah, that's what I was thinking as well - the 2nd option "feels" correct to me. Long story short, it goes like this:

An Emitter needs ShaderPass instances (creates the main shader code, renderTargets, dataTextures, etc.. Also can have a basic calculation string (e.g. value.xyz *= 0.8;) if necessary) in order to actually do something. Some of these ShaderPass instances will be automatically added to an Emitter instance, such as the spawn shader.

A ShaderPass can have custom calculations added to it by adding Module instances to a ShaderPass. Module instances can contain calculations to adjust a ShaderPass's value, such as a CurlNoise module, or a Drag module.

Also, if a ShaderPass or a Module instance needs to read from another ShaderPass's output (for example, a velocity pass might need to read from the position pass), uniforms and texture sampler GLSL code is automatically added.

And yes... it's a dirty ol' beast of an API, but that's the way I like it :D At least it'll make adding new modules/passes nice and easy in the future. Expansions made easy!

squarefeet commented 9 years ago

Just had another thought: using this API, you could theoretically create a ShaderPass that would render a diffuse texture that you could use as the texture for the particles... Oh god. What have I created!? :D