Skip to main content
gamedev.net
🔒 Locked 🎮 Unity

Speeding up particle effect

Started by medevilenemy 17 replies 3k views
Original Post
medevilenemy
medevilenemy
I am working on particle emitters for engine exhaust for the game I'm making, and I really need to optimize it. Right now it is taking a framerate cost of about 20fps. I would like to minimize that as much as possible. Basically, its a large number of triangular planar particles. Every update, their alpha level drops slightly (to create a fade-with-distance effect) and they move slightly backwards on the z axis. Is there a way to put particle drawing into a display list or something so that it draws faster (to the point of relatively minimal framerate cost)? I've used display lists before, but on stationary effects where nothing ever changes. here is the code to my engine-emitter subsystem:

class REPart // Rectangular engine particle
{
    public:
        void InitParticle(float xmin, float xmax, float ymin, float ymax, float zc, float r, float g, float b);
        void DrawParticle(void);
        bool IsVisible(void);
    private:
        float x; // Horizontal Location
        float y; // Vertical Location
        float z;
        float red;
        float green;
        float blue;
        float alpha;
};
void REPart::InitParticle(float xmin, float xmax, float ymin, float ymax, float zc, float r, float g, float b)
{

    float width = xmax - xmin;
    width = fabs(width);
    float half = (width / 2.0);

    float height = ymax - ymin;
    height = fabs(height);
    float hhalf = (height / 2.0);

    x = (((float)rand()/(float)RAND_MAX))*(xmin);
    x+= (((float)rand()/(float)RAND_MAX))*(xmax);
    y = (((float)rand()/(float)RAND_MAX))*(ymin);
    y+= (((float)rand()/(float)RAND_MAX))*(ymax);
    z = zc;

    red = r;
    green = g;
    blue = b;
    alpha = (((float)rand()/(float)RAND_MAX))*(1.0);
    //alpha  = 1.0-(fabs(x)/half);
    //alpha -= (fabs(y)/half);
}

void REPart::DrawParticle(void)
{
    float size = 0.001;
    glColor4f(red, green, blue, alpha);
    glBegin(GL_TRIANGLES);
        glVertex3d(x, y, z);
        glVertex3d(x+size, y, z);
        glVertex3d(x+size, y+size, z);
    glEnd();

    z+=0.006;
    alpha-=0.01;
}
bool REPart::IsVisible(void)
{
    if(alpha > 0)
         return true;
    if(alpha <= 0)
         return false;
    return true;
}
class Engine : public Rotatable
{
    public:
    Engine()
    {
    };
    ~Engine()
    {
    };
    virtual void Update()
    {
    }

    protected:
    float dx; // X location relative to parent
    float dy; // Y location relative to parent
    float dz; // Z location relative to parent
    float pitch; // Pitch relative to parent (rotation about parent's X axis
    float yaw; // Yaw relative to parent (rotation about parent's Y axis
    float r;
    float g;
    float b;
    float wid;
    float len;


    Rotatable *parent; // A reference to the parent ship, to facilitate easier updates.

    //etc
};
class Rect_Engine : public Engine
{
    public:
    Rect_Engine()
    {
    };
    Rect_Engine(Rotatable *par, float dispx, float dispy, float dispz, float length, float width, float rx, float ry, float red, float green, float blue)
    {
        int i;

        dx = dispx;
        dy = dispy;
        dz = dispz;
        pitch = rx;
        yaw = ry;
        wid = width;
        len = length;
        r = red;
        g = green;
        b = blue;

        parent = par;
        Position = parent->GetPos();
        UpVector = parent->GetUp();
        RightVector = parent->GetRight();
        ViewDir = parent->GetView();


        MoveForward(-dz);
        StrafeRight(dx);
        MoveUpward(dy);
        RotateX(pitch);
        RotateY(yaw);

        for(i=0; i<MAX_ENGINE_PARTS; i++)
        {
            parts.InitParticle(dispx, width, dispy, length, dispz, red, green, blue);
        }

    }
    void Update() // Updates position
    {
        int i;

        Position = parent->GetPos();
        UpVector = parent->GetUp();
        RightVector = parent->GetRight();
        ViewDir = parent->GetView();

        MoveForward(-dz);
        StrafeRight(dx);
        MoveUpward(dy);
        RotateX(pitch);
        RotateY(yaw);

        for(i=0; i<MAX_ENGINE_PARTS; i++)
        {
            if(parts.IsVisible() == false)
                parts.InitParticle(dx, wid, dy, len, dz, r, g, b);
            else
                parts.DrawParticle();
        }

        // CODE FOR DRAWING THE BLOODY THING HERE
    }
    private:
    REPart parts[MAX_ENGINE_PARTS];
};






