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

squarefeet commented 10 years ago

@movitto, @alexeld, @AdrienMorgan, @DelvarWorld, @stemkoski - Just tagging you guys to let you know the existence of this thread. If you have anything to add, please do. Not expecting anything, but comments/criticism of the current version would be very handy.

Cheers.

Usnul commented 10 years ago

hey @squarefeet thanks for creating this thread. I read SuckerPunch presentation on their particle engine for their ps4 game "infamous". The slides are at: http://www.suckerpunch.com/images/stories/SPP_Particles_Full.pptx they are some 500MB thanks to high-res clips, so download at your own risk. Bill Rockenbeck names 180,000 as a high number of particles to manage (what they have as a limit it seems). I know for a fact that glsl can manage a lot more than that for simple systems. The relevance of that particular engine is their simulation of smoke, they use only a handful of particles to do that. For those particles to look nice you need quite a few things though:

but this is only for the sake of behaviour of particles themselves, another thing is appearance and there things like reaction to lights/shadows and casting of shadows is apparently a lot more important, I'm quite certain it would be pretty hard to formulate without heavily coupling with some particular rendering pipeline, so I'm just leaving it out there.

Trouble with encoding things like "spiral" or "straight line" or whatever else is an interesting motion shape someone might want directly into the engine is lack of flexibility. I believe a great engine is:

if you could supply emitter with following position function:

pos = vec3(sine(time)*r, cos(time)*r);
r = log(time);

you could essentially give people freedom to define spirals, however it would give them a lot more than that. And maybe it is a good way, to just allow injection of a snippet of code into shader, where shader includes some useful function like noise inside of it already.

Another thing in terms of implementation that i'm quite certain of - it has to remove JS out of the loop almost entirely, best option for that right now seems to use 2 buffers and swap them between simulation steps, use one for lookup and other for writing new state. I believe there are better ways potentially, but they aren't supported by what's enabled in a browser by default (without toggling flags in chrome or equivalent elsewhere).

From my personal experience - keeping things as generic and as simple as possible usually pays out in the long run.

cihadturhan commented 10 years ago

I agree on what @Usnul said about injectible function. An injectable functions means fully generic system. Adding a gravity would be as simple as

pos = vec3(x0, y0 + g*time*time/2, z0);

and you can switch from a cylinder to a sphere easily by changing two parameters. If you've heard about superformula you'll definetely understand what I mean. This was also an issue I felt when I was coding Hydrogen atom probablity density viewer with SPE because I had a formula for probablity distribution and I had to inject it to place points to the related coordinates but the current library didn't help me about that and I had to change many parts.

This also may lead an issue about easiness of code because a developer should find out the right formula to generate desired shape of particle system. We should consider it.

Usnul commented 10 years ago

I don't really see this as an issue @cihadturhan, there is an option of creating a library of these parameterizable Emitters, something along the lines of:

var BasicPhysicsEmitter = function(options){
...
var velocity = options.velocity;
addUniform({ name: "velocity", value: velocity, type: "v3"});
...
"pos = pos+velocity*time" 
...
}

let people use these as much as they like, I'd imagine it would serve large portion of users.

PS: That hydrogen looks awesome :)

cihadturhan commented 10 years ago

Yep, on a second thought I think that won't be a problem.

Thanks @Usnul

squarefeet commented 10 years ago

This is all great stuff. I'd never thought of having custom position calculations... I'm assuming these will either be written in GLSL and injected at runtime (or, if an emitter is already running and its pos calculation is changed, it's shader's rebuilt)?

For the sake of playing devil's advocate, I have some issues with it:

  1. GLSL isn't as easy to get to grips with as JS (it's userbase is much smaller). @cihadturhan - you mentioned this as well.
  2. Changing the pos function during runtime could cause quite a few stutters.
  3. Assuming it would be GLSL code injected into the shaders, this code would probably have to be written as strings... that feels a little clunky.

To argue against myself:

  1. @Usnul: Your suggestion of a library of parameterizable emitters is a good call. That makes the issue of GLSL not being as well-known as JS a non-issue. We'd have the base library, then a bunch of general-use, pre-defined 'shape' emitters that would have their pos calculations filled in?
  2. The use of multiple buffers (maybe even more than two) could alleviate this stuttering. Since we'd have fewer CPU cycles being used to calculate new pos/vel/accel/etc. values, these cycles could be used to swap buffers and re-compile shaders.
  3. I can't think of another way around this problem... Chalk it up to a limitation of the platform?

