Chapter 26. Render to and read from GL texture

Here I want to render a bit more complicated white noise images. I want bigger dots with a soft transition between them. The task splits in following steps:

  • Generate initial black/white image (as we did in previous chapter)
  • Generate a GL texture out of it
  • Make it renderable (associate a render buffer with it)
  • Force GL to render to new render buffer
  • Render random black and white bigger dots into it
  • Read this surface back from GPU
  • Blur received image
  • And save it

It will require a few new variables and functions in Texture class.

1. Start VS, open C:\CPP\a997modeler\p_windows\p_windows.sln.


2. Open Texture.h and replace code by:

 #pragma once
#include <string>
#include <vector>
#include "platform.h"

class Texture
{
public:
    //texture's individual descriptor:
    unsigned int GLid = -1; // GL texture id
    int size[2] = { 0,0 };  // image size
    std::string source; //file name
    //if renderable ?
    unsigned int frameBufferId = 0;
    unsigned int depthBufferId = 0;
    //end of descriptor

    //static array (vector) of all loaded textures
    static std::vector<Texture*> textures;

public:
    static int loadTexture(std::string filePath, int glRepeatH = GL_MIRRORED_REPEAT, int glRepeatV = GL_MIRRORED_REPEAT);
    static int findTexture(std::string filePath);
    static int cleanUp();
    static unsigned int getGLid(int texN) { return textures.at(texN)->GLid; };

    static int saveBMP(std::string filePath, unsigned char* buff, int w, int h, int bytesPerPixel=4);
    static int saveTGA(std::string filePath, unsigned char* buff, int w, int h, int bytesPerPixel=4);

    static int generateTexture(std::string imgID, int w, int h, unsigned char* imgData, int glRepeatH = GL_MIRRORED_REPEAT, int glRepeatV = GL_MIRRORED_REPEAT);
    static int detachRenderBuffer(Texture* pTex);
    static int attachRenderBuffer(int texN, bool zBuffer = false) { return attachRenderBuffer(textures.at(texN), zBuffer); };
    static int attachRenderBuffer(Texture* pTex, bool zBuffer = false);
    static int setRenderToTexture(int texN) { return setRenderToTexture(textures.at(texN)); };
    static int setRenderToTexture(Texture* pTex);
    static int getImageFromTexture(int texN, unsigned char* imgData);
    static int blurRGBA(unsigned char* imgData, int w, int h, int blurLevel);
};


3. Open Texture.cpp and replace code by:

#include "Texture.h"
#define STB_IMAGE_IMPLEMENTATION  //required by stb_image.h
#include "stb_image.h"
#include "platform.h"
#include "utils.h"

//static array (vector) of all loaded textures
std::vector<Texture*> Texture::textures;

