前言
本教程第一步是通過兩個點畫一條直線到圖像中,例如

最后,通過畫直線的API,畫出一個頭部模型

好了,現(xiàn)在我們開始吧
準備工作
現(xiàn)在我們需要做一些準備工作,因為本教程主要參考這個英文教程,所以用的一些基礎(chǔ)代碼是從那里拷貝過來,包括image文件的創(chuàng)建、寫入與保存,以及模型文件的讀取。因為我還做了一些小修改,所以大家可以到我的github里面下載代碼和資源,有問題隨時與我溝通。
1. 試著生成一張tga圖片
// 構(gòu)造了一個*TGAImage*對象用于圖片生成,大小為100*100像素。
TGAImage image(100, 100, TGAImage::RGB);
// 之后設(shè)置坐標為(40,50)的像素顏色為紅色。
image.set(40, 50, TGAColor(255, 0, 0, 255));
// 第三行為圖像上下翻轉(zhuǎn)(不翻轉(zhuǎn)一下方向會錯誤,個中玄機不是很了解)。
image.flip_vertically();
// 第四行為保存為tga文件,注意如果文件夾不存在不會自動創(chuàng)建。
image.write_tga_file("output/lesson1/point.tga");
執(zhí)行上面的代碼,會會生成下面這樣一張圖片(注意看中間偏左的點):
tips: 如果tga圖片打不開,這有個免費軟件可以打開tga圖片。

假如你生成了上面那個圖片,就已經(jīng)開了個好頭了!
2. 增加一個基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)
下面要開始嘗試畫直線了,在畫直線之前我們要準備一個基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu):點。
struct float2
{
float x = 0;
float y = 0;
float2() {}
float2(float xx, float yy) : x(xx), y(yy) { }
};
3. 畫一條直線
好吧,點已經(jīng)有了。下面我們構(gòu)想一下畫直線的接口應(yīng)該如何。一條直線應(yīng)該是由連續(xù)的點組成的,我們做的接口應(yīng)該可以接受兩個頂點為參數(shù),然后這個函數(shù)可以針對兩個頂點間的每一個點做一些特殊操作(例如寫入圖像)。英文教程提供的是下面這樣的接口:
// x0,y0為點0,x1,y1為點1,image是要寫入的圖像,color為線顏色
void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color);
我認為這樣的接口對于后續(xù)的工作不夠友好,因此準備做一個這樣的接口:
void Interpolation(float2 p1, float2 p2, function<void(float2)> handler);
這樣是一個插值接口,可以對p1,p2之間的點通過handler做回調(diào)。
那么我們畫直線的地方就變成了
const TGAColor red = TGAColor(255, 0, 0, 255); // 線的顏色
TGAImage image(100, 100, TGAImage::RGB); // 圖像寫入的對象
// 兩個頂點分別為(10, 10)和(90, 90)
// 那么這條直線應(yīng)該就是從(10, 10)到(90, 90)的45度斜線
// p點是線段上的點,例如(10,10)、(11,11)、(12,12)...(90,90)
Interpolation({10, 10}, {90, 90}, [&](float2 p) {
image.set(p.x, p.y, red);
});
image.flip_vertically();
image.write_tga_file("output/lesson1/redline.tga"); // 輸出
好了,現(xiàn)在就差Interpolation函數(shù)的實現(xiàn)了。
第一次畫線嘗試
第一節(jié)課的目標是渲染一個由線組成的網(wǎng)格。為了實現(xiàn)這個目標,我們需要先學(xué)會如何畫一個線。在看其他人的實現(xiàn)之前,我們可以先嘗試自己實現(xiàn)一下。在點(x0,y0)與點(x1,y1)之間畫一條線段,代碼也許看來是這樣子的:
void Interpolation(float2 p1, float2 p2, function<void(float2)> handler)
{
int x0 = p1.x;
int y0 = p1.y;
int x1 = p2.x;
int y1 = p2.y;
for (float t = 0.0; t < 1.0; t += 0.01)
{
int x = x0*(1.0 - t) + x1*t;
int y = y0*(1.0 - t) + y1*t;
handler(float2(x, y));
}
}
得到

第二次嘗試
第一次嘗試的代碼存在的問題是那個變量(0.01),如果我們把它變成了0.1,我們的線段就會變成這樣。

