Chapter 25. Writing BMP and TGA files

I was planning to generate and save a few "white noise" images for future use in some recognizable graphics format. In order not to overload the project by new heavy libraries, I decided to write own little function. For the sake of simplicity I won't use compression and will support only 1 pixel format - RGBA. The only 2 options that would fit are BMP and TGA formats. BMP is more common, TGA is simpler. However, Photoshop doesn't understand 4 bytes BMPs, Windows Paint doesn't understand TGAs, so, just in case, we'll implement both in Texture class.

Windows

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>

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
    //end of descriptor

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

public:
    static int loadTexture(std::string filePath);
    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);
};


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 texN = findTexture(filePath);
    if (texN >= 0)
        return texN;
    //if here - texture wasn't loaded
    //create Texture object
    Texture* pTex = new Texture();
    textures.push_back(pTex);
    pTex->source.assign(filePath);
    // load an image
    int nrChannels;
    unsigned char* imgData = stbi_load(filePath.c_str(),
        &pTex->size[0], &pTex->size[1], &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
    glGenTextures(1, &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, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // attach/load image data
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pTex->size[0], pTex->size[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, imgData);
    glGenerateMipmap(GL_TEXTURE_2D);
    // 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);
        glDeleteTextures(1, &pTex->GLid);
        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;
}

See the source code above, it's pretty self explanatory. Simply header and data. Just 1 detail: both formats are using BGRA pixel format, not RGBA.


Now, white noise. We will generate 64x64 RGBA image (not a texture, but simply 4-bytes pixels array). Then we'll fill out all 4 channels by random numbers in 0 to 255 range, so each of 4 channels will contain it's own black-and-white image. We'll do it in TheGame::run(). Following code generates 2 images:

First - with binary black/white values (0 or 255):

Second one - with gray-scale values (from 0 to 255):

And so on for all 4 channels, so resulting images will look like

and

Both images will be recorded in BMP format.

  • If you want to view them in Photoshop, you can save in TGA format also.

The code:

int TheGame::run() {
    /*
    getReady();
    while (!bExitGame) {
        drawFrame();
    }
    cleanUp();
    */
    int wh[2] = { 64,64 };
    int bytesPerPixel = 4;
    unsigned char* buff = new unsigned char[wh[1] * wh[0] * 4];
    std::string fileName = "wn64_2";
    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++)
                buff[idx + i] = getRandom(0, 1) * 255;
        }
    Texture::saveBMP("/dt/out/" + fileName + ".bmp", (unsigned char*)buff, wh[0], wh[1]);

    fileName = "wn64_256";
    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++)
                buff[idx + i] = getRandom(0, 255);
        }
    Texture::saveBMP("/dt/out/" + fileName + ".bmp", (unsigned char*)buff, wh[0], wh[1]);

    delete[] buff;
    mylog("Ready\n");
    return 1;
}

4. Open TheGame.cpp and replace this time NOT entire code as we did before, but only run() function by the code above.


5. Build and run.

Now images are generated and saved in

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


6. In Windows File Explorer under C:\CPP\engine\dt create new folder:

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


7. Copy both BMP files from C:\CPP\a997modeler\p_windows\Debug\dt\out

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


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

Actually, it's ok to delete parent folder dt ( C:\CPP\a997modeler\p_windows\Debug\dt) too, since it's automatically re-generating by xcopy instructions during each build.


Leave a Reply

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