I'm liking the idea of moving the functions that currently reside in SPE.utils into the shader, though. This would free up a lot of CPU cycles for the buffer swapping (which I'll probably have to quiz you quite a bit on, @Usnul!). I know neither of you explicitly stated that the SPE.utils functions would be moved to the shaders, but that does seem to be a logical extension of what you've both suggested so far...

What I would absolutely love to be able to do is get values back out of the shaders. At the moment (and please do correct me if I'm wrong) but I'm pretty sure that we can only whack values into a shader, but not get the result back into JS. WebCL should solve this problem, but I believe that is quite a way off from being included in browsers as standard. The reason I bring this up is that, in theory, we could do all of the setting up in JS, and then use multiple shaders to do different calculations (possibly even particle interactivity, eg. boids simulation). Oh, the possibilities!

Dreaming aside, though, this is a good step forward. @cihadturhan: That hydrogen density sim is really awesome - great work!

squarefeet commented 10 years ago

PS. Just done a v. quick lookup on the state of WebCL. The 1.0 spec was released in March this year, but Firefox, at least, isn't going to implement it. They seem to be erring on the side of OpenGL ES 3.1's compute shaders instead. I haven't yet found a timeline for ES 3.1's adoption.

Usnul commented 10 years ago

Hey @squarefeet, I like your reasoning. Let me try and address some issues:

I could see every emitter class (not instance) use a separate shader, but at the same time - even hundreds wouldn't present much of a challenge, when thinking about games today that employ thousands of shaders at the same time.

Usnul commented 10 years ago

here's a promising thread from three.js on the similar concept of using double buffering: https://github.com/mrdoob/three.js/issues/1183

cihadturhan commented 10 years ago

A couple of things,

Usnul commented 10 years ago

@cihadturhan I'm a little confused on this point. if we use an FBO - then vertex shader is pretty much excluded, you have a quad and that's it, each fragment however represents a particle, and looks up position of that particle from a texture (FBO). Having UV (0.5,1) for instance, we'd go to texture and lookup pixel at at that location, then we'd unpack state from it, including position of the particle. In a way UV only serves as ID of that particle. GLSL is quite new to me, and a lot of it i still don't understand, for instance I'm not sure how then this data can be used to place sprites or how it can be shared with things like THREE.js. On the other hand, if we have a vertex shader responsible for particle simulation, I'm not sure how FBO lookups would work, and how these vertices are going to be linked to specific texels.

cihadturhan commented 10 years ago

@Usnul I understand how you want to use FBO but I have no knowledge on how FBO works in deep. Will the coordinates change in the next frame like in gl_Vertex? This is what I don't know and I wonder much. If it always stores the new one then it's perfectly fine to use FBO.

Usnul commented 10 years ago

@cihadturhan here's a basic rundown:

set fbo1 as texture for lookup
set fbo2 as render target (where we draw to)
begin render
...
render done
swap fbo1 and fbo2 so that next time 1 is used for render target and 2 for lookup
rinse and repeat

okay, that's great, and here's what's happening inside fragment shader:

get pixel from texture based on U and V coordinates
do clever stuff with pixel data
set gl_FragColor with new pixel data so that it gets written to render target U V coordinates

the proviso here is that texture for lookup and render targets are identical in structure, this is why you can quite easily identify where to write, and why you can swap them too.

As you can see, particle state would correspond to a specific pixel, so it is preserved between render cycles. Inside the shader you have access to that state and also have the opportunity to use that "old" state when creating new one. A simple static shader, for instance, could just copy old state so that new state remains the same as the old one.

cihadturhan commented 10 years ago

@Usnul Thank you very much for explaining, I'm new to texture stuff. I see what you want to do and it's very clever :+1:

If the number of particles are n * m, then we'll use all of the pixels :)

PS: we can assign m = 1 everytime so that's not a problem though.

movitto commented 10 years ago

@squarefeet @Usnul this all seems reasonable / cool. Am also relatively new to GLSL so can't comment too much but agree it would be awesome to parameterize the shader w/ custom algorithms (if it's feasible / practical). Also agree on addressing the performance implications, perhaps in addition to the solutions presented above, we could support multiple modes w/ different shaders optimized for different scenarios.

In general would be good to try to do this as a pluggable architecture, where at the base we present simple classes / interface able to be extended/used for custom scenarios. Would be glad to help where/when I can.

squarefeet commented 10 years ago

@Usnul The FBO stuff does certainly seem to overcome our problem with particle state between render cycles - great call to use that. From my extremely limited understanding, though, wouldn't we only have 4 values per pixel to describe a particle's state (R/G/B/A)? I think I'm just missing the connection between how I describe a particle's state in my mind (pos, color, velocity, acceleration, opacity, angle), and how the shader would describe the particle's state using an FBO. Though I guess we would only really need to store the position? Maybe, if necessary, we could use more than one texel to describe a vertex...

