Skip to content

Programming Guides

Estard edited this page May 19, 2021 · 2 revisions

Table of Contents

Guides

Writing PNGs

General Procedure:

Suppose you have some image data and want to save it as a png file using the tga::writePNG() function. You probably have some storage similar to this:

struct Image
{
    uint32_t width, height;
    std::vector<Pixel> data;
};

The width and the height are the dimensions of your image and the data vector contains all the pixel data in linear memory. A single Pixel contains the data for a single point in your image. This means it is a collection of up to 4 color channels (red, green, blue, alpha). Since the tga::writePNG() function expects a std::vector<uint8_t>, the first thing you need to do is convert your Pixel data into an array of uint8_t.

Image myImage;
std::vector<uint8_t> pngData;
for(const Pixel& pixel: myImage.data){
    uint8_t redByte = convertToByte(pixel.red);
    uint8_t greenByte = convertToByte(pixel.green);
    uint8_t blueByte = convertToByte(pixel.blue);
    uint8_t alphaByte = convertToByte(pixel.alpha);

    pngData.push_back(redByte);
    pngData.push_back(greenByte);
    pngData.push_back(blueByte);
    pngData.push_back(alphaByte);
}

If your Pixel doesn't have certain components (e.g. no alpha channel) then you would just omit the corresponding code. The only thing left is to determine the correct tga::Format for the function call. For that we need to know the number of components a pixel has, the size of a single component in bits and the type of our components. The size and type of the components is predetermined by the function as uint8_t is expected. uint8_thas a size of 8 bits and is of type unsigned integer. The number of components is determined by the format of your pixel. If your pixel only has a single value (eg. for red or for gray), the corresponding Format would be: tga::Format::r8_uint For two components it would be: tga::Format::r8g8_uint For three (rgb): tga::Format::r8g8b8_uint And for four (rgba): tga::Format::r8r8g8b8a8_uint

With the name you want to save your image as, you now know everything and have everything to call the function. Here is how you would do it for a Pixel with red, green, blue and alpha:

Image myImage;
std::vector<uint8_t> pngData;
/*Convert image data to vector of uint8_t*/
tga::writePNG("MyImage.png",myImage.width,myImage.height,tga::Format::r8g8b8a8_uint,pngData);

Specialized Example:

Suppose your image data is stored using a glm::vec3. The challenge here is to convert the float values to uint8_t values. For that you need to know the minimum and maximum value an individual color channel can have. If you know that, you can write a function like this:

#include "tga/tga_utils.hpp"
#include "tga/tga_math.hpp"
struct Image
{
    uint32_t width, heigth;
    std::vector<glm::vec3> data;
};
void saveImageAsPNG(Image const& myImage, glm::vec3 const& minPerChannel, glm::vec3 const& maxPerChannel, std::string const& imageName)
{
    std::vector<uint8_t> pngData;
    for(const glm::vec3& pixel: myImage.data){
        // Convert the float values to uint8_t
        glm::u8vec3 pixelAsBytes = glm::remap(pixel,minPerChannel,maxPerChannel,glm::vec3(0),glm::vec3(255));
        pngData.push_back(pixelAsBytes.r);
        pngData.push_back(pixelAsBytes.g);
        pngData.push_back(pixelAsBytes.b);
    }
    // Since we have red, green and blue, the format of the pixel also has 'r', 'g', and 'b' but no 'a'
    tga::writePNG(imageName,myImage.width,myImage.heigth,tga::Format::r8g8b8_uint,pngData);
}

Working with Images

General Ideas:

Suppose you have some image file that you want to work with in your code. The first step is to aquire the image data. For that, the functions tga::loadImage() and tga::loadHDRImage() can be used to load the raw image data. But the raw byte data isn't very convenient to work with. Since the interpretation of the image data is specific to the programs use-case, you will need to write needed functionality yourself. A structure that provides access to image data in a specific way is generally called a sampler. A general sampler could look like this:

class Sampler
{
public:
    Pixel sample(Coordinate);
    void set(Coordinate, Pixel)
private:
    tga::Image myImage;
};

A Pixel contains the information that should be extracted from the image. Coordinate contains values that specify the coordinates inside the image. The sample extracts the raw bytes from the image data and returns collected as a single pixel. The general sequence of events inside this function is like this:

Pixel sample(Coordinate coordinate)
{
    size_t indexOfPixelStart = pixelToByteCoordinates(coordinate);
    Pixel sampledPixel = defaultValuesForPixel;
    for(uint32_t i = 0; i < myImage.components; i++){
        size_t indexOfPixelComponent = indexOfPixelStart + i;
        uint8_t extractedByte = myImage.data[indexOfPixelComponent];
        sampledPixel.setComponent(i,extractedByte);
    }
    return sampledPixel;
}

The set functionality works analogous to the sample methode

void set(Coordinate coordinate, Pixel pixelValue)
{
    size_t indexOfPixelStart = pixelToByteCoordinates(coordinate);
    for(uint32_t i = 0; i < myImage.components; i++){
        size_t indexOfPixelComponent = indexOfPixelStart + i;
        myImage.data[indexOfPixelComponent] = pixelValue.getComponent(i);
    }
}

Specialized Example:

Suppose you need a sampler that extracts rgb values as a glm::vec3 from an 2D image. If requested coordinates are outside of the image dimensions, the should be wrapped around to make the image repeat itself. The functionality could be part of a sampler class or a standalone function. Here is how it would look as a standalone function

glm::vec3 sample(tga::Image const& myImage, uint32_t x, uint32_t y)
{
    // First, wrap the coordinates using modulo
    x = x % myImage.width;
    y = y % myImage.height;

    // Second, caluclate the Pixel Coordinate inside the data array
    // The size of a pixel in bytes is given with the 'components' member of tga::Image
    size_t pixelStart = (x + y * myImage.width) * myImage.components;

    // Third, initialize the return value with default values
    glm::vec3 extractedPixel = glm::vec3(0);

    // There is always have atleast one component inside a pixel
    extractedPixel.r = myImage.data[pixelStart];

    // When the image has only one component per pixel, it's a black and white image
    // Black and white images as rgb have the same value inside every component, so we duplicate the values
    if(myImage.components == 1)
        extractedPixel.b = extractedPixel.g = extractedPixel.r;

    // For 2 or more components, we set the green component
    if(myImage.components >= 2)
        extractedPixel.g = myImage.data[pixelStart + 1];

    // For 3 or more components, we set the blue component
    if(myImage.components >= 3)
        extractedPixel.b = myImage.data[pixelStart + 2];

    // We don't care about any alpha values so we are done
    return extractedPixel;
}

The implementation of a set() function is left as an exercise for the reader.