Search code examples
openglopengl-estexturesvboopengl-es-1.1

Texture on quad rendered using vertex buffer objects only half transparent


I'm converting some code from OpenGL 1.3 to OpenGL ES 1.1. It's a 2D game, so it mostly comes down to rendering textures onto quads. There is no immediate mode in OpenGL ES, so I had to use vertex buffer objects instead.

But it seems like only one of the two triangles making up each quad handles transparency. Here's a screenshot:

enter image description here

Here's how I render textured quads right now, which causes that:

glBindTexture2D(GL_TEXTURE_2D, id);

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);

const GLfloat texture_coordinates[] = {0, 0,
                                       0, 1,
                                       1, 1,
                                       1, 0};
glTexCoordPointer(2, GL_FLOAT, 0, texture_coordinates);

const GLfloat vertices[] = {0, 0,
                            0, height,
                            width, height,
                            width, 0};
glVertexPointer(2, GL_FLOAT, 0, vertices);

const GLubyte indices[] = {0, 1, 2,
                           0, 2, 3};
glDrawElements(GL_TRIANGLE_STRIP, 6, GL_UNSIGNED_BYTE, indices);

glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);

And here is how I used to render textured quads using immediate mode, which works fine:

glBindTexture2D(GL_TEXTURE_2D, id);

glBegin(GL_QUADS);

glTexCoord2i(0, 0);
glVertex2i(0, 0);

glTexCoord2i(1, 0);
glVertex2i(width, 0);

glTexCoord2i(1, 1);
glVertex2i(width, height);

glTexCoord2i(0, 1);
glVertex2i(0, height);

glEnd();

Below is an example program reproducing the issue.

You can compile it on Linux like this:

g++ `pkg-config --cflags --libs sdl gl libpng` reproduce.cpp

And on Mac OS X like this:

clang++ -framework OpenGL `pkg-config --cflags --libs sdl libpng` reproduce.cpp

Here's a 512x256 transparent PNG image you can save as "transparent.png":

enter image description here

#include <cmath>
#include <cstdio>
#include <iostream>
#include <png.h>
#include <SDL.h>
#include <SDL_main.h>
#include <SDL_opengl.h>
#include <stdexcept>
#include <sstream>

#define USE_VBO 1

struct Pixmap {
    int width;
    int height;
    const unsigned char* data;
    GLenum format;
};

Pixmap load_png(const std::string& path)
{
    FILE* const file = fopen(path.c_str(), "rb");
    if (!file)
        throw std::runtime_error("Unable to open " + path);

    png_byte header[8];
    fread(header, 1, 8, file);
    const bool is_png = !png_sig_cmp(header, 0, 8);
    if (!is_png)
        throw std::runtime_error(path + " is not a PNG");

    png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING,
                                             NULL, NULL, NULL);
    if (!png)
        throw std::runtime_error("Failed to create png struct");

    png_infop info = png_create_info_struct(png);
    if (!info) {
        png_destroy_read_struct(&png, (png_infopp) NULL, (png_infopp) NULL);
        throw std::runtime_error("Failed to create png info struct");
    }

    png_infop info_end = png_create_info_struct(png);
    if (!info_end) {
        png_destroy_read_struct(&png, &info, (png_infopp) NULL);
        throw std::runtime_error("Failed to create png info struct");
    }

    if (setjmp(png_jmpbuf(png))) {
        png_destroy_read_struct(&png, &info, &info_end);
        throw std::runtime_error("Error from libpng");
    }

    png_init_io(png, file);
    png_set_sig_bytes(png, 8);
    png_read_info(png, info);

    int bit_depth;
    int color_type;
    png_uint_32 image_width, image_height;
    png_get_IHDR(png, info, &image_width, &image_height, &bit_depth,
                 &color_type, NULL, NULL, NULL);

    png_read_update_info(png, info);

    GLenum format;
    switch (color_type) {

    case PNG_COLOR_TYPE_RGBA:
        format = GL_RGBA;
        break;
    case PNG_COLOR_TYPE_RGB:
        format = GL_RGB;
        break;
    default:
        png_destroy_read_struct(&png, &info, &info_end);
        std::ostringstream message_stream;
        message_stream << "Unsupported PNG color type: " << color_type;
        throw std::runtime_error(message_stream.str());
    }

    const int row_bytes = png_get_rowbytes(png, info);
    png_byte* image_data = new png_byte[row_bytes * image_height];
    png_bytep* row_pointers = new png_bytep[image_height];
    for (unsigned int i = 0; i < image_height; i++)
        row_pointers[i] = image_data + i * row_bytes;

    png_read_image(png, row_pointers);

    png_destroy_read_struct(&png, &info, &info_end);
    delete[] row_pointers;
    fclose(file);

    Pixmap pixmap;
    pixmap.width = image_width;
    pixmap.height = image_height;
    pixmap.data = image_data;
    pixmap.format = format;
    return pixmap;
}