As far as linking a particle/vertex to a specific texel co-ordinate, could we not just have a FloatArray that stores UV co-ords? Obv. two entries per vertex ([u,v,u,v, etc.]). Have that as a uniform, and store the start position in the FloatArray for each vertex as an attribute:

uniforms: {
    texels: new Float32Array(...)
}

attributes: {
    coords: { type: 'f', value: [...]
}

I know I'm probably bringing the discussion backwards a little here, but I just want to be sure I fully understand your proposal :)


@movitto Great to have you on board. I think it's more than feasible to have custom shader algorithms - as @Usnul mentioned, shader re-compilation doesn't seem to pose as much of a problem as we first thought. That, and the changing of shader algorithm(s) during runtime would probably be a small use-case anyway.

The idea of different shaders for different scenarios is a good idea - I reckon for ease of development, though (and for DRY purposes), it would be prudent to make our shader code as modular as possible; broken up into small-ish chunks that we could just tie together to make new shaders as and when necessary ( similar to what THREE.js does ). That said, I think that's what you might've meant when you talked about the 'pluggable architecture'!

squarefeet commented 10 years ago

Okay, been doing a bit of reading into the FBO stuff, and @Usnul - I followed your link to the double-buffering thread in the THREE.js repo. It helped quite a lot, thanks.

About the 4 values p/pixel issue: looking at this example of GPGPU boids/flocking (which is just stunning, btw) from here, it appears that @zz85 is using multiple textures per "particle" (one for position, another for velocity, etc.) Following this pattern would solve the issue of only 4 values p/pixel.

His SimulationRenderer.js file would be a good inspiration for the FBO-swapping...

squarefeet commented 10 years ago

Just got a quick practicality question:

What would you guys prefer? At this stage, it'd just be sketches, tests, etc.

Usnul commented 10 years ago

I'd say a SPE mark.2 would make sense :) As a possibility we could bind that engine to this one's API as well, though not as a primary API target I guess. But yeah - i see it as a clean slate which shares common goals with this project, but is, as you said - a re-write as opposed to an alteration.

squarefeet commented 10 years ago

So I've been getting my head around GPGPU/FBO/render-to-texture stuff, and I've re-written the THREE.js gpgpu birds example. It sounds a little strange to have done that but it gave me a good way to learn FBO stuff, as well as make a base "class" that we can use in the next version of the particle engine.

I've submitted this as a possible replacement for the existing SimulationRenderer for THREE.js here. Hopefully I'll get some useful feedback either here or on that thread and it can move forward into a useful little "class".

In short, this version of the Simulation Renderer takes care of generating data textures, swapping renderer targets / buffers, and automatically passing generated textures into shaders. It's fairly straightforward to use, so I haven't added many comments, but let me know if it's not as straightforward as I think it is and I'll comment the crap out of it :)

@Usnul: Once this SimulationRenderer is declared usable/useful, then I'll start up the SPE v2.0 repo.

squarefeet commented 10 years ago

I'm quite comfortable with FBO stuff now, so I'm starting to think about how we should organise what textures hold what data.

To start with, lets keep things simple and have the following per-particle attributes:

We could have one texture for each of these, but I'm wondering if we could pack some of them together. Like using the unused w component of the RGBA Position texture as the opacity value, etc. Or maybe even having the position and velocity textures combined, using an offset parameter to determine whether a pixel of the texture is a position value or a velocity value - Soulwire did something similar here.

We could also work around having one render-to-texture pass for each of the textures as well - maybe by using the velocity calculations from the existing SPE shaders that extrapolate velocity values based on particle age. These velocity calculations could be integrated into the position texture shader directly, so that's one less render pass to do. This may cause problems later on when we come to add vector fields and stuff like that, though.

So... Issues that need discussing:

Usnul commented 10 years ago

could make a mapping mechanism for variables, something along the lines of: int -> ... float -> ... v3 -> rgb v4 -> rgba let the mapping mechanism pack/unpack several floats into a single rgb/rgba etc as you said. Keep track of what FBOs we have and what fields in those aren't used yet. I'd say making it generic from the start would solve a few problems in the future.

squarefeet commented 10 years ago

I've been playing around with a few things tonight and wanted to get feedback on a possible "core" API. I'm not going to paste any of the internals I have going at the moment, because they're an absolute mess, but as far as using the thing goes, this is what I have:

var myParticles = new Particles();
myParticles.addProperty( 'acceleration', new THREE.Vector3( 0, 1, 0 ) );
myParticles.addCalculation( 'velocity', 'velocity += acceleration * delta' );
myParticles.addCalculation( 'position', 'position += velocity * delta' );

