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.