int Texture::loadTexture(std::string filePath, int glRepeatH, int glRepeatV) {
    int texN = findTexture(filePath);
    if (texN >= 0)
        return texN;
    //if here - texture wasn't loaded
    // load an image
    int nrChannels, w, h;
    unsigned char* imgData = stbi_load(filePath.c_str(),
        &w, &h, &nrChannels, 4); //"4"-convert to 4 channels -RGBA
    if (imgData == NULL) {
        mylog("ERROR in Texture::loadTexture loading image %s\n", filePath.c_str());
    }
    // generate texture
    generateTexture(filePath, w, h, imgData, glRepeatH, glRepeatV);
    // release image data
    stbi_image_free(imgData);

    return (textures.size() - 1);
}
int Texture::findTexture(std::string filePath) {
    int texturesN = textures.size();
    if (texturesN < 1)
        return -1;
    for (int i = 0; i < texturesN; i++) {
        Texture* pTex = textures.at(i);
        if (pTex->source.compare(filePath) == 0)
            return i;
    }
    return -1;
}
int Texture::cleanUp() {
    int texturesN = textures.size();
    if (texturesN < 1)
        return -1;
    //detach all textures
    glActiveTexture(GL_TEXTURE0); // activate the texture unit first before binding texture
    glBindTexture(GL_TEXTURE_2D, 0);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, 0);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, 0);
    glActiveTexture(GL_TEXTURE3);
    glBindTexture(GL_TEXTURE_2D, 0);
    //release all textures
    for (int i = 0; i < texturesN; i++) {
        Texture* pTex = textures.at(i);
        detachRenderBuffer(pTex);
        glDeleteTextures(1, (GLuint*)&pTex->GLid);
        pTex->GLid = 0;
        delete pTex;
    }
    textures.clear();
    return 1;
}
int Texture::saveBMP(std::string filePath, unsigned char* buff, int w, int h, int bytesPerPixel) {
    std::string fullPath = getFullPath(filePath);
    std::string inAppPath = getInAppPath(fullPath);
    makeDirs(inAppPath);
    FILE* outFile;
    myFopen_s(&outFile, fullPath.c_str(), "wb");
    if (outFile == NULL) {
        mylog("ERROR in Texture::saveBMP: Can't create file %s\n", filePath.c_str());
        return -1;
    }
    struct {
        char chars2skip[2]; //
            //BMP Header
        char bm[2] = { 0x42, 0x4D }; //	"BM"
        myUint32 fileSize = 0; // Size of the BMP file, little-endian
        myUint32 unused = 0;
        myUint32 dataOffset = 0; // Offset where the pixel array (bitmap data) can be found, little-endian
        //DIB Header
        myUint32 dibHeaderSize = 0; // Number of bytes in the DIB header, little-endian
        myUint32 imgW = 0; // Width of the bitmap in pixels, little-endian
        myUint32 imgH = 0; // Height of the bitmap in pixels, little-endian
        char colorPlainsN[2] = { 1,0 };
        char bitsPerPixel[2] = { 32,0 };
        myUint32 compression = 0; //0-BI_RGB
        myUint32 dataSize = 0; // Size of the raw bitmap data (including padding), little-endian
        myUint32 printResution[2] = { 2835 ,2835 }; // Print resolution of the image,
                //72 DPI × 39.3701 inches per metre yields 2834.6472, little-endian
        myUint32 paletteColors = 0; // Number of colors in the palette
        myUint32 importantColors = 0; //0 means all colors are important
    } bmpHeader;
    int rowSize = w * bytesPerPixel;
    int rowPadding = (4 - rowSize % 4) % 4;
    int rowSizeWithPadding = rowSize + rowPadding;
    int dataSize = rowSizeWithPadding * h;
    int headerSize = sizeof(bmpHeader) - 2; //-chars2skip
    bmpHeader.fileSize = dataSize + headerSize;
    bmpHeader.dataOffset = headerSize;
    bmpHeader.dibHeaderSize = headerSize - 14; //-BMP Header size
    bmpHeader.imgW = w;
    bmpHeader.imgH = h;
    if (bytesPerPixel != 4)
        bmpHeader.bitsPerPixel[0] = bytesPerPixel * 8;
    bmpHeader.dataSize = dataSize;
    fwrite(&bmpHeader.bm, 1, headerSize, outFile);
    //data, from bottom to top
    unsigned char zero[4] = { 0,0,0,0 };
    unsigned char bgra[4];
    for (int y = h - 1; y >= 0; y--) {
        for (int x = 0; x < w; x++) {
            int pixelOffset = y * rowSize + x * 4;
            bgra[0] = buff[pixelOffset + 2];
            bgra[1] = buff[pixelOffset + 1];
            bgra[2] = buff[pixelOffset + 0];
            bgra[3] = buff[pixelOffset + 3];
            fwrite(bgra, 1, bytesPerPixel, outFile);
        }
        if (rowPadding != 0)
            fwrite(zero, 1, rowPadding, outFile);
    }
    fflush(outFile);
    fclose(outFile);

    return 1;
}