For each addCalculation() call made, an FBO object is set up (double-buffered).

Note that I haven't defined any initial values for velocity or position. These default to vec3(0,0,0) if not defined.

Also, for each addCalculation() and each addProperty() call, a uniform is added to each of the FBOs created, so any of the properties can be accessed from any of the shaders that get created.

Oh, and one more thing, I haven't done anything re. emitter groups. Just this core API work.

Thoughts? I'm thinking that whilst this gives us the most freedom we need (particularly taking care of custom position calculations), it does seem to possibly be at the cost of usability.

Usnul commented 10 years ago

that looks pretty good @squarefeet. Regarding groups, i suggest using a hash of sprite URLs:

var sprites = {};
function Particles(url){
  var texture;
  if(sprites.hasOwnProperty(url)){
   texture = sprites[url];
  }else{
   texture = THREE.TextureUtils.load(url);
   sprites[url] = texture;
  }
  ...
}

also, regarding syntax: i do think it's verbose, but there are ways of reducing that. One way would be to write a glsl parser which would be a small investment and would probably be done later in the project. Another way would be to do something like this:

myParticles.properties.add('acceleration',new THREE.Vector3(0,1,0));
myParticles.calculations.add('velocity += acceleration * delta')
     .add('position += velocity * delta');

parsing this would be relatively easy, as all you'd have to do is tokenize it and exclude reserved tokens like float, int etc. and only keep ID tokens ([a-zA-Z][a-zA-Z0-9]*)

squarefeet commented 10 years ago

I'm not sure why we'd need a GLSL parser?

EDIT: Ah, wait. I think I understand why now... you've removed the first argument for the calculation.add() function. We'd need to parse the only argument you have given in order to find the property name that needs calculating?

Usnul commented 10 years ago

yup :) besides that, not all operations might end up as assignments, and some "variables" may only need to be temporary

Usnul commented 10 years ago

glsl parser project in JS: https://github.com/chrisdickinson/glsl-parser

cihadturhan commented 10 years ago

@squarefeet I really like this implementation. Maybe we can add chaining to all functions such as

myParticles.addProperty( 'acceleration', new THREE.Vector3( 0, 1, 0 ) );
                  .addCalculation( 'velocity += acceleration * delta' );
                  .addCalculation( 'position += velocity * delta' );
squarefeet commented 10 years ago

@Usnul: Thanks for that glsl parser link, I'll check it out as soon as I can. @cihadturhan: Chaining will be nice and easy to add.

I was also thinking of trying to figure out some sort of automatic way to find out what properties of an RGB/RGBA texture weren't being used, and flag those as usable. So if someone adds a velocity calculation, that could create an RGBA texture with the alpha channel unused, so that'll be flagged and taken up if a user adds an int or float property (opacity, for example). It might be quite tricky to implement but will save on render passes and texture generation.

Usnul commented 10 years ago

this is just a sketch, can be a lot simpler

vectors = [];
//prepare buckets
vectors[4] = [];
vectors[3] = [];
vectors[2] = [];
vectors[1] = [];
//
function addProperty(name,arity){
    var bucket = vectors[arity];
    bucket.push(name);
}
//
function getFBOs(){
    var fbos = [];
    //first make FBOs for 4 and 3 arity vectors
    vectors[4].forEach(function(name){ /* make RGBA FBO */ });
    vectors[3].forEach(function(name){/* make RGB FBO */});
    //pack the rest
    var stuff = [];
    Array.prototype.push.call(stuff,vectors[2].map(function(name){ return {arity:2, name:name}; }));
    Array.prototype.push.call(stuff,vectors[1].map(function(name){ return {arity:1, name:name}; }));
    //
    var available = 0,
        rgba = null,
        vector = null;
    while(stuff.length > 0){
        if(available <= 0){
            //depleted
            if(rgba!==null){
                fbos.push(rgba);
            }
            available= 4;
            rgba = /* make RGBA FBO */;
        }
        //find vector to pack
        var packet = -1;
        for(var i=0; i<stuff.length; i++){
            vector = stuff[i];
            if(vector.arity === available){ //exact fit
                packet = i;
                break;
            }else if(vector.arity < available && (packet<0 || stuff[packet].arity < vector.arity) ){
                packet = i;
            }
        }
        //
        if(packet >= 0){
            //we have a vector to place
            vector = stuff[packet];
            var index = ['r','g','b','a'][4-available];
            available -= vector.arity;
            stuff.splice(packet,1);
            rgba.reserve(index,vector.arity, vector.name);
        }else{
            //couldn't fit any vector in the remaining space, start new fbo
            available = 0;
        }
    }
    if(rgba!==null){
        fbos.push(rgba);
    }
    return fbos;
}
Usnul commented 10 years ago

