实现OpenGL渲染器原理篇(七)——TGA文件的读取及编码

最近在Github上复现了一个渲染器render的项目:
Github链接:tiny render

我希望在博客上可以记录自己的学习过程,博客主要分为两大类:《原理篇》和《语法篇》。
原理篇则主要讲的是——实现渲染器过程中所需要的图形学渲染算法知识。
语法篇则主要讲的是——实现渲染器过程中使用到的C++的语法知识。


在作者的博客开始之前,就已经编写好了tgaimage.h/.cpp文件。那么我们今天就来解读一下tgaimage.h/.cpp两个文件的内容:

首先,像每个头文件一样,需要加入包含保护措施以防止文件被重复包含。实际操作则是在头文件的顶部加入几行代码:

#ifndef __TEXTURE_H__    // 看看此头文件是否已经被包含
 #define __TEXTURE_H__    // 如果没有,定义它

然后,在VS2019中,则会自动出现一行代码:

#endif      // __TEXTURE_H__ 结束包含保护

这三行代码防止此文件被重复包含。


然后,在这个头文件中,加入下面这行:

#pragma pack(push,1)
#pragma pack(pop)

这两行是用来指定数据在内存中的对齐方式的。

接下来,则是定义了3个结构struct。
第一个是class TGAImage,我们需要一块空间存储图像数据以及OpenGL生成纹理所需的类型。

class TGAImage
{
protected:
    //*** data为什么是char*类型的?***
    unsigned char* data;  // data是用来控制整个图像的颜色值
                          // 一般对于图像数据的读取,都使用 char* 类型

    int width;            // 整个图像的宽度
    int height;           // 整个图像的高度

    int bytespp;          // 控制单位像素的bit数

    bool   load_rle_data(std::ifstream& in); 
    bool unload_rle_data(std::ofstream& out);

public:
    enum Format 
    {
        GRAYSCALE = 1, RGB = 3, RGBA = 4
    };

    TGAImage();
    TGAImage(int w, int h, int bpp);
    TGAImage(const TGAImage& img);

    bool read_tga_file(const char* filename);
    bool write_tga_file(const char* filename, bool rle = true);

    bool flip_horizontally();
    bool flip_vertically();

    bool scale(int w, int h);

    TGAColor get(int x, int y);

    bool set(int x, int y, TGAColor c);

    ~TGAImage();

    TGAImage& operator =(const TGAImage& img);

    int get_width();
    int get_height();

    int get_bytespp();
    unsigned char* buffer();

    void clear();
};

第一个结构介绍完了,看看另外两个结构,他们将会在处理TGA文件的过程中使用。

第二个是TGA_Header,用来表示文件头,文件头用来决定文件类型。

接下来,则是声明结构中的一些实例,只有这样我们才可以在程序中使用它们。

我们看tgaimage.cpp文件中的TGAImage类的成员函数read_tga_file(),里面定义了TGAHeader:

TGA_Header header;    // 用来存储我们的文件头

然后还是在TGAImage类的成员函数load_rle_data()中定义了TGAColor:

TGAColor colorbuffer;  // 用来存储文件信息

读取文件一般有两种情况:读取一个未压缩的文件/读取一个压缩的文件。

好,我们来看看如何读取一个TGA文件:

bool TGAImage::read_tga_file(const char* filename) 

这个函数只有一个参数,参数是一个字符串,告诉计算机去哪里找我们的tga文件。

        std::ifstream in;
    in.open(filename, std::ios::binary);   // ios::binary是因为tga是个二进制文件

打开由“filename”参数指定的文件,它由函数的指针传递进去。以读模式打开文件。

接下来的几行检查指定的文件是否已经正确地打开。(如果此处有错误,返回false)

if (!in.is_open())
    {
        std::cerr << "can't open file " << filename << "\n";
        in.close();
        return false;
    }

下一步,我们尝试读取文件的(首12个字节)的内容并且将它们存储在我们的TGAHeader结构中,这样,我们得以检查文件类型。如果fread失败,则关闭文件,显示一个错误,并且函数返回false。

    TGA_Header header; 
    in.read((char*)&header, sizeof(header)); // header是对象,char*&类型的,后面的则是尺度大小
                                             // 这个header是char*&类型,下面data是char*类型
                                             // 也许是因为header是个通常的类变量,data是指针变量

    if (!in.good())  // 关于good: https://www.jianshu.com/p/e9fdc4cd3e0f 和 https://stackoverflow.com/questions/41926303/difference-between-ifstream-good-and-boolifstream
    {
        in.close();
        std::cerr << "an error occured while reading the header\n";
        return false;
    }