Thanks for your time, folks! [edit] I know the code there would make it very ugly. This is basically the first revision of it. I'm working on an algorithm to make the particle placement and coloring look better. [edit#2] Sorry, I just noticed I forgot the REPart definition. It has been added. [Edited by - medevilenemy on July 18, 2008 12:23:07 AM]
There was a saying we had in college: Those who walk into the engineering building are never quite the same when they walk out.
medevilenemy
medevilenemy
I don't know how to do that. I've tried to figure out vertex arrays/VBOs before... but without any success. Could someone show a modification of the code that would work? Thanks for commenting.
There was a saying we had in college: Those who walk into the engineering building are never quite the same when they walk out.
JParishy
JParishy
For vertex arrays, you'd want to do something like...

float Triangle[3 * 3] = {    -0.5f, 0.0f, 0.0f,      0.0f, 0.5f, 0.0f,      0.5f, 0.0f, 0.0f, };// http://opengl.org/documentation/specs/man_pages/hardcopy/GL/html/gl/enableclientstate.html// void glEnableClientState(GLenum cap)glEnableClientState(GL_VERTEX_ARRAY);/* You are probably going to want to enable GL_TEXTURE_COORD_ARRAY and GL_NORMAL_ARRAY. */// http://www.opengl.org/documentation/specs/man_pages/hardcopy/GL/html/gl/vertexpointer.html// void glVertexPointer(GLint size, GLenum type, GLsizei stride, const GLvoid *pointer )glVertexPointer(3, GL_FLOAT, 0, Triangle);/* There is also glNormalPointer() and glTexCoordPointer() */// glTexCoordPointer(): http://www.opengl.org/documentation/specs/man_pages/hardcopy/GL/html/gl/texcoordpointer.html// glNormalPointer(): http://www.opengl.org/documentation/specs/man_pages/hardcopy/GL/html/gl/normalpointer.html// http://www.opengl.org/documentation/specs/man_pages/hardcopy/GL/html/gl/drawarrays.html// void glDrawArrays(GLenum mode, GLint first, GLsizei count)glDrawArrays(GL_TRIANGLE, 0, 1); // count is number of primitives, not vertices


I hope that helps.
medevilenemy
medevilenemy
How do I color, place, and move so many particles?
There was a saying we had in college: Those who walk into the engineering building are never quite the same when they walk out.
medevilenemy
medevilenemy
Apologies, but I don't know the first thing about how to use vertex arrays (though I keep trying to figure it out).
There was a saying we had in college: Those who walk into the engineering building are never quite the same when they walk out.
JParishy
JParishy
Did you read my post above? The sample and the links in the comments will tell you everything you need to know.
mkuhlens
mkuhlens
Another thing you can try which can give a great performance increase is point sprites. It essentially mimics having a textured quad as a particle but you need only supply a point.

check out the GL_ARB_point_sprite extension.

here is some sample code I took from a particle system I wrote back in the day.

const float quadratic[] =  { 1.0f, 0.0f, 0.01f };glPointParameterfvARB(GL_POINT_DISTANCE_ATTENUATION_ARB, quadratic);glPointParameterfARB(GL_POINT_FADE_THRESHOLD_SIZE_ARB, 60.0f);glPointParameterfARB(GL_POINT_SIZE_MIN_ARB, 1.0f);glPointParameterfARB(GL_POINT_SIZE_MAX_ARB, MaxPointSize);glTexEnvf(GL_POINT_SPRITE_ARB, GL_COORD_REPLACE_ARB, GL_TRUE);glEnable(GL_POINT_SPRITE_ARB);glPointSize( size );	glBegin(GL_POINTS);    	for(all particles){    glColor3f(particle->color.x, particle->color.y, particle->;color.z);    glVertex3f(particle->position.x,particle->position.y,particle->position.z);}glEnd();glDisable(GL_POINT_SPRITE_ARB);
medevilenemy
medevilenemy
I'm trying it the point sprite way... just one more thing about that: how do I enable the extension?
There was a saying we had in college: Those who walk into the engineering building are never quite the same when they walk out.
Fingers_
Fingers_
You could probably improve your performance a bunch just by putting your glBegin() just before the "for(i=0; i
mkuhlens
mkuhlens
Quote:
Original post by medevilenemy
I'm trying it the point sprite way... just one more thing about that: how do I enable the extension?


Enabling the extension involves checking your vid card for support of the extension. You can get a list of supported extensions with glGetString(GL_EXTENSIONS). Its sort of dirty on windows what you have to do. For example with the point sprite extension you need to check for "GL_ARB_point_sprite" and if your card supports it do something like this