here's a better mapper, type names are taken from GLSL spec.

var m = new VectorMapper()
    .add('isAffectedByGravity', 'bool')
    .add('velocity', 'vec3')
    .add('acceleration', 'vec3')
    .add('spriteSize', 'ivec2')
    .add('id', 'int')
    .add('time', 'float');
//pack properties to FBOs
m.getFBOs();
//find out what FBO variable 'id' was mapped to
m.getMapping('id'); // {vector:{name:id}, position:2}
var VectorMapper = function () {
    "use strict";
    var typeLookup = {
        bool: THREE.UnsignedByteType,
        int: THREE.IntType,
        float: THREE.FloatType
    };
    var vectors = this.vectors = [];
    var mapping = [];
    var fboId = 0;
    this.fbos = [];

    function makeFBO(type, arity, width, height) {
        var threeType = typeLookup[type];
        var threeFormat = [null, null, null, THREE.RGBFormat, THREE.RGBAFormat][arity];
        var target = new THREE.WebGLRenderTarget(width, height, {
            wrapS: THREE.RepeatWrapping,
            wrapT: THREE.RepeatWrapping,
            minFilter: THREE.NearestFilter,
            magFilter: THREE.NearestFilter,
            format: threeFormat,
            type: threeType,
            stencilBuffer: false
        });
        return {id:"lookupTexture"+(fboId++), type: type, arity: arity, available: arity, target: target};
    }

    function assign(vector, fbo) {
        mapping.push({
            vector: vector,
            fbo: fbo,
            position: fbo.arity - fbo.available
        });
        fbo.available -= vector.arity;
    }

    this.add = function (name, glslType) {
        var primitives = ['bool', 'int', 'float'];
        var reVector = /([ib]?)vec([234])/;
        var type,
            arity;
        if (primitives.indexOf(glslType) !== -1) {
            //type is primitive
            arity = 1;
            type = glslType;
        } else {
            var matches = reVector.exec(glslType);
            switch (matches[1]) {
                case "":
                    type = 'float';
                    break;
                case"i":
                    type = 'int';
                    break;
                case "b":
                    type = 'bool';
                    break;
            }
            arity = parseInt(matches[2]);
        }
        var vector = {name: name, type: type, arity: arity, glslType: glslType};
        vectors.push(vector);
        return this;
    };
    this.getMapping = function (name) {
        for (var i = 0; i < mapping.length; i++) {
            var m = mapping[i];
            if (m.vector.name === name) {
                return m;
            }
        }
        //not found
        return null;
    };
    this.getFBOs = function (numValues) {
        var fboHeight, fboWidth;
        fboWidth = fboHeight = Math.ceil(Math.sqrt(numValues));

        var fbos = this.fbos = [];
        //FBOs that still have some slots available
        var unsaturated = [];
        //sort vectors in order of arity
        vectors.sort(function (a, b) {
            return b.arity - a.arity;
        });
        //start packing
        var copy = vectors.slice(), i, j, vector, candidate, fbo;

        while (copy.length > 0) {
            //first try to fill one of unsaturated fbos
            for (j = 0; j < unsaturated.length; j++) {
                fbo = unsaturated[j];
                candidate = -1;
                for (i = 0; i < copy.length; i++) {
                    vector = copy[i];
                    if (vector.type !== fbo.type) {
                        continue;//wrong type
                    } else if (vector.arity === fbo.available) {
                        //perfect match, assign
                        candidate = i;
                        break;
                    } else if (vector.arity < fbo.available && (candidate !== -1 && copy[candidate].arity < vector.arity)) {
                        candidate = i;
                    }
                }
                if (candidate !== -1) {
                    //found a fit
                    vector = copy[candidate];
                    assign(vector, fbo);
                    copy.splice(candidate, 1);
                    if (fbo.available === 0) {
                        unsaturated.splice(j--, 1);
                    }
                }
            }
            if (copy.length > 0) {
                //pick top vector and create an FBO for it
                vector = copy.shift();
                fbo = makeFBO(vector.type, Math.max(3, vector.arity), fboWidth, fboHeight);
                assign(vector, fbo);
                fbos.push(fbo);
                if (fbo.available > 0) {
                    unsaturated.push(fbo);
                }
            }
        }
        return fbos;
    };
};

it will attempt to pack attributes into few FBOs keeping restriction of types, the packing is greedy but otherwise fairly standard (largest-first).

Usnul commented 10 years ago

here's a function for making uniforms:

