orlp
orlp

Reputation: 118006

How to render a lot of colored, rotated, scaled, and translated quads in one pass?

We are talking 2D sprites here, so I have 4 vertices for each sprite (8 GLint's) and 4 texcoords (another 8 GLint's). I have a sorting routine which spits out lists of sprites that can be rendered in one pass (they have the same blending and same texture, blablabla). However, each sprite also has a translation, rotation, scale, etc.

I currently do this (pseudocode):

cdef int *vertices
cdef int *texcoords

bind_texture(texture)
set_blending_mode_and_other_blablabla(spritelist)

vertices = alloc_mem(len(sprites) * 8 * sizeof(GLint))
texcoords = alloc_mem(len(sprites) * 8 * sizeof(GLint))

glEnableClientState(GL_VERTEX_ARRAY)
glEnableClientState(GL_TEXTURE_COORD_ARRAY)

glTexCoordPointer(2, GL_INT, 0, texcoords)
glVertexPointer(2, GL_INT, 0, vertices)

load_vertices(sprites, vertices)
load_texcoords(sprites, texcoords)

for index, sprite in spritelist:
    glPushMatrix()

    glColor4f(sprite.red, sprite.green, sprite.blue, sprite.alpha)
    glTranslatef(int(sprite.x), int(sprite.y), 0.0)
    glRotatef(sprite.rotation, 0.0, 0.0, 1.0)
    glScalef(sprite.scale_x, sprite.scale_y, 1.0)
    glTranslatef(-int(sprite.anchor_x), -int(sprite.anchor_y), 0.0)

    glDrawArrays(GL_QUADS, 4 * i, 4)

    glPopMatrix()

Now, as you can guess, this isn't terribly fast. I'd like to draw everything in one pass. I really have no idea how to pass the translation, rotation, etc data into OpenGL. If someone could point out an efficient render path or two that would be very nice.

The exact per-sprite unique data is:

Please be gentle, I'm new to OpenGL.

Upvotes: 2

Views: 1786

Answers (1)

genpfault
genpfault

Reputation: 52167

Stick your sprite info in a vertex attribute(s) and apply them in your vertex shader.

Alternatively you can move the scaling, translation, and rotation computation onto the CPU and just pass off the pre-transformed vertex buffer off to the GPU each frame. This will cut way down on GL API calls, at the cost of increased CPU usage. I'd recommend Eigen or glm for the heavy matrix lifting.

EDIT: Shader solution:

// g++ main.cpp -lGLEW -lglut -lGL
#include <GL/glew.h>
#include <GL/glut.h>
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

// OpenGL Mathematics (GLM): http://glm.g-truc.net/
#include <glm/glm.hpp>
#include <glm/gtc/random.hpp>
using namespace glm;

// stores/manipulates a list of rectangular sprites and their vertexes
struct SpriteWrangler
{
    SpriteWrangler( unsigned int aSpriteCount )
    {
        verts.resize( aSpriteCount * 4 );
        states.resize( aSpriteCount );

        for( size_t i = 0; i < states.size(); ++i )
        {
            states[i].vel = linearRand( vec2( -30, -30 ), vec2( 30, 30 ) );
            states[i].rotvel = linearRand( -1.0f, 1.0f );

            Vertex vert;
            vert.pos = linearRand( vec2( -400, -400 ), vec2( 400, 400 ) );
            vert.dim = linearRand( vec2( 20, 20 ), vec2( 60, 60 ) );
            vert.rotation = linearRand( 0.0f, 2 * 3.14159f );
            vert.r = (unsigned char)linearRand( 64.0f, 255.0f );
            vert.g = (unsigned char)linearRand( 64.0f, 255.0f );
            vert.b = (unsigned char)linearRand( 64.0f, 255.0f );
            vert.a = 255;

            vert.meta = vec2( 5, 0 );
            verts[i*4 + 0] = vert;
            vert.meta = vec2( 15, 0 );
            verts[i*4 + 1] = vert;
            vert.meta = vec2( 25, 0 );
            verts[i*4 + 2] = vert;
            vert.meta = vec2( 35, 0 );
            verts[i*4 + 3] = vert;
        }
    }

    void wrap( const float minVal, float& val, const float maxVal )
    {
        if( val < minVal )
            val = maxVal - fmod( maxVal - val, maxVal - minVal );
        else
            val = minVal + fmod( val - minVal, maxVal - minVal );
    }

