簡單繪制一個三角形
上一節末我們已經能夠畫一條直線了
void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color)
{
// ...
}
在VS裡建立一個geometry.h,然後把代碼複制進去,這個頭檔案裡就是一些坐标類型和計算,這個頭檔案依舊來自最開始介紹的git,你想去自己下載下傳也行~
#ifndef __GEOMETRY_H__
#define __GEOMETRY_H__
#include <cmath>
#include <iostream>
#include <vector>
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
template <class t> struct Vec2
{
t raw[2];
t& x;
t& y;
Vec2<t>() : raw(), x(raw[0]), y(raw[1]) { x = y = t(); }
Vec2<t>(t _x, t _y) : raw(), x(raw[0]), y(raw[1]) { x = _x; y = _y; }
Vec2<t>(const Vec2<t>& v) : raw(), x(raw[0]), y(raw[1]) { *this = v; }
Vec2<t>& operator =(const Vec2<t>& v)
{
if (this != &v)
{
x = v.x;
y = v.y;
}
return *this;
}
Vec2<t> operator +(const Vec2<t>& V) const { return Vec2<t>(x + V.x, y + V.y); }
Vec2<t> operator -(const Vec2<t>& V) const { return Vec2<t>(x - V.x, y - V.y); }
Vec2<t> operator *(float f) const { return Vec2<t>(x * f, y * f); }
t& operator[](const int i) { return raw[i]; }
template <class > friend std::ostream& operator<<(std::ostream& s, Vec2<t>& v);
};
template <class t> struct Vec3
{
t raw[3];
t& x;
t& y;
t& z;
Vec3<t>() : raw(), x(raw[0]), y(raw[1]), z(raw[2]) { x = y = z = t(); }
Vec3<t>(t _x, t _y, t _z) : raw(), x(raw[0]), y(raw[1]), z(raw[2]) { x = _x; y = _y; z = _z; }
template <class u> Vec3<t>(const Vec3<u>& v);
Vec3<t>(const Vec3<t>& v) : raw(), x(raw[0]), y(raw[1]), z(raw[2]) { *this = v; }
Vec3<t>& operator =(const Vec3<t>& v)
{
if (this != &v)
{
x = v.x;
y = v.y;
z = v.z;
}
return *this;
}
Vec3<t> operator ^(const Vec3<t>& v) const { return Vec3<t>(y * v.z - z * v.y, z * v.x - x * v.z, x * v.y - y * v.x); }
Vec3<t> operator +(const Vec3<t>& v) const { return Vec3<t>(x + v.x, y + v.y, z + v.z); }
Vec3<t> operator -(const Vec3<t>& v) const { return Vec3<t>(x - v.x, y - v.y, z - v.z); }
Vec3<t> operator *(float f) const { return Vec3<t>(x * f, y * f, z * f); }
t operator *(const Vec3<t>& v) const { return x * v.x + y * v.y + z * v.z; }
float norm() const { return std::sqrt(x * x + y * y + z * z); }
Vec3<t>& normalize(t l = 1) { *this = (*this) * (l / norm()); return *this; }
t& operator[](const int i) { return raw[i]; }
template <class > friend std::ostream& operator<<(std::ostream& s, Vec3<t>& v);
};
typedef Vec2<float> Vec2f;
typedef Vec2<int> Vec2i;
typedef Vec3<float> Vec3f;
typedef Vec3<int> Vec3i;
template <> template <> Vec3<int>::Vec3(const Vec3<float>& v);
template <> template <> Vec3<float>::Vec3(const Vec3<int>& v);
template <class t> std::ostream& operator<<(std::ostream& s, Vec2<t>& v)
{
s << "(" << v.x << ", " << v.y << ")\n";
return s;
}
template <class t> std::ostream& operator<<(std::ostream& s, Vec3<t>& v)
{
s << "(" << v.x << ", " << v.y << ", " << v.z << ")\n";
return s;
}
//////////////////////////////////////////////////////////////////////////////////////////////
const int DEFAULT_ALLOC = 4;
class Matrix
{
std::vector<std::vector<float> > m;
int rows, cols;
public:
Matrix(int r = DEFAULT_ALLOC, int c = DEFAULT_ALLOC);
inline int nrows();
inline int ncols();
static Matrix identity(int dimensions);
std::vector<float>& operator[](const int i);
Matrix operator*(const Matrix& a);
Matrix transpose();
Matrix inverse();
friend std::ostream& operator<<(std::ostream& s, Matrix& m);
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#endif //__GEOMETRY_H__
然後我們把原本的line函數再封裝一下,改用Vec2i類型,或者就直接把原來的line的參數改掉就行...我就是懶得改了...
void line(Vec2i t0, Vec2i t1,TGAImage& image, TGAColor color)
{
line(t0.x, t0.y, t1.x, t1.y, image, color);
}
然後利用這個函數很容易寫出一三角形的繪制方法
void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
line(t0, t1, image, color);
line(t1, t2, image, color);
line(t2, t0, image, color);
}
測試一下
const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor red = TGAColor(255, 0, 0, 255);
const TGAColor green = TGAColor(0, 255, 0, 255);
const int width = 200;
const int height = 200;
int main()
{
TGAImage image(width, height, TGAImage::RGB);
Vec2i t0[3] = { Vec2i(10, 70), Vec2i(50, 160), Vec2i(70, 80) };
Vec2i t1[3] = { Vec2i(180, 50), Vec2i(150, 1), Vec2i(70, 180) };
Vec2i t2[3] = { Vec2i(180, 150), Vec2i(120, 160), Vec2i(130, 180) };
triangle(t0[0], t0[1], t0[2], image, red);
triangle(t1[0], t1[1], t1[2], image, white);
triangle(t2[0], t2[1], t2[2], image, green);
image.flip_vertically();
image.write_tga_file("output.tga");
return 0;
}