function generateFBOUniforms(mapper) {
    "use strict";
    var uniforms = {};
    mapper.fbos.forEach(function (fbo) {
        uniforms[fbo.id] = { type: "t", value: fbo.target };
    });
    return uniforms;
}

and here's a one for unpacking variables in the shader:

function generateUnpackSnippet(mapper) {
    "use strict";
    function accessor(from,length){
        return ['x', 'y', 'z', 'w'].slice(from,from+length).join("");
    }

    var lines = mapper.vectors.map(function (vector) {
        var mapping = mapper.getMapping(vector.name);
        return vector.glslType+" "+vector.name+" = texture2D( "+mapping.fbo.id+", uv )."+accessor(mapping.position,vector.arity)+";";
    });
    return lines.join("\n");
}

uniforms are in THREE style. Here's a sample output of unpack code generator:

vec3 velocity = texture2D( lookupTexture0, uv ).xyz;
vec3 acceleration = texture2D( lookupTexture1, uv ).xyz;
ivec2 spriteSize = texture2D( lookupTexture2, uv ).xy;
bool isAffectedByGravity = texture2D( lookupTexture3, uv ).x;
int id = texture2D( lookupTexture2, uv ).z;
float time = texture2D( lookupTexture4, uv ).x;

that's for

    .add('isAffectedByGravity', 'bool')
    .add('velocity', 'vec3')
    .add('acceleration', 'vec3')
    .add('spriteSize', 'ivec2')
    .add('id', 'int')
    .add('time', 'float');
squarefeet commented 10 years ago

Holy awesome code, Batman. I'll take a good look at this tonight and get back to you properly then :)

squarefeet commented 10 years ago

I've taken a look through this, and it's absolutely superb, @Usnul. Exactly what I was looking to do. I reckon it'll serve as a good base for sorting out FBOs and assignments :)

I want to have a good ol' tinker with it, and see if I can build up a double-buffered render chain from it. Once that's done, I'll start trying to work it into an emitter to see how it holds up. I'll open up a new repo for all of this v. soon.

Thanks!

squarefeet commented 9 years ago

Hey guys,

So... it's been a long, long while since the last message in this thread, so quite a lot to update you all on:

About the alpha version:

I would love to try to get particle lights implemented but I fear I'd have to crack open Three.js's internals to do that, which would be beyond the scope of the project. Having the particles react to a scene's lights might be do-able, though... Must look into that. Talking of which, I'd like to get shadows implemented, but that brings with it a whole new issue of particle sorting, which is a very expensive operation. I've experimented with radix sort on the GPU, but it's not smooth enough for my tastes (it sorts them over multiple frames).

There are also multiple ways of setting most parameters. For example, you can set the velocity value to be a THREE.Vector3, or a set of THREE.Vector3s that will be linearly interpolated through over the age of the particle, or as a random value between minimum and maximum bounds. I'm working on getting parameters to support sets of values that will change over the lifetime of the emitter as well as the lifetime of a particle. These value options replace all of the *Start, *Middle and *End properties in the current version. It's so much nicer to work with (IMHO).

So yeah, it's a very modular system. Create an emitter, then create as many modules as you want (each with various parameters) and add them to the emitter. Pretty much it. There's currently no grouping, though, and I'm not sure whether this is a viable with the architecture I've made so far.

With all that said..! I'm afraid I'm re-writing it again (only the 4th re-write!) The way it's set up at the moment is that almost all of the modules have their own textures, and shaders. The more modules one has, the more render passes and buffer swapping, which isn't good for performance. I'm still planning on keeping the modularity, but I'm heavily re-working the shader code. As it stands at the moment, changing parameters on-the-fly isn't really cost-effective.

At the moment, I'm running only four passes regardless of how many modules are enabled (spawn, forces [vel., accel., noise, basically anything that affects velocity], color, and position/draw), and things are speeding up rather nicely. It also helps that I'm pushing less data to/from the GPU.

As far as custom calculations go, that's going to be added quite near the end of this re-write but it's at the front of my mind whilst I'm writing the rest of it.

With everything running on the GPU and JS only really doing the buffer swapping, there's not really a way to get per-frame access to a particular particle's properties. Luckily, in this re-write most of the parameters are dynamic, with only the one texture holding both velocity and position data. So things can still be changed on a per-frame basis without repopulating the texture buffer (which is an exponentially expensive operation).

I think that pretty much covers most things... hit me with any questions. It might be hard without seeing any code, but if you want examples of it's current state, just let me know and I'll do some copypasta.

I'll whack it up here under a new repo once I'm happy with it and it's stable enough for people to poke around and experiment with.