接着,通过我们编的程序刚读取的头,我们继续尝试确定文件类型。
这可以告诉我们它是压缩的、未压缩甚至是错误的文件类型。

    unsigned long nbytes = bytespp * width * height;

    data = new unsigned char[nbytes];

    if (3 == header.datatypecode || 2 == header.datatypecode) // 3 2?
    {
        in.read((char*)data, nbytes); // data是要读取的对象(一般都是char*类型的),nbytes就是尺度大小
        
        if (!in.good()) 
        {
            in.close();
            std::cerr << "an error occured while reading the data\n";
            return false;
        }
    }

    else if (10 == header.datatypecode || 11 == header.datatypecode)  // 10 11?
    {
        if (!load_rle_data(in)) 
        {
            in.close();

            std::cerr << "an error occured while reading the data\n";
            return false;
        }
    }

    else 
    {
        in.close();

        std::cerr << "unknown file format " << (int)header.datatypecode << "\n";
        return false;
    }

现在我们有了计算图像的高度、宽度和BPP的全部信息。我们在纹理和本地结构中都将存储它。

    width = header.width;    // 计算高度
    height = header.height;  // 计算宽度
    bytespp = header.bitsperpixel >> 3;  // 计算BPP

现在,我们需要确认高度和宽度至少为1个像素,并且bpp是24或32。如果这些值中的任何一个超出了它们的界限,我们将再一次显示一个错误,关闭文件,并且离开此函数。

if (width <= 0 || height <= 0 || 
        (bytespp != GRAYSCALE && bytespp != RGB && bytespp != RGBA))   
    {
        in.close();
        std::cerr << "bad bpp (or width/height) value\n";                 
        return false;
    }

接下来我们设置图像的类型。24 bit图像是GL_RGB,32 bit 图像是GL_RGBA。

    if (!(header.imagedescriptor & 0x20)) 
    {
        flip_vertically();
    }

    if (header.imagedescriptor & 0x10)  
    {
        flip_horizontally();
    }

我们需要一些空间去存储整个图像数据,因此我们将要使用new分配正确的内存数量,然后我们确认内存已经分配,并且它不是NULL。如果出现了错误,则运行错误处理代码。

    unsigned long nbytes = bytespp * width * height;
    data = new unsigned char[nbytes];

使用new来分配内存。

接下来,这里我们尝试读取所有的图像数据。如果不能,我们将再次触发错误处理代码。

if (!data) 
        return false;

TGA文件用逆OpenGL需求顺序的方式存储图像,因此我们必须将格式从BGR到RGB。

以上是读取未压缩型TGA文件的方法。读取RLE压缩型文件的步骤稍微难一点。我们像平时一样读取文件头并且收集高度/宽度/色彩深度,这和读取未压缩版本是一致的。


前面的基本步骤都一样,我们需要决定组成图像的像素数
我们将它存储在变量“pixelcount”中。
我们也需要存储当前所处的像素,以及我们正在写入的图像数据的字节,这样避免溢出写入过多的旧数据。

我们将要分配足够的内存来存储一个像素

bool TGAImage::load_rle_data(std::ifstream& in) 
{
    unsigned long pixelcount = width * height;  // 图像中的像素数
    unsigned long currentpixel = 0;             // 当前正在读取的像素
    unsigned long currentbyte = 0;              // 当前正在向图像中写入的像素

    TGAColor colorbuffer;                       // 一个像素的存储空间
        ...
}

接下来我们将要进行一个大循环。让我们将它分解为更多可管理的块

首先我们声明一个变量来存储“块”头
块头指示接下来的段是RLE还是RAW,它的长度是多少。
如果一字节头小于等于127,则它是一个RAW头

头的值是颜色数,是负数。

在我们处理其它头字节之前,我们先读取它并且拷贝到内存中。
这样我们将我们得到的值加1,然后读取大量像素并且将它们拷贝到ImageData中,就像我们处理未压缩型图像一样(代码好像没体现?)。

如果头大于127,那么它是下一个像素值随后将要重复的次数
要获取实际重复的数量,我们将它减去127以除去1bit的的头标示符

然后我们读取下一个像素并且依照上述次数连续拷贝它到内存中