PFNGLPOINTPARAMETERFVPROC glPointParameterfv = NULL;PFNGLPOINTPARAMETERFPROC  glPointParameterf = NULL;glPointParameterfv =  (PFNGLPOINTPARAMETERFVPROC) wglGetProcAddress("glPointParameterfv");glPointParameterf =  (PFNGLPOINTPARAMETERFPROC) wglGetProcAddress("glPointParameterf");


As I said it can get sort of dirty. Since this is probably not enough information, I suggest googling the topic of opengl extensions. There are many tutorials. There are also libraries that can help you load extensions. GLEW is a good one. If you get deep into opengl and use windows then extensions will be a part of your life so its nice to get familiar with them.
joe1024
joe1024
20 fps drop ? From how many fps ? 1000 or 100 ? Or 40 ? Thats a huge difference.

OK, so you
ve got the particles rendered. Thats good. Whats even better is that you want that rendered faster, since youve got the slowest possible method of rendering particles. but at least it works.

But, no matter how efficiently you render them, you still have one major CPU bottleneck - CPU update of particle
s vertices. Get rid of that by calculating the vertex positions inside the vertex shader. Google these threads for more info - I believe there was an article on MVPs Robert Dunlops site about this (not sure if it was vertex-shader specific, though).

Using this method, youll put the particles update from CPU to GPU, which is fine, since its extremely improbable youll use the vertex-transform muscle power of todays cards to their max, anyway.
clb
clb
You don't measure performance by FPS or by the change in FPS. You measure it by timing *how long* it takes to execute your code. Even more important is to get to know how occupied each stage of the pipeline is with respect to the other stages. Typically the stages you're interested in are something like CPU usage, dynamic per-frame CPU->GPU data bandwidth, GPU vertex and pixel processing overhead, framebuffer pixel fillrates.

It doesn't help anyone too much at all to see that the FPS dropped by 20, since it doesn't give a clue to the reason why that happens. Is it because CPU processing of the particles was too slow? or perhaps because transferring the particle vertices in VB is too slow? or perhaps because of the big particle sizes causing fillrate bottleneck, or a texture read bottleneck?

There's a few easy things to do to get started on the right track of profiling. First, get rid of the print(fps) and replace it with print(msecs in CPU logic per frame) and print(msecs in render calls per frame). To get a rough estimate whether you're CPU or GPU limited, measure the time spent in D3DDevice->Present(), (and Begin()) as any time spent there will tell you that the CPU is stalling to wait for GPU to finish rendering.

If you're GPU limited, you typically want to know what causes it. Too much dynamic data? Too big/many textures? Too much per-pixel fill? Too much arithmetic or shader complexity? The typical/quick way to evaluate between these is to reduce to smaller textures, simpler shaders, remove filling, etc.
Another way is to use pix or perfhud. My advice is quite DX-specific and you're using OGL, but I reckon perfhud works on OGL as well.
medevilenemy
medevilenemy
Sorry about the poor representation of the slowdown... in the normal immediate mode version it drops from about 47-48fps to about 18-20. From experience with earlier rough code of mine, it is almost definitely the immediate-mode draw calls bottlenecking the CPU. glBegin() itself is rather slow, but glBegin() with three glVertex3f() and a glEnd() 10,000 times per frame is horrific. Ideally, I'd like to offload as much of this to the gpu as possible. Both VAs and Point sprites look like great ways to do this (especially point sprites since it would only need a third of the verts, and those would all be kicked off to the gpu and out of the immediate pipeline.

I'm working on trying to get the point sprite extension to work, currently trying to get over a couple errors (PFNGLPOINTPARAMETERFVPROC undefined, etc). Perhaps I have an outtdated version of glext.h
There was a saying we had in college: Those who walk into the engineering building are never quite the same when they walk out.
medevilenemy
medevilenemy
Just downloaded the newest version of glext.h, and changed the calls from glPointParameterfvARB and glPointParameterfARB to glPointParameterfv and glPointParameterf and it compiles and draws now. Tweaked a couple more things and I now have 10,000 particles with a hit of about 3fps from about 60 (sorry, I reported it wrong before). I'm working on tweaking some more to get that down farther (also, my dev machine is intentionally weaker than I expect most systems to run this will be. It only has intel integrated graphics).

Anyway, thanks for all the help, any more suggestions welcome!
There was a saying we had in college: Those who walk into the engineering building are never quite the same when they walk out.
medevilenemy
medevilenemy
Ok, I have it working fairly well now. Just a couple things I can't figure out how to tweak properly: I can't figure out how to get the particle size to change... glPointSize() doesn't seem to do anything, The particles fade to black as the camera moves towards them, and get brighter as you move away... I'm guessing that has something to do with the attenuation but I can't get that to do anything (it doesn't seem to have any obvious effect). Thanks again!