int Texture::saveTGA(std::string filePath, unsigned char* buff, int w, int h, int bytesPerPixel) {
    std::string fullPath = getFullPath(filePath);
    std::string inAppPath = getInAppPath(fullPath);
    makeDirs(inAppPath);
    FILE* outFile;
    myFopen_s(&outFile, fullPath.c_str(), "wb");
    if (outFile == NULL) {
        mylog("ERROR in Texture::saveBMP: Can't create file %s\n", filePath.c_str());
        return -1;
    }
    unsigned char tgaHeader[18] = { 0,0,2,0,0,0,0,0,0,0,0,0, (unsigned char)(w % 256), (unsigned char)(w / 256),
        (unsigned char)(h % 256), (unsigned char)(h / 256), (unsigned char)(bytesPerPixel * 8), 0x20 };
    fwrite(tgaHeader, 1, 18, outFile);
    //data
    unsigned char bgra[4];
    for (int i = 0; i < w * h; i++) {
        int pixelOffset = i * 4;
        bgra[0] = buff[pixelOffset + 2];
        bgra[1] = buff[pixelOffset + 1];
        bgra[2] = buff[pixelOffset + 0];
        bgra[3] = buff[pixelOffset + 3];
        fwrite(bgra, 1, bytesPerPixel, outFile);
    }
    fflush(outFile);
    fclose(outFile);

    return 1;
}
int Texture::generateTexture(std::string imgID, int w, int h, unsigned char* imgData, int glRepeatH, int glRepeatV) {
    //glRepeat options: GL_REPEAT, GL_CLAMP_TO_EDGE, GL_MIRRORED_REPEAT
    if (!imgID.empty()) {
        int texN = findTexture(imgID);
        if (texN >= 0)
            return texN;
    }
    //if here - texture wasn't generated
    //create Texture object
    Texture* pTex = new Texture();
    textures.push_back(pTex);
    pTex->size[0] = w;
    pTex->size[1] = h;
    pTex->source.assign(imgID);
    // generate texture
    glGenTextures(1, (GLuint*)&pTex->GLid);
    glBindTexture(GL_TEXTURE_2D, pTex->GLid);
    // set the texture wrapping/filtering options (on the currently bound texture object)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, glRepeatH);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, glRepeatV);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);// GL_LINEAR);  //
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // attach image data (if provided)
    if (imgData != NULL) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pTex->size[0], pTex->size[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, imgData);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    return (textures.size() - 1);
}
int Texture::detachRenderBuffer(Texture* pTex) {
    if (pTex->frameBufferId == 0)
        return 0;
    if (pTex->depthBufferId > 0) {
        glDeleteRenderbuffers(1, (GLuint*)&pTex->depthBufferId);
        pTex->depthBufferId = 0;
    }
    glDeleteFramebuffers(1, (GLuint*)&pTex->frameBufferId);
    pTex->frameBufferId = 0;
    return 1;
}

int Texture::attachRenderBuffer(Texture* pTex, bool zBuffer) {
    if (pTex->frameBufferId > 0)
        return 0; //attached already
    //generate frame buffer
    glGenFramebuffers(1, (GLuint*)&pTex->frameBufferId);
    if (zBuffer) {
        //generate depth buffer
        glGenRenderbuffers(1, (GLuint*)&pTex->depthBufferId);
        // create render buffer and bind 16-bit depth buffer
        glBindRenderbuffer(GL_RENDERBUFFER, pTex->depthBufferId);
        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, pTex->size[0], pTex->size[1]);
        glBindRenderbuffer(GL_RENDERBUFFER, 0); //release
    }
    return 1;
}
int Texture::setRenderToTexture(Texture* pTex) {
    if (pTex->frameBufferId == 0) {
        mylog("ERROR in Texture::setRenderToTexture: %s not renderable", pTex->source.c_str());
        return -1;
    }
    // Bind the framebuffer
    glBindFramebuffer(GL_FRAMEBUFFER, pTex->frameBufferId);
    // specify texture as color attachment
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pTex->GLid, 0);
    // attach render buffer as depth buffer
    if (pTex->depthBufferId > 0) {
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, pTex->depthBufferId);
        glClear(GL_DEPTH_BUFFER_BIT);
    }
    // check status
    int status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (status != GL_FRAMEBUFFER_COMPLETE) {
        std::string str;
        if (status == GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT)
            str.assign("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT");
        else if (status == GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT)
            str.assign("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT");
        else if (status == GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE)
            str.assign("GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE");
        else if (status == GL_FRAMEBUFFER_UNSUPPORTED)
            str.assign("GL_FRAMEBUFFER_UNSUPPORTED");
        else
            str.assign("hz");
        mylog("Modeler.setRenderToTextureBind to texture %s failed: %s\n", pTex->source.c_str(), str.c_str());
        return -1;
    }
    glViewport(0, 0, pTex->size[0], pTex->size[1]);
    return 1;
}
int Texture::getImageFromTexture(int texN, unsigned char* imgData) {
    Texture* pTex = textures.at(texN);
    glBindTexture(GL_TEXTURE_2D, pTex->GLid);
    glBindFramebuffer(GL_FRAMEBUFFER, pTex->frameBufferId);

    glReadPixels(0, 0, pTex->size[0], pTex->size[1], GL_RGBA, GL_UNSIGNED_BYTE, imgData);
    return 1;
}
int Texture::blurRGBA(unsigned char* imgData, int w0, int h0, int blurLevel) {
    unsigned char* imgTemp = new unsigned char[w0 * h0 * 4];
    int w00 = blurLevel * 2 + 1;
    for (int y0 = 0; y0 < h0; y0++) {
        int y1 = y0 - blurLevel;
        int h1 = w00;
        if (y1 < 0) {
            int d = -y1;
            y1 += d;
            h1 -= d;
        }
        else if (y1 > h0 - w00) {
            int d = y1 - (h0 - w00);
            h1 -= d;
        }
        for (int x0 = 0; x0 < w0; x0++) {
            int x1 = x0 - blurLevel;
            int w1 = w00;
            if (x1 < 0) {
                int d = -x1;
                x1 += d;
                w1 -= d;
            }
            else if (x1 > w0 - w00) {
                int d = x1 - (w0 - w00);
                w1 -= d;
            }
            int sum[4] = { 0,0,0,0 };
            for (int y = y1; y < y1 + h1; y++) {
                for (int x = x1; x < x1 + w1; x++) {
                    int idx = (y * w0 + x) * 4;
                    for (int ch = 0; ch < 4; ch++)
                        sum[ch] += imgData[idx + ch];
                }
            }
            int n = w1 * h1;
            int idx = (y0 * w0 + x0) * 4;
            for (int ch = 0; ch < 4; ch++)
                imgTemp[idx + ch] = (unsigned char)(sum[ch] / n);
        }
    }
    memcpy(imgData, imgTemp, w0 * h0 * 4);
    delete[] imgTemp;
    return 1;
}