PS. Thanks @Usnul for pointing me in the direction of textures/FBO/GP-GPU. It led me down a wild garden path of joyous discovery!

squarefeet commented 9 years ago

Just because pictures can say more than words, here's some screenshots of the progress so far:

Plain ol' curl noise

Emitting particles from a geometry's vertices

cihadturhan commented 9 years ago

Wow, you've made huge amount of work. That's awesome. I'm looking forward to see the demos live. Is this blue one called curl noise? I'm on mobile now, didn't watch the video.

By the way, if you're studying you can request github students pack which includes free github subscription for a year.

Usnul commented 9 years ago

Hey @squarefeet , that's amazing progress. Would be interesting to have a look at the code. You mentioned "per-particle" access, i'm actually not too worries about that since you can always use canvas operations to from JS side to work with pixels in a buffer, or if it's a binary array - things are even simpler. Would be silly to manipulate all particles this way and defeat the purpose of GPU engine, but for some small pools of say 200 particles or so it could be a nice addition.

Regarding particle lighting, there's an interesting introductory set of slides which you probably have seen already: http://www.roxlu.com/downloads/scholar/008.rendering.practical_particle_lighting.pdf

I've been reading around particle engines for a while now, and i'm quite convinced now that integration and dynamic properties of particles are more important than count overall. By that I mean aspects which make particles "feel" more like they are a part of the world, here are a few examples:

I've seen many examples where only a handful of particles (>500) were used to achieve visually amazing effects. That's not to say that really high particle count is a great thing on its own :)

One more piece to consider is screen-space, some effects such as rain or snow are often done in screen space, because you can get higher density in 2d with same count than you can in 3d.

Lastly, regarding cracking open three.js - it's remarkably nicer than it was several months ago, shaders for all materials are now composed out of fragments (chunks/snippets), so you might be able to create a custom material by manipulating order of these chunks and introducing your own.

At the end of the day, i'd personally be more in favour of having strong support for Three.js, but no core integration, allowing particle engine to be used with different rendering engines. Babylon is getting a lot of traction, and there are many projects which go beyond stock three.js.

squarefeet commented 9 years ago

Hey guys, Thanks for the kind words :)

@cihadturhan: Yep, that's curl noise. In both of the screenshots, actually. I'm past my studying days - I just take free things where available ;)

@Usnul: I'm totally with you re. appearance of particles more important than the count. Sure, higher counts are more impressive initially but really of little real-world value (unless of course, you're building a tech demo!)

I think once I've got the new shader up and running (I've called it BigDaddyShader... it does velocity, position, colour, and as-yet-undefined "other" all in one pass, yay) and functioning with the modules from the previous rewrite, I'm definitely going to start tackling the task of using depth buffers to do various things. First of my list will probably be collision, as that will teach me how to use the depth buffer in quite an in-depth (no pun intended) fashion. I'll also start testing the limits of THREE.js, as it might be that shadows and/or lighting are already supported for particles. The former is quite possible, the latter not so, if my understanding of THREE's source is anything to go by.

I'm definitely keen on getting soft particles introduced. Maybe that should be my first port of call instead of collision...I never make things easy for myself!

You're right about THREE's internals, btw. I nicked the idea of using shader "chunks" to build shaders, and I daresay I'll be able to bastardise their lighting shaders to whack in my draw shader. Maybe even the shadow stuff.

I think when it comes down to this engine being used as a part of a bigger system (games, environments, etc., rather than nerdy simulations), the sorting issue won't be too much to worry about, as the particle count will be fairly low. At least I hope so.

About this library supporting not just THREE: Definitely in favour of that. I'll need to get my "native" WebGL chops up to scratch, but it's something I've definitely considered and researched already. I'm not too sure about Babylon, but I daresay if it's anything like THREE re. ease-of-use, then it shouldn't be a problem. The "native" WebGL version will be the trickiest IMHO.

Usnul commented 9 years ago

Using chunks is the most primitive way to generate code, nothing wrong with primitive, but there is a lot more to code generation. Next step is probably templating, beyond that is tree transform (aka parse-tree -> AST -> transform -> transform -> generate code from AST)

You could use point cloud from threejs, but you would be bound to a vertex buffer then, which i assume isn't a good fit for your engine. Three JS works with shadows for geometry, so i doubt that you'd be able to make it work without changes. Good news is that shadow maps are pretty easy to code - it's just a B/W projection.

Collisions. Bullet physics engine has recently been updated (about a year ago now actually) to do solving on GPU, but i'm not sure what kind of shaders they use - probably geometry or compute, still might be a useful source of info as it's open source. I wouldn't go beyond planes, cubes, spheres and maybe heightmaps for collision solving. I'm still certain that it would/will be a pain to do this with just the vertex and fragment shaders.