GLuint create_texture(Pixmap pixmap)
{
    GLuint id;
    glGenTextures(1, &id);
    glBindTexture(GL_TEXTURE_2D, id);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D, 0, pixmap.format, pixmap.width,
                 pixmap.height, 0, pixmap.format, GL_UNSIGNED_BYTE,
                 pixmap.data);
    return id;
}

void draw_texture(const GLuint id, const int width, const int height)
{
    glBindTexture(GL_TEXTURE_2D, id);

#if USE_VBO
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    const GLfloat texture_coordinates[] = {0, 0,
                                           0, 1,
                                           1, 1,
                                           1, 0};
    glTexCoordPointer(2, GL_FLOAT, 0, texture_coordinates);

    const GLfloat vertices[] = {0, 0,
                                0, height,
                                width, height,
                                width, 0};
    glVertexPointer(2, GL_FLOAT, 0, vertices);

    const GLubyte indices[] = {0, 1, 2,
                               0, 2, 3};
    glDrawElements(GL_TRIANGLE_STRIP, 6, GL_UNSIGNED_BYTE, indices);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
#else
    glBegin(GL_QUADS);

    glTexCoord2i(0, 0);
    glVertex2i(0, 0);

    glTexCoord2i(1, 0);
    glVertex2i(width, 0);

    glTexCoord2i(1, 1);
    glVertex2i(width, height);

    glTexCoord2i(0, 1);
    glVertex2i(0, height);

    glEnd();
#endif
}

int main(int argc, char* argv[])
{
    const int width = 512;
    const int height = 256;

    SDL_Init(SDL_INIT_VIDEO);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_SetVideoMode(width, height, 0, SDL_OPENGL);

    glEnable(GL_TEXTURE_2D);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, width, height, 0, -1, 1);
    glMatrixMode(GL_MODELVIEW);

    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    try {
        Pixmap pixmap = load_png("transparent.png");
        GLuint texture = create_texture(pixmap);
        draw_texture(texture, pixmap.width, pixmap.height);
    } catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
    }

    SDL_GL_SwapBuffers();

    SDL_Event event;
    for (;;) {
        SDL_WaitEvent(&event);
        if (event.type == SDL_QUIT)
            return 0;
    }

    return 0;
}

Solution

  • Seeing you use indexed drawing with a simple triangle strip for a quad made me wonder, and in fact this is your problem. Your index array looks like you want to draw two indexed triangles and not a single triangle strip. So you draw a triangle strip with 6 vertices and thus 4 triangles, which means additional triangles that wrap somehow behind your other two and cause double drawing, which then results in the darker parts.

    So the easiest solution would be to change GL_TRIANGLE_STRIP to GL_TRIANGLES, but maybe reorder your vertices/indices a bit, as otherwise you are drawing your triangles in clockwise order, whereas your 1.3 quad example uses counter-clockwise order (it may be that this is irrelevant in your case, but that would be the wrong approach in the first place, never ignore your ordering).

    But you know what, it's a triangle strip for two triangles, which needs just 4 vertices. So there isn't any need for any index array at all, just use good old glDrawArrays and draw the vertices in order. But reorder them a bit (triangle strips use a zig-zag pattern, so order them from left to right, top to bottom):

    const GLfloat texture_coordinates[] = {0, 1,
                                           0, 0,
                                           1, 1,
                                           1, 0};
    glTexCoordPointer(2, GL_FLOAT, 0, texture_coordinates);
    
    const GLfloat vertices[] = {0, height,
                                0, 0,
                                width, height,
                                width, 0};
    glVertexPointer(2, GL_FLOAT, 0, vertices);
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);