从二维开始
假设有这样一张图片,横轴是X轴,纵轴是Y轴
想象一下,假设我们现在是从正面在看这张图,那么如果我们从顶部来看,俯视的看,会是什么样的情况?可能很抽象,尽可能的想象一下,应该是如此,我们看到的应该是图中我画出来的那一条线段。
为什么最开始是一段绿色而不是红色?很明显,因为我们是俯视来看的,那么上面的绿色肯定会遮住下面的红色,后面也同理,是什么决定了我们能看到的颜色?是高度,或者说Y的值,Y更大的颜色会遮挡住Y更小的颜色,这就是深度缓冲。
那么对于这么一张俯视的图来说,我们需要多少个深度缓冲呢?Width x Height吗?其实完全不用,最终我们看到的只是一条线段,我们只需要记录这条线段上,每一个点的深度就好,也就是需要Wdith个深度缓冲。
写段代码可能理解起来更加容易
首先我们画出这四条线段,这利用我们之前创建的函数,应该是非常容易的
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 TGAColor blue = TGAColor(0, 0, 255, 255);
const int width = 800;
const int height = 800;
int main()
{
TGAImage scene(width, height, TGAImage::RGB);
// 红黄蓝三条线段,代表我们正面看到的情况
line(Vec2i(20, 34), Vec2i(744, 400), scene, red);
line(Vec2i(120, 434), Vec2i(444, 400), scene, green);
line(Vec2i(330, 463), Vec2i(594, 200), scene, blue);
// 底部的一根线段,代表我们从顶部俯视看到的情况
line(Vec2i(10, 10), Vec2i(790, 10), scene, white);
scene.flip_vertically();
scene.write_tga_file("scene.tga");
return 0;
}
应该会得到这样一个结果
此时底部的线段是全白的,当然这是因为我们还没开始写我们的缓冲区算法。。
// 注意,这个算法还是会画出红蓝绿三条线段的,你会发现和直线算法非常的像,只是多了一个y轴深度缓冲区
void rasterize(Vec2i p0, Vec2i p1, TGAImage& image, TGAColor color, int ybuffer[])
{
if (p0.x > p1.x)
{
std::swap(p0, p1);
}
for (int x = p0.x; x <= p1.x; x++)
{
float t = (x - p0.x) / (float)(p1.x - p0.x);
int y = p0.y * (1. - t) + p1.y * t + .5;
image.set(x, y, color);
// 到底为止,其实就是在画直线,还是利用x算出y,然后给对应坐标的像素赋颜色
// =====================
// =====================
// x代表着当前这个像素的横坐标,利用他,我们可以获取到深度缓冲数组中,对应的像素的深度,拿出来和当前的y作比较
// 如果当前的y更大,那么说明当前像素在上面,应该要遮住下面的像素,那么就给白色线段赋值
if (ybuffer[x] < y)
{
ybuffer[x] = y;
image.set(x, 10, color);
}
}
}
代码其实简单,但是初看可能没法抽象出来,多看,多画图,原理真的很简单
结果如下,非常完美
线性插值
其实一维缓冲能够明白的话,那二维是完全没有任何理解障碍的,对我来说唯一的区别就是,缓冲数组必须从一维变成二维,但其实这也是无所谓的,用一维数组一样可以写
当然这里还有一个问题,对于一个三角形而言,我们是知道他3个顶点的XYZ的,毕竟这在建模时就确定了,但是!但是一个三角形是会覆盖很多像素的,我们如何确定每一个像素的深度呢?
插值...还是插值...
如图所示,假如我们知道A的横坐标是X1,B的横坐标是X2,t代表着AC/AB,也就是AC线段在AB线段中的占比
那么我们应该如何描述C的横坐标呢?
其实很简单,而且我们上面也一直是这么做的...
这就是线性插值,t相当于一个权重,很明显的能发现,如果t是1,那么完全就是B点,即Xc=X2,反过来说,如果t是0,那么完全就是A点,而当C在AB中间时,具体的值就由两边的权重决定,从直觉上来说,这个公式是很容易理解的...
重心插值
但是,在这里,我们并不是要对两个点进行差值,而是要对三角形内的某一点进行差值,说的更具体一点,我们是要利用三角形三个顶点的Z值,插值出三角形内部某一点的Z值
对于线段的插值很好理解,而想要插值三角形,一般就会利用重心公式,说的更简单点,那就是面积法
假设三角形的总面积是A
那么A1的占比为A1/A,A2的占比为A2/A,A3的占比为A3/A
如何插值呢?其实这里的面积占比就等于上面的权重,权重都知道了,那么插值也就很简单的
好了,现在问题就变成了,怎么求三角形的面积?利用向量的叉积...
具体原理就不在这里解释了,总之,结论是三角形ABC的面积等于
那么自然三角形APB和三角形APC的面积也很容易求,三角形BPC的面积只要用总面积减去两个小三角形的面积就能够轻松获得
回到三维中
首先,我们需要写一个向量叉乘的公式...
在geometry.h中添加
Vec3f cross(Vec3f v1, Vec3f v2)
{
return Vec3f(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x);
}
这是两个三维向量的叉乘公式,但实际上我们这里需要的只是二维向量的叉乘,接着往下看~
我们改进一下我们的barycentric算法
Vec3f barycentric(Vec3f A, Vec3f B, Vec3f C, Vec3f P)
{
Vec3f s[2];
for (int i=2; i--; )
{
s[i][0] = C[i]-A[i];
s[i][1] = B[i]-A[i];
s[i][2] = A[i]-P[i];
}
Vec3f u = cross(s[0], s[1]);
if (std::abs(u[2])>1e-2)
return Vec3f(1.f-(u.x+u.y)/u.z, u.y/u.z, u.x/u.z);
return Vec3f(-1,1,1);
}
咱们解析一下这个算法,首先经过for循环,我们可以得到一个长度为2的Vec3f数组,他里面的内容是这样的
然后我们让S0叉乘S1,其算法我们上面也写了,结果还是一个三维向量,是
res = Vec3f(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x);
咱们先看他的Z
我们现在只是在求三角形的面积,所以这里的ABC三个点是二维的,我们不需要他们的Z值
那么可以得到
你会发现这就是上面叉乘结果的Z值!不过是相反的,但是向量叉积的正负只是代表了方向,对于二维三角形来说,这是没有意义的,咱们可以取绝对值,这就代表了三角形的总面积!即 Z = AC x AB
同理,咱们再看看res的X值和Y值
说到这这个函数的作用就完全明白了,一次叉乘就计算出了我们所需要的所有面积
我们先把原来的代码修改一下
Vec3f barycentric(Vec3f A, Vec3f B, Vec3f C, Vec3f P)
{
Vec3f s[2];
for (int i = 2; i--; )
{
s[i][0] = C[i] - A[i];
s[i][1] = B[i] - A[i];
s[i][2] = A[i] - P[i];
}
Vec3f u = cross(s[0], s[1]);
if (std::abs(u[2]) > 1e-2)
return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
return Vec3f(-1, 1, 1);
}
void triangle(Vec3f* pts, TGAImage& image, TGAColor color)
{
Vec2f bboxmin(std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
Vec2f bboxmax(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
Vec2f clamp(image.get_width() - 1, image.get_height() - 1);
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 2; j++)
{
bboxmin[j] = std::max(0.f, std::min(bboxmin[j], pts[i][j]));
bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts[i][j]));
}
}
Vec3f P;
for (P.x = bboxmin.x; P.x <= bboxmax.x; P.x++)
{
for (P.y = bboxmin.y; P.y <= bboxmax.y; P.y++)
{
Vec3f res = barycentric(pts[0], pts[1], pts[2], P);
if (res[0] < 0 || res[1] < 0 || res[2] < 0)
continue;
image.set(P.x, P.y, color);
}
}
}
主要是把原来的Vec2i换成了Vec3f,并用上了真正的重心算法(之前那个朴素的算法当然也是没问题的...
尝试绘制一下
Vec3f world2screen(Vec3f v)
{
return Vec3f(int((v.x + 1.) * width / 2. + .5), int((v.y + 1.) * height / 2. + .5), v.z);
}
int main()
{
model = new Model("obj/african_head.obj");
TGAImage image(width, height, TGAImage::RGB);
Vec3f light_dir(0, 0, -1);
for (int i = 0; i < model->nfaces(); i++)
{
std::vector<int> face = model->face(i);
Vec3f screen_coords[3];
Vec3f world_coords[3];
for (int j = 0; j < 3; j++)
{
Vec3f v = model->vert(face[j]);
screen_coords[j] = world2screen(v);
world_coords[j] = v;
}
Vec3f n = (world_coords[2] - world_coords[0]) ^ (world_coords[1] - world_coords[0]);
n.normalize();
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;
}
就不放图了,应该是没有问题的
然后我们补上深度缓冲
void triangle(Vec3f* pts, float* zbuffer, TGAImage& image, TGAColor color)
{
//...
for (P.x = bboxmin.x; P.x <= bboxmax.x; P.x++)
{
for (P.y = bboxmin.y; P.y <= bboxmax.y; P.y++)
{
Vec3f bc_screen = barycentric(pts[0], pts[1], pts[2], P);
if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
P.z = 0;
// 这里是差值计算Z
for (int i = 0; i < 3; i++) P.z += pts[i][2] * bc_screen[i];
if (zbuffer[int(P.x + P.y * width)] < P.z)
{
zbuffer[int(P.x + P.y * width)] = P.z;
image.set(P.x, P.y, color);
}
}
}
}
int main()
{
// ...
// 定义缓冲区并初始化
float* zbuffer = new float[width * height];
for (int i = width * height; i--; zbuffer[i] = -std::numeric_limits<float>::max());
for (int i = 0; i < model->nfaces(); i++)
{
// ..
if (intensity > 0)
{
triangle(screen_coords, zbuffer,image, TGAColor(intensity * 255, intensity * 255, intensity * 255, 255));
}
}
image.flip_vertically();
image.write_tga_file("output.tga");
delete model;
return 0;
}
结果如下,十分完美!