問題出在有多少個像素要去畫,而用一個靜態(tài)值確定有多少個像素去畫顯然是不正確的,所以我們嘗試對代碼做出如下修改
void Interpolation(float2 p1, float2 p2, function<void(float2)> handler)
{
int x0 = p1.x;
int y0 = p1.y;
int x1 = p2.x;
int y1 = p2.y;
for (int x = x0; x <= x1; x++)
{
float t = (x - x0) / (float)(x1 - x0);
int y = y0*(1. - t) + y1*t;
handler(float2(x, y));
}
}
我們通過以下代碼進行測試
TGAImage image(100, 100, TGAImage::RGB);
Interpolation0({ 13, 20 }, { 80, 40 }, [&](float2 p) { image.set(p.x, p.y, white); });
Interpolation0({ 20, 13 }, { 40, 80 }, [&](float2 p) { image.set(p.x, p.y, red); });
Interpolation0({ 80, 40 }, { 13, 20 }, [&](float2 p) { image.set(p.x, p.y, red); });
image.flip_vertically();
image.write_tga_file("output/lesson1/temp.tga");
得到結(jié)果

結(jié)果第一條線顯示的還不錯,第二條線中間出現(xiàn)了不連續(xù),而第三條線直接沒畫出來。注意第一條線和第三條線畫的是不同顏色的相同的線,只是方向不同。我們看見了白色的線沒有看見紅色的線,這又暴露出了我們代碼的一個問題:畫出的線段不應(yīng)該依賴與點的順序,線段(a,b)和線段(b,a)看起來應(yīng)該是一樣的。
第三次嘗試
我們可以通過交換p0和p1的來保證x0的總是比x1小。
第二條線段不連續(xù)的原因是線段的高度比線段的寬度要大。似乎我們可以通過對線段的旋轉(zhuǎn)保證線段的高度比寬度小來解決這個問題:
void Interpolation(float2 p1, float2 p2, function<void(float2)> handler)
{
int x0 = p1.x;
int y0 = p1.y;
int x1 = p2.x;
int y1 = p2.y;
bool steep = false;
// 保證高度小于寬度
if (std::abs(x0 - x1) < std::abs(y0 - y1))
{
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
// 保證從左向右畫
if (x0 > x1)
{
std::swap(x0, x1);
std::swap(y0, y1);
}
for (int x = x0; x <= x1; x++) {
float t = (x - x0) / (float)(x1 - x0);
int y = y0*(1.0 - t) + y1*t;
if (steep) handler(float2(y, x));
else handler(float2(x, y));
}
}
得到

看起來還不錯
使用最優(yōu)化的算法
畫線是一個圖形渲染的基礎(chǔ),對性能要求極高,因此我們需要找出一個比較高效的算法,參考這篇知乎專欄和原英文教程,我們可以最終寫出這樣的插值算法:
void Interpolation(float2 p1, float2 p2, function<void(float2)> handler)
{
int x0 = p1.x;
int y0 = p1.y;
int x1 = p2.x;
int y1 = p2.y;
bool steep = false;
if (std::abs(x0 - x1)<std::abs(y0 - y1)) {
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
if (x0>x1) {
std::swap(x0, x1);
std::swap(y0, y1);
}
int dx = x1 - x0;
int dy = y1 - y0;
int derror2 = std::abs(dy) * 2;
int error2 = 0;
int y = y0;
for (int x = x0; x <= x1; x++) {
if (steep)
handler(float2(y, x));
else
handler(float2(x, y));
error2 += derror2;
if (error2 > dx) {
y += (y1>y0 ? 1 : -1);
error2 -= dx * 2;
}
}
}
畫出模型
我使用的是英文教程中提供的模型代碼,下面是模型繪制代碼:
const float width = 1000;
const float height = 1000;
const TGAColor white = TGAColor(255, 255, 255, 255);
Model model("resource/african_head/african_head.obj");
TGAImage image(width, height, TGAImage::RGB);
for (int i = 0; i<model.nfaces(); i++) {
vector<int> face = model.face(i);
for (int j = 0; j<3; j++) {
float3 v1 = model.vert(face[j]);
float3 v2 = model.vert(face[(j + 1) % 3]);
float2 p1((v1.x + 1.0) * width / 2.0, (v1.y + 1.0) * height / 2.0);
float2 p2((v2.x + 1.0) * width / 2.0, (v2.y + 1.0) * height / 2.0);
Interpolation(p1, p2, [&](float2 p) {
image.set(p.x, p.y, white);
});
}
}
image.flip_vertically();
image.write_tga_file("output/lesson1/model.tga");
因為我們是正面朝向模型,所以模型中點的深度z我們暫時可以忽略,通過model.vert(int index)接口,我們?nèi)〉昧四P椭械捻旤c數(shù)據(jù)(face中存儲的是頂點索引數(shù)據(jù),用頂點索引的好處是節(jié)省頂點數(shù)量,畢竟每個三角形都會與其他三角形共用頂點),如果你之前寫的代碼沒有問題,那么應(yīng)該會生成一張這樣的圖片:

好了,本次的教程就結(jié)束了,下次教程我們會開始畫三角形,這樣我們的模型就會變成不是鏤空的了。