if (chunkheader < 128)  // 如果是RAW块
        {
            chunkheader++;      // 变量值加1以获取RAW像素的总数

            //--开始像素读取循环--
            for (int i = 0; i < chunkheader; i++)
            {
                //--尝试读取一个像素--
                in.read((char*)colorbuffer.raw, bytespp);

                if (!in.good()) 
                {
                    std::cerr << "an error occured while reading the header\n";

                    //--如果失败,返回false--
                    return false;
                }

                for (int t = 0; t < bytespp; t++)
                    data[currentbyte++] = colorbuffer.raw[t];

                currentpixel++;

                if (currentpixel > pixelcount) 
                {
                    std::cerr << "Too many pixels read\n";
                    return false;
                }
            }
        }

        else 
        {
            chunkheader -= 127;

            in.read((char*)colorbuffer.raw, bytespp);

            if (!in.good()) 
            {
                std::cerr << "an error occured while reading the header\n";
                return false;
            }

            for (int i = 0; i < chunkheader; i++) 
            {
                for (int t = 0; t < bytespp; t++)
                    data[currentbyte++] = colorbuffer.raw[t];

                currentpixel++;

                if (currentpixel > pixelcount) 
                {
                    std::cerr << "Too many pixels read\n";
                    return false;
                }
            }
        }
//--开始循环--
    do                                         
    {
        //--存储Id块值的变量--
        unsigned char chunkheader = 0;
        chunkheader = in.get();   // get()针对二进制文件的读写:http://c.biancheng.net/cpp/biancheng/view/2231.html 和 http://c.biancheng.net/view/1534.html
        
        //--尝试读取块的头--
        if (!in.good()) 
        {
            std::cerr << "an error occured while reading the data\n";
            return false;
        }

接下来我们将要看看它是否是RAW头。如果是,我们需要将此变量的值加1以获取紧随头之后的像素总数

if (chunkheader < 128)
        {
            chunkheader++;

我们开启另一个循环读取所有的颜色信息。它将会循环块头中指定的次数,并且每次循环读取和存储一个像素

首先,我们读取并检验像素数据
单个像素的数据将被存储在colorbuffer变量中。
然后我们将检查它是否为RAW头。
如果是,我们需要添加一个到变量之中以获取头之后的像素总数

//--开始像素读取循环--
            for (int i = 0; i < chunkheader; i++)
            {
                //--尝试读取一个像素--
                in.read((char*)colorbuffer.raw, bytespp);

                if (!in.good()) 
                {
                    std::cerr << "an error occured while reading the header\n";

                    //--如果失败,返回false--
                    return false;
                }

我们循环中的下一步将要获取存储在colorbuffer中的颜色值,并且将其写入稍后将要使用的imageData变量中。

在这个过程中,数据格式将会由BGR翻转为RGB或由BGRA转换为RGBA,具体情况取决于每像素的比特数

当我们完成任务后我们增加当前的字节和当前的像素计数器

for (int i = 0; i < chunkheader; i++) // 开启一个循环读取所有的颜色信息
                                                  // 它将会循环块头中指定的次数
                                                  // 并且每次循环读取和存储一个像素
            {
                for (int t = 0; t < bytespp; t++)
                    data[currentbyte++] = colorbuffer.raw[t];

                currentpixel++;

                if (currentpixel > pixelcount) 
                {
                    std::cerr << "Too many pixels read\n";
                    return false;
                }
            }

下一段处理描述RLE段的“块”头。首先我们将chunkheader减去127来得到获取下一个颜色重复的次数。

else      // 如果是RLE头
                  // 如果头大于127,那么它是下一个像素值随后将要重复的次数。
        {
            chunkheader -= 127; 

然后我们尝试读取下一个颜色值。

//---读取下一个像素---
            in.read((char*)colorbuffer.raw, bytespp);

            if (!in.good()) 
            {
                std::cerr << "an error occured while reading the header\n";
                return false;
            }

接下来,我们开始循环拷贝我们多次读到内存中的像素,这由RLE头中的值规定。

然后,我们将颜色值拷贝到图像数据中预处理R和B的值交换

随后,我们增加当前的字节数、当前像素,这样我们再次写入值时可以处在正确的位置。

            for (int i = 0; i < chunkheader; i++) 
               {
                for (int t = 0; t < bytespp; t++)
                    data[currentbyte++] = colorbuffer.raw[t];

                currentpixel++;

只要仍剩有像素要读取,我们将会继续主循环。最后,我们关闭文件并返回成功。

if (currentpixel > pixelcount) 
                {
                    std::cerr << "Too many pixels read\n";
                    return false;
                }
            }
        }
    } while (currentpixel < pixelcount);

    return true;
```chun

你可能感兴趣的:(实现OpenGL渲染器原理篇(七)——TGA文件的读取及编码)