天天看點

03 三角形與光栅化

簡單繪制一個三角形

上一節末我們已經能夠畫一條直線了

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;
}
           
03 三角形與光栅化

這是很簡單的...但不是我們想要的...很顯然,我們應該需要一個填充滿的三角形,而不是像這樣隻有邊框

填充一個三角形

思路是很簡單的,我們周遊所有的像素,然後判斷這個像素在不在某個三角形内,如果在,那就填充他

因為有多個三角形,是以我們是分批周遊的,也就是說如果有3個三角形,那麼就需要把所有的像素周遊3遍,但很明顯這是有很大改進空間的,對于某一個三角形來說,我們不需要周遊所有的像素,隻需要周遊它的包圍盒就行

03 三角形與光栅化

如圖,假設這是一張30x30像素的圖像,那麼,如果我們想要周遊所有像素,那就需要計算900次,而三角形的包圍盒,就是圖中紅色部分,我們隻周遊這部分的話,那很明顯,隻需要周遊6x10=60次,當三角形數量非常大時,效率會提升非常的多!

那麼現在就有兩個問題

  1. 怎麼确定一個三角形的包圍盒?
  2. 怎麼判斷某一個點是否在三角形内?

第一個問題是比較簡單的,根據3個頂點的坐标,很容易就能夠确定這個包圍盒,那麼如何解決問題2呢?一般來說,我們會利用向量的叉乘

關于向量的叉乘我就不科普太多了,這部分知識掌我握程度不夠,了解也很淺,這邊就直接展示他的原理了

03 三角形與光栅化

首先二維向量叉乘的公式是 (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;
}
           
03 三角形與光栅化

引入模型解析檔案

在這裡,下載下傳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;
}
           
03 三角形與光栅化

結果會是一張純白的圖檔,看起來完全不是一個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;
}
           
03 三角形與光栅化

圖中的每一個部分都是三角形,很容易發現三角形的朝向和顔色有直接關系,不難了解~

但是我們會發現嘴巴和眼睛那裡有很大的問題

我直接說原因了,比如嘴巴那部分,其實繪制的不是嘴巴,而是我們的後腦勺,為什麼?

因為我們現在沒有利用到坐标裡的z軸,也就是說,如果我們的後腦勺和嘴巴,他們的三角形面片都是正面(因為如果是背面那會被忽視)