squarefeet commented 9 years ago

Ha, I don't know whether going down the AST would is completely necessary, here; pretty sure chunking it up will suffice!

I'm currently using the PointCloud... I'm not sure why being bound to a vertex buffer is a bad thing? Not sure of the alternative..? Unless you're thinking that shadows are more easily calculated/cast using faces?

I'll definitely dig into the bullet engine and see how they're doing the depth collision. I'm also digging into UE4's internals to discover how they do their depth collision... hopefully I can get enough pointers. Failing that, this page has a very good tutorial. Just missing the bit about projecting a particle's position into clip-space (damn).

Also, just because it's kinda pretty, here's a screenshot of the BigDaddyShader (I really need to rename that...) in action. The bottom square is the spawn texture (particle age is in the x, or red, channel, so that's what's causing the fade. Inactive particles are blue, active ones are slowly fading from that same blue to white. The square above it is the BigDaddyShader's texture. It's 4x the size of the spawn texture, as there are 4 pixels per particle (velocity, position, color, and other). The velocity is currently being affected by curl noise (I'm addicted to that stuff):

screen shot 2014-10-20 at 18 48 04

Next up:

Usnul commented 9 years ago

having vertex buffer is actually great, I think. The projection itself is somewhat easy, but for light you will need a normal as well as position, otherwise it's back to billboards.

MAX_TEXTURE_SIZE is important to consider BigDaddyShader is a good name, as good as any other, i'd drop "Shader" part for brevity just "BigDaddy" - i'd be proud to use something with a name like that :) Can't wait to have a look at the code, everything so far looks very enticing.

eldog commented 9 years ago

Very useful library, looking forward to the new features and flexibility.

Just want to vouch for updating the editor with these changes too, as it is a really valuable tool for getting the right look for an effect (and made me decide that this was the library to use).

squarefeet commented 9 years ago

@Usnul "BigDaddy" is just the internal filename for the shader... I don't think that's what I'm going to call the final version! Maybe as a nickname... ;)

@eldog Thanks, I wasn't aware anyone was using the Editor! That has reminded me to push it to the master branch... Completely forgot to do that. I think there are some issues around exporting and whatnot, though..? There definitely will be an Editor for v2, but it will come once v2 is stable. I'm also going to get a site up for v2 specifically (that's not gh-pages), with some tutorials, maybe some videos, full and proper documentation, and the editor. Lots to do!

squarefeet commented 9 years ago

Got stuck doing particle collision with the depth buffer... Posted a question on stack overflow and linking it here in case anyone has any ideas!

https://stackoverflow.com/questions/26576429/projecting-fbo-value-to-screen-space-to-read-from-depth-texture

Usnul commented 9 years ago

i have a suspicion that you're not taking clipping planes into account (far, near). I'm not sure what space you use to resolve collisions, but if you are using screen-space - you would have to first project position of a particle into screen space coordinate system, otherwise you would have to apply inverse of that transform to depth buffer. If you are using the depth buffer - you'll have to get normals for texels as well, some SSAO algorithms implement inference of normals from depth buffer and can help with that.

squarefeet commented 9 years ago

I've tried taking clipping planes into account, in fact I use that already in the draw shader: http://jsfiddle.net/1dchvbge/1/ (see the HTML section, and scroll down until you see the draw shader section)

The problem I have is that I can't seem to project the particle position into screen space correctly. I need to use the particle's screenspace xy as a UV coord for the depth texture... I've read quite a lot about projecting to screenspace, but nothing seems to work. So frustrating!

Once I manage to find out how to (correctly) project into screenspace without using gl_FragCoord, then the rest of it will be nice and easy... Famous last words, hey?

squarefeet commented 9 years ago

Ah-ha! Figured it out. I've added an answer to my own question (!) on my SO thread: https://stackoverflow.com/questions/26576429/projecting-fbo-value-to-screen-space-to-read-from-depth-texture/26609097#26609097

New fiddle showing it in action is here: http://jsfiddle.net/30rtw4b1/1/

squarefeet commented 9 years ago

I can't be bothered to put up a fiddle for this just yet, so here's a video I made of collisions with (probably buggy) collision response:

https://vimeo.com/110275317

Vimeo's processing it at the moment, so should be ready in about 30mins. Will post a fiddle link when I get around to it! Was recorded on my Macbook Air, so performance isn't great when capturing the screen.

squarefeet commented 9 years ago

Okay, here's a fiddle: http://jsfiddle.net/vhw6o7ef/1/ Let me know if it doesn't render properly for you (compare it to the video).