這是很簡單的...但不是我們想要的...很顯然,我們應該需要一個填充滿的三角形,而不是像這樣隻有邊框
填充一個三角形
思路是很簡單的,我們周遊所有的像素,然後判斷這個像素在不在某個三角形内,如果在,那就填充他
因為有多個三角形,是以我們是分批周遊的,也就是說如果有3個三角形,那麼就需要把所有的像素周遊3遍,但很明顯這是有很大改進空間的,對于某一個三角形來說,我們不需要周遊所有的像素,隻需要周遊它的包圍盒就行
如圖,假設這是一張30x30像素的圖像,那麼,如果我們想要周遊所有像素,那就需要計算900次,而三角形的包圍盒,就是圖中紅色部分,我們隻周遊這部分的話,那很明顯,隻需要周遊6x10=60次,當三角形數量非常大時,效率會提升非常的多!
那麼現在就有兩個問題
- 怎麼确定一個三角形的包圍盒?
- 怎麼判斷某一個點是否在三角形内?
第一個問題是比較簡單的,根據3個頂點的坐标,很容易就能夠确定這個包圍盒,那麼如何解決問題2呢?一般來說,我們會利用向量的叉乘
關于向量的叉乘我就不科普太多了,這部分知識掌我握程度不夠,了解也很淺,這邊就直接展示他的原理了
首先二維向量叉乘的公式是 (x1,y1)x(x2,y2)=x2y1-x1y2,結果是一個值(其實應該也算是向量,不多解釋了)
那麼怎麼利用這個公式來判斷D E兩點是否在三角形内呢?
A(4,5) B(6,15) E(3,10) D(7,10)
AB=(2,10) AE=(-1,5) AD=(3,5)
ABxAE = -10 - 10 = -20
ABxAD = 30 - 10 = 20
結果很明顯了,如果E點在AB的外側,那麼ABxAE就<0,反之如果D點在AB的内側,ABxAD就>0
真的如此嗎?
看起來是這樣的..但其實不一定,多試幾次就會發現問題...根據我們求的順序可能會發生<0在内側而>0在外側的情況
但有一個結論是一定的:如果P點在三角形内側,那麼他與三個頂點組成的向量,分别叉乘的結果是一緻的,即要麼都大于0要麼都小于0
是以我們依次計算
ABxAD BCxBD CAxCD
如果每一個結果都>0或者都<0,那就說明這個點在三角形的内部,如果有大有小,那麼就在三角形外側
注意一定要按順序,要麼是ABxAD BCxBD CAxCD要麼是ACxAD CBxCD BAxBD
想實作這個算法的方式有很多,直接利用三維向量的cross來做是最簡單的,但是可能不太好了解,這裡我寫上我的做法,非常非常非常的直覺~
// pts是三角形的三個頂點,P就是想判斷的點
int barycentric(Vec2i* pts, Vec2i P)
{
int pre = -1;
for (int i = 0; i < 3; i++)
{
// AB = B - A
int x1 = pts[(i + 1) % 3][0] - pts[i][0];
int y1 = pts[(i + 1) % 3][1] - pts[i][1];
// AP = P - A
int x2 = P[0] - pts[i][0];
int y2 = P[1] - pts[i][1];
// x2y1-x1y2
int res = (x2 * y1 - x1 * y2);
// 第一次的時候我們就确定好,這個點如果在内側,是需要都大于0還是都小于0,隻有第一次會計算哦
if (pre == -1)
{
pre = res > 0 ? 1 : 0;
}
res = res > 0 ? 1 : 0;
if (res != pre)
{
return 0;
}
}
return 1;
}
然後我們來改善我們的三角形算法
void triangle(Vec2i *pts, TGAImage &image, TGAColor color) {
// 這裡是包圍盒的最大最小範圍
Vec2i bboxmin(image.get_width()-1, image.get_height()-1);
Vec2i bboxmax(0, 0);
// 這個是用來限制我們的範圍,總不能比圖檔本身大小還大吧?
Vec2i clamp(image.get_width()-1, image.get_height()-1);
// 分别對三個頂點周遊
for (int i=0; i<3; i++)
{
// 分别對xy周遊
for (int j=0; j<2; j++)
{
// 這裡嵌套了兩層,先比較目前記錄的最小和目前頂點,再比較0和最小點
bboxmin[j] = std::max(0, std::min(bboxmin[j], pts[i][j]));
bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts[i][j]));
}
}
Vec2i P;
for (P.x = bboxmin.x; P.x <= bboxmax.x; P.x++)
{
for (P.y = bboxmin.y; P.y <= bboxmax.y; P.y++)
{
int res = barycentric(pts, P);
if (res == 0)
continue;
image.set(P.x, P.y, color);
}
}
}
測試一下吧
int main()
{
TGAImage frame(200, 200, TGAImage::RGB);
Vec2i pts[3] = { Vec2i(10,10), Vec2i(100, 30), Vec2i(190, 160) };
triangle(pts, frame, red);
frame.flip_vertically();
frame.write_tga_file("output.tga");
return 0;
}
引入模型解析檔案
在這裡,下載下傳model.h,model.cpp,并把obj檔案夾裡的模型資料檔案也下載下傳下來,導入VS中
我們用txt打卡obj檔案夾中的檔案,發現就是一堆資料而已
具體的資料格式解析可以參考這裡,注意這是obj檔案,不是fbx
模型繪制
const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor red = TGAColor(255, 0, 0, 255);
const TGAColor green = TGAColor(0, 255, 0, 255);
Model* model = NULL;
const int width = 800;
const int height = 800;
int main()
{
model = new Model("obj/african_head.obj");
TGAImage image(width, height, TGAImage::RGB);
Vec3f light_dir(0, 0, -1);
// model->nfaces()代表這個obj資料一共有多少個面
for (int i = 0; i < model->nfaces(); i++)
{
// 這裡我們擷取到模型的某一個面
std::vector<int> face = model->face(i);
// 每一個面由3個頂點構成,這裡我們擷取到這3個定點,儲存到screen_coords中
Vec2i screen_coords[3];
for (int j = 0; j < 3; j++)
{
// 注意,obj中我們擷取到的是頂點資料,是世界坐标,而我們繪制在螢幕上需要的是螢幕坐标,是需要轉換的
Vec3f world_coords = model->vert(face[j]);
screen_coords[j] = Vec2i((world_coords.x + 1.) * width / 2., (world_coords.y + 1.) * height / 2.);
}
// 最終我們得到了三個頂點坐标,注意這裡是沒有z的,隻用了xy
triangle(screen_coords, image, white);
}
image.flip_vertically();
image.write_tga_file("output.tga");
delete model;
return 0;
}
結果會是一張純白的圖檔,看起來完全不是一個3D模型,為什麼會這樣?
其實算法是沒有問題的,但我們繪制的時候隻是單純的調用了繪制三角形的方法,而它會把三角形内的像素都填充為白色,并沒有考慮三角形的朝向與顔色的關系,那自然就是純白的一張圖檔了
稍微改進一下
int main()
{
model = new Model("obj/african_head.obj");
TGAImage image(width, height, TGAImage::RGB);
// 定義一個光照方向,用來計算顔色衰減,(0,0,-1)代表垂直往圖檔内看
Vec3f light_dir(0, 0, -1);
for (int i = 0; i < model->nfaces(); i++)
{
std::vector<int> face = model->face(i);
Vec2i screen_coords[3];
// 這裡我們儲存一下我們的世界坐标
Vec3f world_coords[3];
for (int j = 0; j < 3; j++)
{
Vec3f v = model->vert(face[j]);
screen_coords[j] = Vec2i((v.x + 1.) * width / 2., (v.y + 1.) * height / 2.);
world_coords[j] = v;
}
// 這裡計算的是目前這個三角形面的法線,計算的很粗略,其實原理很簡單,這裡是世界坐标是三維的,我們知道三角形的三個頂點坐标,那很容易知道任意兩條邊 // 的向量,而三維向量的叉乘,結果還是一個向量,并且同時垂直于叉乘的兩個向量,那可不就是這個面的法線嘛?
Vec3f n = (world_coords[2] - world_coords[0]) ^ (world_coords[1] - world_coords[0]);
n.normalize();
// 用光照方向乘以法線方向,注意這裡是點積
// 其實也很好了解,如果法線方向是(0,0,-1)和光照方向完全一緻,那麼結果就是1,
// 如果法線方向類似于(0,0,1)和光照方向是反的,結果就是個負數,那麼說明這個面是背對我們的,就不需要繪制了
// 而如果法線方向是其他的方向,比如(0.5,0,-0.5),稍微有點偏斜的方向,那麼結果就是0.5,說明這個三角形的亮度需要暗一些
// 當然這隻是粗略計算
float intensity = n * light_dir;
if (intensity > 0)
{
triangle(screen_coords, image, TGAColor(intensity * 255, intensity * 255, intensity * 255, 255));
}
}
image.flip_vertically();
image.write_tga_file("output.tga");
delete model;
return 0;
}
圖中的每一個部分都是三角形,很容易發現三角形的朝向和顔色有直接關系,不難了解~
但是我們會發現嘴巴和眼睛那裡有很大的問題
我直接說原因了,比如嘴巴那部分,其實繪制的不是嘴巴,而是我們的後腦勺,為什麼?
因為我們現在沒有利用到坐标裡的z軸,也就是說,如果我們的後腦勺和嘴巴,他們的三角形面片都是正面(因為如果是背面那會被忽視)