    void Update( float dt )
    {
        for( size_t i = 0; i < states.size(); ++i )
        {
            Vertex& vert = verts[i*4 + 0];
            vert.pos += states[i].vel * dt;
            vert.rotation += states[i].rotvel * dt;

            wrap( -400.0f, vert.pos.x, 400.0f );
            wrap( -400.0f, vert.pos.y, 400.0f );
            wrap( 0.0f, vert.rotation, 2 * 3.14159f );

            verts[i*4 + 1].pos = verts[i*4 + 2].pos = verts[i*4 + 3].pos = vert.pos;
            verts[i*4 + 1].rotation = verts[i*4 + 2].rotation = verts[i*4 + 3].rotation = vert.rotation;
        }
    }

    struct Vertex
    {
        vec2 pos;
        vec2 dim;
        vec2 meta;
        float rotation;
        unsigned char r, g, b, a;
    };

    struct State
    {
        vec2 vel;       // units per second
        float rotvel;   // radians per second
    };

    vector< Vertex > verts;
    vector< State > states;
};

// RAII vertex attribute wrapper
struct Attrib
{
    Attrib( GLint prog, const char* name, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* pointer )
    {
        mLoc = glGetAttribLocation( prog, name );
        if( mLoc < 0 ) return;
        glVertexAttribPointer( mLoc, size, type, normalized, stride, pointer );
        glEnableVertexAttribArray( mLoc );
    }

    ~Attrib()
    {
        if( mLoc >=0 ) glDisableVertexAttribArray( mLoc );
    }

    GLint mLoc;
};

// GLSL shader program loader
struct Program
{
    static GLuint Load( const char* vert, const char* geom, const char* frag )
    {
        GLuint prog = glCreateProgram();
        if( vert ) AttachShader( prog, GL_VERTEX_SHADER, vert );
        if( geom ) AttachShader( prog, GL_GEOMETRY_SHADER, geom );
        if( frag ) AttachShader( prog, GL_FRAGMENT_SHADER, frag );
        glLinkProgram( prog );
        CheckStatus( prog );
        return prog;
    }

private:
    static void CheckStatus( GLuint obj )
    {
        GLint status = GL_FALSE, len = 10;
        if( glIsShader(obj) )   glGetShaderiv( obj, GL_COMPILE_STATUS, &status );
        if( glIsProgram(obj) )  glGetProgramiv( obj, GL_LINK_STATUS, &status );
        if( status == GL_TRUE ) return;
        if( glIsShader(obj) )   glGetShaderiv( obj, GL_INFO_LOG_LENGTH, &len );
        if( glIsProgram(obj) )  glGetProgramiv( obj, GL_INFO_LOG_LENGTH, &len );
        std::vector< char > log( len, 'X' );
        if( glIsShader(obj) )   glGetShaderInfoLog( obj, len, NULL, &log[0] );
        if( glIsProgram(obj) )  glGetProgramInfoLog( obj, len, NULL, &log[0] );
        std::cerr << &log[0] << std::endl;
        exit( -1 );
    }

    static void AttachShader( GLuint program, GLenum type, const char* src )
    {
        GLuint shader = glCreateShader( type );
        glShaderSource( shader, 1, &src, NULL );
        glCompileShader( shader );
        CheckStatus( shader );
        glAttachShader( program, shader );
        glDeleteShader( shader );
    }
};

#define GLSL(version, shader) "#version " #version "\n" #shader

const char* vert = GLSL
(
    120,
    uniform mat4 projection;
    uniform mat4 modelview;

    attribute vec2 position;
    attribute vec2 scale;
    attribute float rotation;
    attribute vec4 color;

    attribute vec2 meta;

    varying vec4 fragColor;
    varying vec2 fragTexCoord;

    void main( void )
    {
        fragColor = color;

        vec2 off;
        vec2 tex;
        // probably a better way to do this
        if( meta.x < 10.0 )
        {
            off = vec2( -1.0, -1.0 );
            tex = vec2( 0.0, 0.0 );
        }
        else if( meta.x < 20.0 )
        {
            off = vec2( 1.0, -1.0 );
            tex = vec2( 1.0, 0.0 );
        }
        else if( meta.x < 30.0 )
        {
            off = vec2( 1.0, 1.0 );
            tex = vec2( 1.0, 1.0 );
        }
        else if( meta.x < 40.0 )
        {
            off = vec2( -1.0, 1.0 );
            tex = vec2( 0.0, 1.0 );
        }
        fragTexCoord = tex;

        // column 1,
        // column 2,
        // column 3
        mat3 scale_mat = mat3
            (
            0.5*scale.x,    0.0,            0.0,
            0.0,            0.5*scale.y,    0.0,
            0.0,            0.0,            1.0
            );

        mat3 rotate_mat = mat3
            (
            cos(rotation),  sin(rotation),  0.0,
            -sin(rotation), cos(rotation),  0.0,
            0.0,            0.0,            1.0
            );

        mat3 translate_mat = mat3
            (
            1.0,        0.0,        0.0,
            0.0,        1.0,        0.0,
            position.x, position.y, 1.0
            );

        vec3 xformed = translate_mat * rotate_mat * scale_mat * vec3( off, 1.0 );

        gl_Position = projection * modelview * vec4( xformed, 1.0 );
    }
);

