本文是对GitHub上的公开项目tinyrenderer的注解,如有侵权等问题,请联系我,我马上删。
一、vec --向量的代码搭建
原文将vec向量的搭建存放在了"geometry.h"头文件中。
1、基础搭建
利用template模板搭建了vec类,template的参数n为vec的维数。
template<int n> struct vec {
double data[n] = {0};
double& operator[](const int i) { assert(i>=0 && i<n); return data[i]; }
double operator[](const int i) const { assert(i>=0 && i<n); return data[i]; }
double norm2() const { return *this * *this; }
double norm() const { return std::sqrt(norm2()); }
};
data用于存储vec各维度的值。
利用operate定义了[]在vec的重载,其中利用assert断言检查,避免了i超过数组边界的情况。
在第二个定义的函数前加const,为了保证地址不被修改。
norm,norm2分别是向量的长度和长度的平方。其中用到了重载后的*(向量的点乘)将在下面介绍。
2、操作符的重载
1)向量*向量
template<int n> double operator*(const vec<n>& lhs, const vec<n>& rhs) {
double ret = 0;
for (int i=n; i--; ret+=lhs[i]*rhs[i]);
return ret;
}
向量各项相乘后相加,结果是数,返回值是double类型。
2)向量+向量|向量-向量
template<int n> vec<n> operator+(const vec<n>& lhs, const vec<n>& rhs) {
vec<n> ret = lhs;
for (int i=n; i--; ret[i]+=rhs[i]);
return ret;
}
template<int n> vec<n> operator-(const vec<n>& lhs, const vec<n>& rhs) {
vec<n> ret = lhs;
for (int i=n; i--; ret[i]-=rhs[i]);
return ret;
}
向量各项相加减,结果是向量。
3)数*向量|向量*数|向量/数
template<int n> vec<n> operator*(const double& rhs, const vec<n> &lhs) {
vec<n> ret = lhs;
for (int i=n; i--; ret[i]*=rhs);
return ret;
}
template<int n> vec<n> operator*(const vec<n>& lhs, const double& rhs) {
vec<n> ret = lhs;
for (int i=n; i--; ret[i]*=rhs);
return ret;
}
template<int n> vec<n> operator/(const vec<n>& lhs, const double& rhs) {
vec<n> ret = lhs;
for (int i=n; i--; ret[i]/=rhs);
return ret;
}
向量各项均乘/除以某个数,结果为向量。
4)<<的重载
template<int n> std::ostream& operator<<(std::ostream& out, const vec<n>& v) {
for (int i=0; i<n; i++) out << v[i] << " ";
return out;
}
将向量转化为字符串。
3、embed|proj操作 -向量嵌入
1)embed
template<int n1,int n2> vec<n1> embed(const vec<n2> &v, double fill=1) {
vec<n1> ret;
for (int i=n1; i--; ret[i]=(i<n2?v[i]:fill));
return ret;
}
将vec<n2>嵌入vec<n1>,多余位数用fill(默认为1)代替。
2)proj
template<int n1,int n2> vec<n1> proj(const vec<n2> &v) {
vec<n1> ret;
for (int i=n1; i--; ret[i]=v[i]);
return ret;
}
将vec<n2>复制给vec<n1>,位数为vec<n1>和vec<n2>中位数小的那个。
4、vec2、vec3具体定义
根据上述vec模板具体定义vec2,vec3。
1)基础定义
template<> struct vec<2> {
double x = 0, y = 0;
double& operator[](const int i) { assert(i>=0 && i<2); return i ? y : x; }
double operator[](const int i) const { assert(i>=0 && i<2); return i ? y : x; }
double norm2() const { return *this * *this; }
double norm() const { return std::sqrt(norm2()); }
vec<2> normalized() { return (*this)/norm(); }
};
template<> struct vec<3> {
double x = 0, y = 0, z = 0;
double& operator[](const int i) { assert(i>=0 && i<3); return i ? (1==i ? y : z) : x; }
double operator[](const int i) const { assert(i>=0 && i<3); return i ? (1==i ? y : z) : x; }
double norm2() const { return *this * *this; }
double norm() const { return std::sqrt(norm2()); }
vec<3> normalized() { return (*this)/norm(); }
};
typedef vec<2> vec2;
typedef vec<3> vec3;
typedef vec<4> vec4;
增加了normalize操作,提供了将向量转化为标准向量(长度为1)的简便方法。
定义了vec4,但未具体定义。
2)vec3的叉乘
原文未在"geometry.h"中具体定义。
vec3 cross(const vec3 &v1, const vec3 &v2);
在cpp中有具体实现
vec3 cross(const vec3 &v1, const vec3 &v2) {
return vec<3>{v1.y*v2.z - v1.z*v2.y, v1.z*v2.x - v1.x*v2.z, v1.x*v2.y - v1.y*v2.x};
}
二、MAT(“矩阵”)搭建
1、矩阵行列式的计算方法
1)普通n阶矩阵的行列式计算
template<int n> struct dt {
static double det(const mat<n,n>& src) {
double ret = 0;
for (int i=n; i--; ret += src[0][i]*src.cofactor(0,i));
return ret;
}
};
利用第一行的的代数余子式展开进行计算,cofactor方法返回的是对应点的代数余子式会在矩阵的搭建中介绍。 (类似递归)
2)一阶矩阵的行列式计算
template<> struct dt<1> {
static double det(const mat<1,1>& src) {
return src[0][0];
}
};
直接返回该值。
2、创建空矩阵及相关基础函数搭建
1)利用vec类型创建空矩阵
vec<ncols> rows[nrows] = {{}};
2)提供引用的重载
vec<ncols>& operator[] (const int idx) { assert(idx>=0 && idx<nrows); return rows[idx]; }
const vec<ncols>& operator[] (const int idx) const { assert(idx>=0 && idx<nrows); return rows[idx]; }
通过索引调用矩阵的某一列,返回vec类型。
3)列的调用
vec<nrows> col(const int idx) const {
assert(idx>=0 && idx<ncols);
vec<nrows> ret;
for (int i=nrows; i--; ret[i]=rows[i][idx]);
return ret;
}
通过[]的重载调用将一列的值赋给一个新的vec类型,并返回这个vec。
4)列的输入
void set_col(const int idx, const vec<nrows> &v) {
assert(idx>=0 && idx<ncols);
for (int i=nrows; i--; rows[i][idx]=v[i]);
}
将输入的vec类数据列,添加入矩阵中
5)单位矩阵
static mat<nrows,ncols> identity() {
mat<nrows,ncols> ret;
for (int i=nrows; i--; )
for (int j=ncols;j--; ret[i][j]=(i==j));
return ret;
}
返回一个指定大小的单位矩阵。
具体逻辑:在行数和列数相等时,该值设为1。
6)提供行列式的计算方法
double det() const {
return dt<ncols>::det(*this);
}
在dt中调用计算方法。
7)取子矩阵
mat<nrows-1,ncols-1> get_minor(const int row, const int col) const {
mat<nrows-1,ncols-1> ret;
for (int i=nrows-1; i--; )
for (int j=ncols-1;j--; ret[i][j]=rows[i<row?i:i+1][j<col?j:j+1]);
return ret;
}
这里只提供了把所求位置所在的第i行和第j列划去后,留下来的n-1阶子矩阵。为了后面方便计算代数余子式
8)求代数余子式
double cofactor(const int row, const int col) const {
return get_minor(row,col).det()*((row+col)%2 ? -1 : 1);
}
利用前面求子矩阵的方法,求出对应子矩阵后,利用递归的思想调用det求矩阵行列式的方法,计算出余子式的值,然后通过行列判断其正负。
9)求伴随矩阵
mat<nrows,ncols> adjugate() const {
mat<nrows,ncols> ret;
for (int i=nrows; i--; )
for (int j=ncols; j--; ret[i][j]=cofactor(i,j));
return ret;
}
利用上述求代数余子式的方法,求该矩阵对应的伴随矩阵。
10)求逆矩阵
mat<nrows,ncols> invert_transpose() const {
mat<nrows,ncols> ret = adjugate();
return ret/(ret[0]*rows[0]);
}
根据公式:逆矩阵 = 伴随矩阵/矩阵的行列式,这里的ret[0]是伴随矩阵的第一行也就是原矩阵的一行各元素对应的各代数余子式,所以这两行相乘就是原矩阵的行列式的值。
11)矩阵转置
mat<ncols,nrows> transpose() const {
mat<ncols,nrows> ret;
for (int i=ncols; i--; ret[i]=this->col(i));
return ret;
}
直接将原矩阵的列赋值给新矩阵的列,返回新矩阵。
为了便于后面使用,这里提供了原矩阵的逆转置矩阵(将逆矩阵转置)。
mat<nrows,ncols> invert() const {
return invert_transpose().transpose();
}
12)<<的重载
template<int nrows,int ncols> std::ostream& operator<<(std::ostream& out, const mat<nrows,ncols>& m) {
for (int i=0; i<nrows; i++) out << m[i] << std::endl;
return out;
}
整体的创建代码如下:
template<int n> struct dt;
template<int nrows,int ncols> struct mat {
vec<ncols> rows[nrows] = {{}};
vec<ncols>& operator[] (const int idx) { assert(idx>=0 && idx<nrows); return rows[idx]; }
const vec<ncols>& operator[] (const int idx) const { assert(idx>=0 && idx<nrows); return rows[idx]; }
vec<nrows> col(const int idx) const {
assert(idx>=0 && idx<ncols);
vec<nrows> ret;
for (int i=nrows; i--; ret[i]=rows[i][idx]);
return ret;
}
void set_col(const int idx, const vec<nrows> &v) {
assert(idx>=0 && idx<ncols);
for (int i=nrows; i--; rows[i][idx]=v[i]);
}
static mat<nrows,ncols> identity() {
mat<nrows,ncols> ret;
for (int i=nrows; i--; )
for (int j=ncols;j--; ret[i][j]=(i==j));
return ret;
}
double det() const {
return dt<ncols>::det(*this);
}
mat<nrows-1,ncols-1> get_minor(const int row, const int col) const {
mat<nrows-1,ncols-1> ret;
for (int i=nrows-1; i--; )
for (int j=ncols-1;j--; ret[i][j]=rows[i<row?i:i+1][j<col?j:j+1]);
return ret;
}
double cofactor(const int row, const int col) const {
return get_minor(row,col).det()*((row+col)%2 ? -1 : 1);
}
mat<nrows,ncols> adjugate() const {
mat<nrows,ncols> ret;
for (int i=nrows; i--; )
for (int j=ncols; j--; ret[i][j]=cofactor(i,j));
return ret;
}
mat<nrows,ncols> invert_transpose() const {
mat<nrows,ncols> ret = adjugate();
return ret/(ret[0]*rows[0]);
}
mat<nrows,ncols> invert() const {
return invert_transpose().transpose();
}
mat<ncols,nrows> transpose() const {
mat<ncols,nrows> ret;
for (int i=ncols; i--; ret[i]=this->col(i));
return ret;
}
};
template<int nrows,int ncols> std::ostream& operator<<(std::ostream& out, const mat<nrows,ncols>& m) {
for (int i=0; i<nrows; i++) out << m[i] << std::endl;
return out;
}
template<int n> struct dt {
static double det(const mat<n,n>& src) {
double ret = 0;
for (int i=n; i--; ret += src[0][i]*src.cofactor(0,i));
return ret;
}
};
template<> struct dt<1> {
static double det(const mat<1,1>& src) {
return src[0][0];
}
};
3、矩阵的计算
1)矩阵*向量
在图形学中可以看作,某个向量经过某个矩阵变化后所得的新向量。
template<int nrows,int ncols> vec<nrows> operator*(const mat<nrows,ncols>& lhs, const vec<ncols>& rhs) {
vec<nrows> ret;
for (int i=nrows; i--; ret[i]=lhs[i]*rhs);
return ret;
}
通过矩阵对应行和原向量的向量的点乘,计算出新向量。
2)矩阵*矩阵
根据运算法则,只允许m*n的矩阵和n*l的矩阵相乘,返回一个m*l的新矩阵。
template<int R1,int C1,int C2>mat<R1,C2> operator*(const mat<R1,C1>& lhs, const mat<C1,C2>& rhs) {
mat<R1,C2> result;
for (int i=R1; i--; )
for (int j=C2; j--; result[i][j]=lhs[i]*rhs.col(j));
return result;
}
将对应矩阵的对应行/列进行向量的点乘计算,算出新矩阵。
3)数*矩阵|矩阵/数
template<int nrows,int ncols>mat<nrows,ncols> operator*(const mat<nrows,ncols>& lhs, const double& val) {
mat<nrows,ncols> result;
for (int i=nrows; i--; result[i] = lhs[i]*val);
return result;
}
template<int nrows,int ncols>mat<nrows,ncols> operator/(const mat<nrows,ncols>& lhs, const double& val) {
mat<nrows,ncols> result;
for (int i=nrows; i--; result[i] = lhs[i]/val);
return result;
}
利用数与矩阵中每行向量做点乘|除实现。
4)矩阵+矩阵|矩阵-矩阵
根据运算法则行列数相同的矩阵才能相加减
template<int nrows,int ncols>mat<nrows,ncols> operator+(const mat<nrows,ncols>& lhs, const mat<nrows,ncols>& rhs) {
mat<nrows,ncols> result;
for (int i=nrows; i--; )
for (int j=ncols; j--; result[i][j]=lhs[i][j]+rhs[i][j]);
return result;
}
template<int nrows,int ncols>mat<nrows,ncols> operator-(const mat<nrows,ncols>& lhs, const mat<nrows,ncols>& rhs) {
mat<nrows,ncols> result;
for (int i=nrows; i--; )
for (int j=ncols; j--; result[i][j]=lhs[i][j]-rhs[i][j]);
return result;
}
两矩阵对应元素相加减即可。
三、TGA图片
用于成像的画布
1、TGAHeader
struct TGAHeader {
std::uint8_t idlength = 0;
std::uint8_t colormaptype = 0;
std::uint8_t datatypecode = 0;
std::uint16_t colormaporigin = 0;
std::uint16_t colormaplength = 0;
std::uint8_t colormapdepth = 0;
std::uint16_t x_origin = 0;
std::uint16_t y_origin = 0;
std::uint16_t width = 0;
std::uint16_t height = 0;
std::uint8_t bitsperpixel = 0;
std::uint8_t imagedescriptor = 0;
};
用于存储一些基本信息
idlength
: 图像标识符的长度。通常用于存储图像的附加信息,长度为字节数。colormaptype
: 指示是否使用颜色映射表。0
表示没有颜色映射表,1
表示有颜色映射表。datatypecode
: 图像数据的类型编码。它指示了图像数据的格式,比如2
代表未压缩的 RGB 图像,3
代表灰度图像,10
代表 RLE 压缩的 RGB 图像等。colormaporigin
: 颜色映射表的起始位置。通常用于定义颜色映射表的开始位置。colormaplength
: 颜色映射表中条目的数量。如果colormaptype
为0
,此值为0
。colormapdepth
: 颜色映射表条目的位深度,通常为24
或32
位。x_origin
: 图像左上角的 X 坐标。y_origin
: 图像左上角的 Y 坐标。width
: 图像的宽度(像素)。height
: 图像的高度(像素)。bitsperpixel
: 每个像素的位数,表示图像的颜色深度(如24
表示 RGB 图像)。imagedescriptor
: 描述图像属性的字节,如图像的压缩类型、方向等。
2、TGAColor
struct TGAColor {
std::uint8_t bgra[4] = {0,0,0,0};
std::uint8_t bytespp = 4;
std::uint8_t& operator[](const int i) { return bgra[i]; }
};
存储一些关于颜色的信息。
3、TGAImage
1)格式(枚举类型)
enum Format { GRAYSCALE=1, RGB=3, RGBA=4 };
grayscale:表示灰度图像,一个像素一个值占一个字节
rgb:真彩图像,一个像素三个值,占三个字节
rgba:真彩+透明,四个值,多一个透明度,占四个字节
2)构造函数
TGAImage() = default;
TGAImage(const int w, const int h, const int bpp);
默认构造函数不进行任何初始化。
参数化构造函数:初始化长、宽、每像素字节数
3)一些成员函数
bool read_tga_file(const std::string filename);
bool write_tga_file(const std::string filename, const bool vflip=true, const bool rle=true) const;
void flip_horizontally();
void flip_vertically();
TGAColor get(const int x, const int y) const;
void set(const int x, const int y, const TGAColor &c);
int width() const;
int height() const;
private:
bool load_rle_data(std::ifstream &in);
bool unload_rle_data(std::ofstream &out) const;
提供了该类型可以使用的一些函数会在后面详细介绍
4)成员变量
int w = 0;
int h = 0;
std::uint8_t bpp = 0;
std::vector<std::uint8_t> data = {};
分别是长、宽、每像素占的字节数和图像初始的数据(每个像素的值)用vec类型表示。
4、成员函数详细解释
1)读取图片
分为两步:先打开文件,然后初始化图片。
打开文件:
std::ifstream in;
in.open(filename, std::ios::binary);
if (!in.is_open()) {
std::cerr << "can't open file " << filename << "\n";
return false;
}
初始化:
TGAHeader header;
in.read(reinterpret_cast<char *>(&header), sizeof(header));
if (!in.good()) {
std::cerr << "an error occured while reading the header\n";
return false;
}
将图片中的基础数据,存入TGAHeader类型中
w = header.width;
h = header.height;
bpp = header.bitsperpixel>>3;
if (w<=0 || h<=0 || (bpp!=GRAYSCALE && bpp!=RGB && bpp!=RGBA)) {
std::cerr << "bad bpp (or width/height) value\n";
return false;
}
将宽、高每像素占的字节数赋值给TGAImage中的变量并检验数据正确性
size_t nbytes = bpp*w*h;
data = std::vector<std::uint8_t>(nbytes, 0);
nbytes存储一共需要的字节数,每一个值都是256的颜色,给data赋值一个256位向量,大小是nbytes,初值为0。
if (3==header.datatypecode || 2==header.datatypecode) {
in.read(reinterpret_cast<char *>(data.data()), nbytes);
if (!in.good()) {
std::cerr << "an error occured while reading the data\n";
return false;
}
} else if (10==header.datatypecode||11==header.datatypecode) {
if (!load_rle_data(in)) {
std::cerr << "an error occured while reading the data\n";
return false;
}
} else {
std::cerr << "unknown file format " << (int)header.datatypecode << "\n";
return false;
}
这里在根据header中的datatypecode变量处理数据。
2
代表未压缩的 RGB 图像,3
代表灰度图像,10
代表 RLE 压缩的 RGB 图像,11
表示使用 RLE压缩的灰度图像。
在未压缩的情况下,直接将图片中的数据导入data中。
在RLE压缩的情况下,调用load_rle_data函数读取data中的数据。
在读取出错或者datatypecode并不是期望数值时,报错。
if (!(header.imagedescriptor & 0x20))
flip_vertically();
if (header.imagedescriptor & 0x10)
flip_horizontally();
imagedescriptor
主要包括:
- 图像的水平翻转:标记图像是否从左到右翻转。
- 图像的垂直翻转:标记图像是否从上到下翻转。
- 图像的存储方式:标识图像是否是从左下角开始(即原点在左下)还是其他方式。
imagedescriptor
字段的值被分解为多个比特位,通常为一个字节,其中包括翻转标志和图像的方向信息。
通过二进制掩码进行判断,图像是否需要垂直或者水平翻转,利用水平或者垂直翻转函数(具体会在后面介绍)执行。
std::cerr << w << "x" << h << "/" << bpp*8 << "\n";
输出信息用于调试
2)载入rle压缩的图片
bool TGAImage::load_rle_data(std::ifstream &in) {
size_t pixelcount = w*h;
size_t currentpixel = 0;
size_t currentbyte = 0;
TGAColor colorbuffer;
do {
std::uint8_t chunkheader = 0;
chunkheader = in.get();
if (!in.good()) {
std::cerr << "an error occured while reading the data\n";
return false;
}
if (chunkheader<128) {
chunkheader++;
for (int i=0; i<chunkheader; i++) {
in.read(reinterpret_cast<char *>(colorbuffer.bgra), bpp);
if (!in.good()) {
std::cerr << "an error occured while reading the header\n";
return false;
}
for (int t=0; t<bpp; t++)
data[currentbyte++] = colorbuffer.bgra[t];
currentpixel++;
if (currentpixel>pixelcount) {
std::cerr << "Too many pixels read\n";
return false;
}
}
} else {
chunkheader -= 127;
in.read(reinterpret_cast<char *>(colorbuffer.bgra), bpp);
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<bpp; t++)
data[currentbyte++] = colorbuffer.bgra[t];
currentpixel++;
if (currentpixel>pixelcount) {
std::cerr << "Too many pixels read\n";
return false;
}
}
}
} while (currentpixel < pixelcount);
return true;
}
chunkheader
是一个用于指示RLE数据块类型的变量
小于128: 表示一个“非压缩块”,这个块包含了实际的颜色数据,chunkheader
的值加1表示该块中有多少个颜色值(像素)。这些颜色值会被逐个读取并存储到data
数组中。
大于或等于128: 表示一个“压缩块”,chunkheader
的值减去127表示该块中的颜色值将被重复的次数。该块在文件中只包含一个颜色值,但实际表示多个相同的颜色像素。程序会读取这个颜色值,并将其重复存储到data
数组中,以符合chunkheader
指定的重复次数。
逐压缩快读取图片数据
先读取chunkheader,判断大小分成两种情况
chunkheader<128
chunkheader+1是该压缩快中像素的个数。
逐像素读取每个压缩快,在每个像素中按bpp的大小逐字节读取每个颜色的数据放入data中。
chunkheader>=128
chunkheader-127是该压缩快中像素的个数
只需要读取一次颜色数据
根据chunkheader-127的个数,将同一个颜色数据按未压缩的同样方法存储到datat中。
3)写TGA文件
初始化
bool TGAImage::write_tga_file(const std::string filename, const bool vflip, const bool rle) const {
constexpr std::uint8_t developer_area_ref[4] = {0, 0, 0, 0};
constexpr std::uint8_t extension_area_ref[4] = {0, 0, 0, 0};
constexpr std::uint8_t footer[18] = {'T','R','U','E','V','I','S','I','O','N','-','X','F','I','L','E','.','\0'};
filename
:要保存的文件名。
vflip
:是否垂直翻转图像。
rle
:是否使用RLE压缩。
developer_area_ref
和 extension_area_ref分别代表
开发者区域和扩展区域,用于存放额外的关于图片的信息,默认初始化为0。
footer是文件尾部标识符,用于标识TGA格式。
打开文件
std::ofstream out;
out.open(filename, std::ios::binary);
if (!out.is_open()) {
std::cerr << "can't open file " << filename << "\n";
return false;
}
打开并检验是否正常打开
写TGA文件头
TGAHeader header = {};
header.bitsperpixel = bpp<<3;
header.width = w;
header.height = h;
header.datatypecode = (bpp==GRAYSCALE?(rle?11:3):(rle?10:2));
header.imagedescriptor = vflip ? 0x00 : 0x20; // top-left or bottom-left origin
out.write(reinterpret_cast<const char *>(&header), sizeof(header));
if (!out.good()) {
std::cerr << "can't dump the tga file\n";
return false;
}
bpp的单位是字节,bitsperpixel希望存储以bit为单位的数据,所以将bpp左移三位(相当于*8)赋值给bitsperpixel。
通过是否为灰度和是否使用rle压缩两个条件,给datatypecode赋相应的值
imagedescriptor存储是否需要竖直翻转
写入文件
if (!rle) {
out.write(reinterpret_cast<const char *>(data.data()), w*h*bpp);
if (!out.good()) {
std::cerr << "can't unload raw data\n";
return false;
}
} else if (!unload_rle_data(out)) {
std::cerr << "can't unload rle data\n";
return false;
}
如果不压缩直接写入data中的数据
如果压缩需要用unload_rle_data写入数据
out.write(reinterpret_cast<const char *>(developer_area_ref), sizeof(developer_area_ref));
if (!out.good()) {
std::cerr << "can't dump the tga file\n";
return false;
}
out.write(reinterpret_cast<const char *>(extension_area_ref), sizeof(extension_area_ref));
if (!out.good()) {
std::cerr << "can't dump the tga file\n";
return false;
}
out.write(reinterpret_cast<const char *>(footer), sizeof(footer));
if (!out.good()) {
std::cerr << "can't dump the tga file\n";
return false;
}
写入developer_area_ref
和 extension_area_ref和footer。
4)将普通图片压缩为rle压缩图片
bool TGAImage::unload_rle_data(std::ofstream &out) const {
const std::uint8_t max_chunk_length = 128;
size_t npixels = w*h;
size_t curpix = 0;
while (curpix<npixels) {
size_t chunkstart = curpix*bpp;
size_t curbyte = curpix*bpp;
std::uint8_t run_length = 1;
bool raw = true;
while (curpix+run_length<npixels && run_length<max_chunk_length)
{
bool succ_eq = true;//像素颜色是否相同
for (int t=0; succ_eq && t<bpp; t++)
succ_eq = (data[curbyte+t]==data[curbyte+t+bpp]);//检验当前像素与下一像素是否相同颜色
curbyte += bpp;//下以像素的字节起点
if (1==run_length)
raw = !succ_eq;
//确定raw,表示的是是否是原始块
if (raw && succ_eq) {
run_length--;
//如果是原始块,run_length-1放入对应位置,而且马上要开始压缩了
break;
}
if (!raw && !succ_eq)
//压缩块的部分结束了
break;
run_length++;
}
curpix += run_length;
//根据raw重新调整run_length的值
out.put(raw?run_length-1:run_length+127);
if (!out.good()) {
std::cerr << "can't dump the tga file\n";
return false;
}
//在对应的位置存入对应数据
out.write(reinterpret_cast<const char *>(data.data()+chunkstart), (raw?run_length*bpp:bpp));
if (!out.good()) {
std::cerr << "can't dump the tga file\n";
return false;
}
}
return true;
}
逐像素,逐字节将普通图片进行压缩。
5)一些其他简单函数
获取像素颜色
TGAColor TGAImage::get(const int x, const int y) const {
if (!data.size() || x<0 || y<0 || x>=w || y>=h)
return {};
TGAColor ret = {0, 0, 0, 0, bpp};
const std::uint8_t *p = data.data()+(x+y*w)*bpp;
for (int i=bpp; i--; ret.bgra[i] = p[i]);
return ret;
}
设置像素颜色
void TGAImage::set(int x, int y, const TGAColor &c) {
if (!data.size() || x<0 || y<0 || x>=w || y>=h) return;
memcpy(data.data()+(x+y*w)*bpp, c.bgra, bpp);
}
水平/竖直翻转
void TGAImage::flip_horizontally() {
int half = w>>1;
for (int i=0; i<half; i++)
for (int j=0; j<h; j++)
for (int b=0; b<bpp; b++)
std::swap(data[(i+j*w)*bpp+b], data[(w-1-i+j*w)*bpp+b]);
}
void TGAImage::flip_vertically() {
int half = h>>1;
for (int i=0; i<w; i++)
for (int j=0; j<half; j++)
for (int b=0; b<bpp; b++)
std::swap(data[(i+j*w)*bpp+b], data[(i+(h-1-j)*w)*bpp+b]);
}
获取宽/高
int TGAImage::width() const {
return w;
}
int TGAImage::height() const {
return h;
}
四、Model三维模型
1、model类型搭建
class Model {
std::vector<vec3> verts{}; // array of vertices
std::vector<vec2> tex_coord{}; // per-vertex array of tex coords
std::vector<vec3> norms{}; // per-vertex array of normal vectors
std::vector<int> facet_vrt{};
std::vector<int> facet_tex{}; // per-triangle indices in the above arrays
std::vector<int> facet_nrm{};
TGAImage diffusemap{}; // diffuse color texture
TGAImage normalmap{}; // normal map texture
TGAImage specularmap{}; // specular map texture
void load_texture(const std::string filename, const std::string suffix, TGAImage &img);
public:
Model(const std::string filename);
int nverts() const;
int nfaces() const;
vec3 normal(const int iface, const int nthvert) const; // per triangle corner normal vertex
vec3 normal(const vec2 &uv) const; // fetch the normal vector from the normal map texture
vec3 vert(const int i) const;
vec3 vert(const int iface, const int nthvert) const;
vec2 uv(const int iface, const int nthvert) const;
const TGAImage& diffuse() const { return diffusemap; }
const TGAImage& specular() const { return specularmap; }
};
verts:模型顶点坐标的集合,用于存储所有顶点的三维坐标。
tex_coord:纹理坐标(u,v)(tex coords是texture coordinate的缩写)。
norms:法线。
facet_vrt,facet_tex,facet_nrm:面片索引,用于确定每一个(三角形)面片所对应的顶点,纹理坐标,法线,分别是在上述数组中的哪一位置。
diffusemap;normalmap;specularmap:分别是漫反射贴图,法线贴图,镜面贴图。
一些成员函数的声明,具体函数会在后面介绍。
2、成员函数的详细解释
1)Model构造函数
Model::Model(const std::string filename) {
std::ifstream in;
in.open(filename, std::ifstream::in);
if (in.fail()) return;
//利用ifstream类型打开文件,并保证文件正常打开
std::string line;
while (!in.eof()) { //检查文件是否读到末尾
std::getline(in, line);
std::istringstream iss(line.c_str());//将line字符串c风格化,再用iss归类
char trash;
if (!line.compare(0, 2, "v ")) { //如果这一行是“v”开头
iss >> trash;
vec3 v;
for (int i=0;i<3;i++) iss >> v[i];
verts.push_back(v);//添加到数组末尾
} else if (!line.compare(0, 3, "vn ")) {
iss >> trash >> trash;
vec3 n;
for (int i=0;i<3;i++) iss >> n[i];
norms.push_back(n.normalized());//标准化后,添加到末尾
} else if (!line.compare(0, 3, "vt ")) {
iss >> trash >> trash;
vec2 uv;
for (int i=0;i<2;i++) iss >> uv[i];
tex_coord.push_back({uv.x, 1-uv.y});//翻转后,添加到末尾
} else if (!line.compare(0, 2, "f ")) {
int f,t,n;
iss >> trash;
int cnt = 0;
while (iss >> f >> trash >> t >> trash >> n) {
facet_vrt.push_back(--f);//
facet_tex.push_back(--t);//索引减一
facet_nrm.push_back(--n);//
cnt++;
}
if (3!=cnt) {
std::cerr << "Error: the obj file is supposed to be triangulated" << std::endl;//判断面片是否为三角形
return;
}
}
}
std::cerr << "# v# " << nverts() << " f# " << nfaces() << " vt# " << tex_coord.size() << " vn# " << norms.size() << std::endl;
load_texture(filename, "_diffuse.tga", diffusemap );
load_texture(filename, "_nm_tangent.tga", normalmap );
load_texture(filename, "_spec.tga", specularmap);
}
读取obj文件中的相关数据后,载入纹理。初始化三维模型。
2)获取属性函数
int Model::nverts() const {
return verts.size();
}
int Model::nfaces() const {
return facet_vrt.size()/3;
}
nverts获取顶点数,nfaces获取面片数。
vec3 Model::vert(const int i) const {
return verts[i];
}
vec3 Model::vert(const int iface, const int nthvert) const {
return verts[facet_vrt[iface*3+nthvert]];
}
通过索引获取顶点,通过面片索引获取对应顶点的函数。
void Model::load_texture(std::string filename, const std::string suffix, TGAImage &img) {
size_t dot = filename.find_last_of(".");//获取后缀名
if (dot==std::string::npos) return;
std::string texfile = filename.substr(0,dot) + suffix;//更改新的后缀名
std::cerr << "texture file " << texfile << " loading " << (img.read_tga_file(texfile.c_str()) ? "ok" : "failed") << std::endl;//通过TGAImage的内置函数载入图片
}
载入纹理
vec3 Model::normal(const vec2 &uvf) const {
TGAColor c = normalmap.get(uvf[0]*normalmap.width(), uvf[1]*normalmap.height());
return vec3{(double)c[2],(double)c[1],(double)c[0]}*2./255. - vec3{1,1,1};
}
将法线贴图中的颜色信息转换为空间坐标轴信息,输出vec3类型。
vec2 Model::uv(const int iface, const int nthvert) const {
return tex_coord[facet_tex[iface*3+nthvert]];
}
vec3 Model::normal(const int iface, const int nthvert) const {
return norms[facet_nrm[iface*3+nthvert]];
}
通过面片索引获取纹理和法线的信息。
五、MVP变换
1、视口变换矩阵
mat<4,4> Viewport;
void viewport(const int x, const int y, const int w, const int h) {
Viewport = {{{w/2., 0, 0, x+w/2.}, {0, h/2., 0, y+h/2.}, {0,0,1,0}, {0,0,0,1}}};
}
将NDC(归一化后的Frustum)转换为屏幕坐标的矩阵变化。
将原来的长度2变为w和h,并移动至中心位置。
2、透视投影
mat<4,4> Projection;
void projection(const double f) { // check https://en.wikipedia.org/wiki/Camera_matrix
Projection = {{{1,0,0,0}, {0,-1,0,0}, {0,0,1,0}, {0,0,-1/f,0}}};
}
将3D模型的坐标,投影至2D平面,其中这个矩阵将y轴进行了对称操作,为了适应不同的坐标系。
-1/f是透视除法的一部分,用于实现透视效果。
(这边原作者用的是简化版的透视投影,常用的透视投影可以在我的另一篇博客中学习)
3、Model&Veiw 变换
void lookat(const vec3 eye, const vec3 center, const vec3 up) { // check https://github.com/ssloy/tinyrenderer/wiki/Lesson-5-Moving-the-camera
vec3 z = (center-eye).normalized();
vec3 x = cross(up,z).normalized();
vec3 y = cross(z, x).normalized();
mat<4,4> Minv = {{{x.x,x.y,x.z,0}, {y.x,y.y,y.z,0}, {z.x,z.y,z.z,0}, {0,0,0,1}}};
mat<4,4> Tr = {{{1,0,0,-eye.x}, {0,1,0,-eye.y}, {0,0,1,-eye.z}, {0,0,0,1}}};
ModelView = Minv*Tr;
}
目标:将相机和物体移动至标准状态下(相机在原点,z轴正方向为观察方向,y轴正方向为相机向上方向)
z:观察方向向量
x:通过up方向和观察方向叉乘得出相机左侧方向
y:用z与x叉乘得出y轴方向,即相机右侧方向
Minv:旋转矩阵,将相机旋转至标准方向
Tr:平移矩阵,将相机平移至原点
ModelView:相机与模型共同移动,相对位置不会发生改变,不影响渲染效果。
先平移,再左乘进行旋转。
4、三角形面片
1)重心坐标(u,v,w)
vec3 barycentric(const vec2 tri[3], const vec2 P) {
mat<3,3> ABC = {{embed<3>(tri[0]), embed<3>(tri[1]), embed<3>(tri[2])}};
if (ABC.det()<1e-3) return {-1,1,1}; // for a degenerate triangle generate negative coordinates, it will be thrown away by the rasterizator
return ABC.invert_transpose() * embed<3>(P);
}
重心坐标:P = uA +vB +wC;(P为所求点,ABC为三角形顶点)
u+v+w = 1;u,v,w>0;
先将三角形置于三维空间中计算,如果三角形面积很小,返回{-1,1,1}传递该信息。
用ABC逆矩阵的转置,因为ABC转置后所构成的矩阵*(u,v,w)的转置 = p点的转置
所以(u,v,w)可以用p点的三维坐标*ABC的逆矩阵的转置计算得出
2)简单光栅化处理
void triangle(const vec4 clip_verts[3], IShader &shader, TGAImage &image, std::vector<double> &zbuffer) {
vec4 pts[3] = { Viewport*clip_verts[0], Viewport*clip_verts[1], Viewport*clip_verts[2] }; // triangle screen coordinates before persp. division
vec2 pts2[3] = { proj<2>(pts[0]/pts[0][3]), proj<2>(pts[1]/pts[1][3]), proj<2>(pts[2]/pts[2][3]) }; // triangle screen coordinates after perps. division
int bboxmin[2] = {image.width()-1, image.height()-1};
int bboxmax[2] = {0, 0};
for (int i=0; i<3; i++)
for (int j=0; j<2; j++) {
bboxmin[j] = std::min(bboxmin[j], static_cast<int>(pts2[i][j]));
bboxmax[j] = std::max(bboxmax[j], static_cast<int>(pts2[i][j]));
}
#pragma omp parallel for
for (int x=std::max(bboxmin[0], 0); x<=std::min(bboxmax[0], image.width()-1); x++) {
for (int y=std::max(bboxmin[1], 0); y<=std::min(bboxmax[1], image.height()-1); y++) {
vec3 bc_screen = barycentric(pts2, {static_cast<double>(x), static_cast<double>(y)});
vec3 bc_clip = {bc_screen.x/pts[0][3], bc_screen.y/pts[1][3], bc_screen.z/pts[2][3]};
bc_clip = bc_clip/(bc_clip.x+bc_clip.y+bc_clip.z); // check https://github.com/ssloy/tinyrenderer/wiki/Technical-difficulties-linear-interpolation-with-perspective-deformations
double frag_depth = vec3{clip_verts[0][2], clip_verts[1][2], clip_verts[2][2]}*bc_clip;
if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0 || frag_depth > zbuffer[x+y*image.width()]) continue;
TGAColor color;
if (shader.fragment(bc_clip, color)) continue; // fragment shader can discard current fragment
zbuffer[x+y*image.width()] = frag_depth;
image.set(x, y, color);
}
}
z_buffer:深度缓冲区
clip_verts:存储三角形的三个顶点(这边应该用的是已经经过NDC标准化后的坐标)
对顶点进行视图变换之后,再将其先转换为非齐次坐标,后从3维降为2维。
bboxmin[]和bboxmax[]用于存储包围盒的大小
逐像素分析三角形面片所在的包围盒。
bc_screen:存储该像素点再三角形面片中的重心坐标。
bc_clip:将bc_screen齐次坐标的最后一位除成1(透视矫正),后对其进行归一化,以便后面的插值、权重等的计算。这里其实是权重向量。
frag_depth:加权平均之后的三角形深度值。(z值)
如果该点不在三角形内或者该点的深度比深度缓冲区对应点的深度大则不进行绘制。
经过片段着色器(会在后面介绍)计算颜色,如果片段着色器认为其可丢弃,则不进行绘制。
更新深度缓冲区该点的值,在img中绘制该点。
六、shader
1、Ishader结构体(父类)
struct IShader {
static TGAColor sample2D(const TGAImage &img, vec2 &uvf) {
return img.get(uvf[0] * img.width(), uvf[1] * img.height());
}
virtual bool fragment(const vec3 bar, TGAColor &color) = 0;
};
根据uvf中存储的uv坐标从纹理图片中获取纹理图中该像素的颜色。
定义片段着色器的虚方法(=0说明是纯虚方法,类似csharp语言中的抽象类)
2、shader(子类)
1)一些量
场景中需要的公共变量
constexpr int width = 800; // output image size//constexpr可以用于修饰函数的常量修饰
constexpr int height = 800;
constexpr vec3 light_dir{1,1,1}; // light source
constexpr vec3 eye{1,1,3}; // camera position
constexpr vec3 center{0,0,0}; // camera direction
constexpr vec3 up{0,1,0}; // camera up vector
extern mat<4,4> ModelView; // "OpenGL" state matrices
extern mat<4,4> Projection;
shader内部的私有变量
const Model &model;
vec3 uniform_l; // light direction in view coordinates
mat<2,3> varying_uv; // triangle uv coordinates, written by the vertex shader, read by the fragment shader
mat<3,3> varying_nrm; // normal per vertex to be interpolated by FS
mat<3,3> view_tri; // triangle in view coordinates
model:储存模型
uniform_1:储存归一化后的光照方向
varying_uv:记录三角形三个顶点的uv坐标
varying_nrm:记录三角形三个顶点的法线
view_tri:记录三角形三个顶点归一化后的空间坐标
2)构造函数
Shader(const Model &m) : model(m) {
uniform_l = proj<3>((ModelView*embed<4>(light_dir, 0.))).normalized(); // transform the light vector to view coordinates
}
主要目的:初始化“光”,将光的三维方向旋转成4维齐次,经过ModelView视口变换后,再变回三维并标准化。结果是光的照射方向。
3)将三角形面片模型标准化
virtual void vertex(const int iface, const int nthvert, vec4& gl_Position) {
varying_uv.set_col(nthvert, model.uv(iface, nthvert));
varying_nrm.set_col(nthvert, proj<3>((ModelView).invert_transpose()*embed<4>(model.normal(iface, nthvert), 0.)));
gl_Position= ModelView*embed<4>(model.vert(iface, nthvert));
view_tri.set_col(nthvert, proj<3>(gl_Position));
gl_Position = Projection*gl_Position;
}
iface:需要处理的三角形的索引
nthvert:需要处理的三角形顶点索引(0,1,2)
gl_Position:处理后该顶点的位置,齐次坐标。
将对应点的uv坐标存入varying_uv对应点的位置
因为法线始终与面(点)垂直,所以在模型经过MV变换之后,要使法线依旧保持垂直,需要对法线进行逆转置变换
n(法线)*v(顶点)=0;n*MV(变换矩阵)*v=0
顶点的三维坐标增加成四维齐次坐标,再进行视角变换
如果将这个齐次坐标转成三维即为顶点归一化后的空间坐标veiw_tri;
如果将这个齐次坐标进行透视,就是这个顶点所在裁剪空间中的最后目标位置,存储在gl_Position中