Perhaps my dev machine just doesn't support the extension? (it is supposed to have full Opengl 1.5, idk when this extension was introduced)

[Edited by - medevilenemy on July 18, 2008 1:38:03 PM]
There was a saying we had in college: Those who walk into the engineering building are never quite the same when they walk out.
DevFred
DevFred
As already suggested, moving the glBegin and glEnd out of the draw function is very likely to give a significant performance boost without changing too much:

void REPart::DrawParticle(void){    float size = 0.001;    glColor4f(red, green, blue, alpha);    glVertex3d(x, y, z);    glVertex3d(x+size, y, z);    glVertex3d(x+size, y+size, z);    z+=0.006;    alpha-=0.01;}// ...    void Update() // Updates position    {        // ...        glBegin(GL_TRIANGLES);        for(i = 0; i < MAX_ENGINE_PARTS; ++i)        {            if(parts.IsVisible())                parts.DrawParticle();            else                parts.InitParticle(dx, wid, dy, len, dz, r, g, b);        }        glEnd();        // ...    }


Note how I switched the if/else blocks to get rid of the == false.

You don't need a seperate step in calculating the absolute value of width and height. And since you seem to need half as a divisor, I suggest to calculate the inverse and multiply by that instead:

    float width = fabs(xmax - xmin);    float ihalf = 2.0f / width;    float height = fabs(ymax - ymin);    float ihhalf = 2.0f / height;    alpha  = 1.0f - (fabs(x) * ihalf);    alpha -= (fabs(y) * ihalf);


The generation of coordinates seems wrong. You start at a point between 0 and xmin, and then add a random number between 0 and xmax. So if you say "I want a point between 99 and 100", you'll get a point between 0 and 199. Not what you want, right? I think you want this instead:
    x = xmin + (((float)rand()/(float)RAND_MAX))*(width);    y = ymin + (((float)rand()/(float)RAND_MAX))*(height);

BTW all this repeating random code is hard on the eye. Make a new inline function that does just that and call it. Then you can keep all changes in one place. For example, you can get rid of the division:
inline float random(float interval){    const float irandmax = 1.0f / RAND_MAX; // evaluated at compile time    return rand() * irandmax * interval; // no division at runtime}// ...    x = xmin + random(width);    y = ymin + random(height);


OK, on to your IsVisible member function. Another one of those if boolexp return true; else return false; examples :) You can make it a lot shorter:
bool REPart::IsVisible() // no need for void inside the paranthesis{    return alpha > 0; // true if alpha is greater than zero, else false}


One last thing: you are not programming in C++, you are programming in C with classes. For example, MAX_ENGINE_PARTS looks very suspicious (a C macro?). Manually looping with an integer and using parts is also not C++.
medevilenemy
medevilenemy
Sorry, a lot of that code has been changed now because of the attempted switch to point sprites. the IsVisible function no longer exists, the drawing routine has been completely rewritten, and doesn't work like the old code. I admit the coordinate generation code is kind of bad... actually, its some old hacked in code from a different project that I'm just using to get the rough effect down (it generates correctly, but in the wrong locations). I'll fix that soon.

I know I don't need to use == false (and at the moment I'm not since the thing works differently now) but I've just always liked to see that there. MAX_ENGINE_PARTS is a #define macro. I guess I'm just one of those few people that always kind of liked the preprocessor. using an integer and looping through parts is valid in this case since I'm actually using an array of known constant size of particle objects. Whenever a particle "dies" it isn't deleted, it is merely reinitialized. Admittedly, I have a rather odd style which is something of a mix of C and C++. Not "proper", I know, but its what I like and what works for me..

Right now, my focus is on figuring out why the point sprite extension doesn't seem to be working right. It looks like intel didn't include it in its implementation of OpenGL. I may need to switch to a different way to speed up the code. I don't know yet, but since I may need perhaps 20 of these effects on the screen at once (at different distances from the camera, of course) I definitely need it as efficient as possible. (and hopefully as simple as possible, so I can tweak it)
There was a saying we had in college: Those who walk into the engineering building are never quite the same when they walk out.
medevilenemy
medevilenemy
Just tried it on my main PC (Athlon 64 X2 4800+, 2gb ddr2, Radeon HD 2600pro) and it looks the same... something doesn't seem quite right. I tried using the opengl extensions viewer to find GL_POINT_SPRITE_ARB and it doesn't seem to exist. There is, however, a GL_ARB_POINT_SPRITE. Maybe that has something to do with it?
There was a saying we had in college: Those who walk into the engineering building are never quite the same when they walk out.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.