Hope the code is readable enough.


Following code renders and saves 3 images with different "blur depths":

int TheGame::run() {
    /*
    getReady();
    while (!bExitGame) {
        drawFrame();
    }
    cleanUp();
    */
    Shader::loadShaders();

    int wh[2] = { 64,64 };
    int bytesPerPixel = 4;
    unsigned char* imgData = new unsigned char[wh[1] * wh[0] * 4];
    for (int y = 0; y < wh[1]; y++)
        for (int x = 0; x < wh[0]; x++) {
            int idx = (y * wh[1] + x) * bytesPerPixel;
            for (int i = 0; i < 4; i++)
                imgData[idx + i] = getRandom(0, 1) * 255;
        }
    //background generated, generate texture:
    int texN = Texture::generateTexture("", wh[0], wh[1], imgData);
    Texture::attachRenderBuffer(texN);
    Texture::setRenderToTexture(texN);

    mat4x4 mProjection, mMVP;
    mat4x4_ortho(mProjection, -wh[0]  / 2, wh[0] / 2, -wh[1] / 2, wh[1] / 2, 1.f, -1.f);
    unsigned char RGBA[4];

    //create a simple 1x1 ucolor square
    GameSubj* pSquare = new GameSubj();
    gameSubjs.push_back(pSquare);
    ModelBuilder* pMB = new ModelBuilder();
    pMB->useSubjN(gameSubjs.size() - 1);
    //define VirtualShape
    VirtualShape vs;
    vs.setShapeType("box");
    v3set(vs.whl, 1, 1, 0);
    Material mt;
    //define material - flat red
    mt.shaderN = Shader::spN_flat_ucolor;
    mt.primitiveType = GL_TRIANGLES;
    mt.uColor.setRGBA(255, 0, 0, 255); //red
    pMB->useMaterial(&mt);
    pMB->buildBoxFace(pMB, "front", &vs);
    pMB->buildDrawJobs(gameSubjs);
    delete pMB;
    //copy mt to pAltMaterial
    pSquare->pAltMaterial = new Material(mt);
    //--end of pSquare
    DrawJob* pDJ = DrawJob::drawJobs.back();

    //-------------blurLevel 3
    int blurLevel = 3;
    std::string fileName = "wn64_blur3";
    int dotSize = blurLevel * 2;
    v3set(pSquare->scale, dotSize, dotSize, 1);

    for (int i = 0; i < 500; i++) {
        pSquare->ownCoords.pos[0] = getRandom(-wh[0] / 2, wh[0] / 2);
        pSquare->ownCoords.pos[1] = getRandom(-wh[1] / 2, wh[1] / 2);
        pSquare->ownCoords.setDegrees(0,0,getRandom(0,90));
        for (int ch = 0; ch < 4; ch++)
            RGBA[ch] = (unsigned char)(getRandom(0, 1) * 255);
        pSquare->pAltMaterial->uColor.setRGBA(RGBA);

        //prepare subject for rendering
        pSquare->buildModelMatrix(pSquare);
        //build MVP matrix for given subject
        mat4x4_mul(mMVP, mProjection, pSquare->ownModelMatrix);
        //render subject
        pDJ->execute((float*)mMVP, NULL, NULL, NULL, pSquare->pAltMaterial);
    }
    Texture::getImageFromTexture(texN, imgData);
    Texture::blurRGBA(imgData, wh[0], wh[1], blurLevel);

    Texture::saveBMP("/dt/out/" + fileName + ".bmp", imgData, wh[0], wh[1]);
    //-------------blurLevel 2
    blurLevel = 2;
    fileName = "wn64_blur2";
    dotSize = blurLevel * 2;
    v3set(pSquare->scale, dotSize, dotSize, 1);

    for (int i = 0; i < 500; i++) {
        pSquare->ownCoords.pos[0] = getRandom(-wh[0] / 2, wh[0] / 2);
        pSquare->ownCoords.pos[1] = getRandom(-wh[1] / 2, wh[1] / 2);
        pSquare->ownCoords.setDegrees(0, 0, getRandom(0, 90));
        for (int ch = 0; ch < 4; ch++)
            RGBA[ch] = (unsigned char)(getRandom(0, 1) * 255);
        pSquare->pAltMaterial->uColor.setRGBA(RGBA);

        //prepare subject for rendering
        pSquare->buildModelMatrix(pSquare);
        //build MVP matrix for given subject
        mat4x4_mul(mMVP, mProjection, pSquare->ownModelMatrix);
        //render subject
        pDJ->execute((float*)mMVP, NULL, NULL, NULL, pSquare->pAltMaterial);
    }
    Texture::getImageFromTexture(texN, imgData);
    Texture::blurRGBA(imgData, wh[0], wh[1], blurLevel);

    Texture::saveBMP("/dt/out/" + fileName + ".bmp", imgData, wh[0], wh[1]);
    //-------------blurLevel 1
    blurLevel = 1;
    fileName = "wn64_blur1";
    dotSize = blurLevel * 2;
    v3set(pSquare->scale, dotSize, dotSize, 1);

    for (int i = 0; i < 500; i++) {
        pSquare->ownCoords.pos[0] = getRandom(-wh[0] / 2, wh[0] / 2);
        pSquare->ownCoords.pos[1] = getRandom(-wh[1] / 2, wh[1] / 2);
        pSquare->ownCoords.setDegrees(0, 0, getRandom(0, 90));
        for (int ch = 0; ch < 4; ch++)
            RGBA[ch] = (unsigned char)(getRandom(0, 1) * 255);
        pSquare->pAltMaterial->uColor.setRGBA(RGBA);

        //prepare subject for rendering
        pSquare->buildModelMatrix(pSquare);
        //build MVP matrix for given subject
        mat4x4_mul(mMVP, mProjection, pSquare->ownModelMatrix);
        //render subject
        pDJ->execute((float*)mMVP, NULL, NULL, NULL, pSquare->pAltMaterial);
    }
    Texture::getImageFromTexture(texN, imgData);
    Texture::blurRGBA(imgData, wh[0], wh[1], blurLevel);

    Texture::saveBMP("/dt/out/" + fileName + ".bmp", imgData, wh[0], wh[1]);

    delete[] imgData;
    mylog("Ready\n");

    return 1;
}

