最近在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