const char* frag = GLSL
(
    120,
    uniform sampler2D texture;

    varying vec4 fragColor;
    varying vec2 fragTexCoord;

    void main( void )
    {
        gl_FragColor = fragColor * texture2D( texture, fragTexCoord );
    }
);

GLuint tex = 0;
void display()
{
    // timekeeping
    static int prvTime = glutGet(GLUT_ELAPSED_TIME);
    const int curTime = glutGet(GLUT_ELAPSED_TIME);
    const float dt = ( curTime - prvTime ) / 1000.0f;
    prvTime = curTime;

    // sprite updates
    static SpriteWrangler wrangler( 100 );
    wrangler.Update( dt );
    vector< SpriteWrangler::Vertex >& verts = wrangler.verts;

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // set up projection and camera
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    double w = glutGet( GLUT_WINDOW_WIDTH );
    double h = glutGet( GLUT_WINDOW_HEIGHT );
    double ar = w / h;
    glOrtho( -400 * ar, 400 * ar, -400, 400, -1, 1);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    // prepare to render
    static GLuint prog = Program::Load( vert, NULL, frag );
    glUseProgram( prog );

    GLfloat projection[16];
    glGetFloatv( GL_PROJECTION_MATRIX, projection );
    glUniformMatrix4fv( glGetUniformLocation( prog, "projection" ), 1, GL_FALSE, projection );

    GLfloat modelview[16];
    glGetFloatv( GL_MODELVIEW_MATRIX, modelview );
    glUniformMatrix4fv( glGetUniformLocation( prog, "modelview" ), 1, GL_FALSE, modelview );

    glUniform1i( glGetUniformLocation( prog, "texture" ), 0 );
    glActiveTexture( GL_TEXTURE0 );
    glBindTexture( GL_TEXTURE_2D, tex );

    // render
    {
        Attrib a1( prog, "position", 2, GL_FLOAT, GL_FALSE, sizeof(SpriteWrangler::Vertex), &verts[0].pos.x );
        Attrib a2( prog, "meta", 2, GL_FLOAT, GL_FALSE, sizeof(SpriteWrangler::Vertex), &verts[0].meta.x );
        Attrib a3( prog, "scale", 2, GL_FLOAT, GL_FALSE, sizeof(SpriteWrangler::Vertex), &verts[0].dim.x );
        Attrib a4( prog, "rotation", 1, GL_FLOAT, GL_FALSE, sizeof(SpriteWrangler::Vertex), &verts[0].rotation );
        Attrib a5( prog, "color", 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(SpriteWrangler::Vertex), &verts[0].r );
        glDrawArrays( GL_QUADS, 0, verts.size() );
    }

    glutSwapBuffers();
}

// run display() every 16ms or so
void timer( int extra )
{
    glutTimerFunc( 16, timer, 0 );
    glutPostRedisplay();
}

int main(int argc, char **argv)
{
    glutInit( &argc, argv );
    glutInitWindowSize( 600, 600 );
    glutInitDisplayMode( GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE );
    glutCreateWindow( "GLSL Sprites" );
    glewInit();

    // create random texture
    unsigned char buffer[ 32 * 32 * 3 ];
    for( unsigned int i = 0; i < sizeof( buffer ); ++i )
    {
        buffer[i] = (unsigned char)linearRand( 0.0f, 255.0f );
    }

    // upload texture data
    glGenTextures(1, &tex);
    glBindTexture(GL_TEXTURE_2D, tex);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );
    glTexImage2D(GL_TEXTURE_2D, 0, 3, 32, 32, 0, GL_RGB, GL_UNSIGNED_BYTE, buffer);

    glutDisplayFunc( display );
    glutTimerFunc( 0, timer, 0 );
    glutMainLoop();
    return 0;
}

Upvotes: 2

Related Questions