4. Open TheGame.cpp and replace TheGame::run() function by the code above.

Details:

a) Line 13: we are creating 64x64 RGBA buffer.

b) Lines 14-19: filling this buffer out by a simple background (as in previous chapter). Each of 4 channels will look like

Combined channels image:

c) Lines 21-23: generating a texture, render buffer and setting rendering to created render buffer/texture.

d) Lines 29-49: creating a flat 1x1 square object which we will draw to generated texture.

e) Line 55: For the first image dots(squares) size will be 6x6.

f) Lines 58-72: Rendering these squares in random colors in random coordinates.

g) Line 73: Retrieving resulting image back from GPU:

where each channel looks like

h) Line 74: Blur:

where each channel looks like

i) Line 78: Saving in file.

j) Lines 77 to 126: the same for blur depth 2

and 1


5. Build and run. New images are generated and saved in

C:\CPP\a997modeler\p_windows\Debug\dt\out


6. In Windows File Explorer copy all newly generated files from

C:\CPP\a997modeler\p_windows\Debug\dt\out

to C:\CPP\engine\dt\common\img\whitenoise

So, now we have 5 BMP files in this folder.


7. Ok to delete folder C:\CPP\a997modeler\p_windows\Debug\dt .


These pretty pictures are not so precious by themselves, but their black/white channels - ARE. We'll need them soon.


Leave a Reply

Your email address will not be published. Required fields are marked *