深度缓存(Z buffer)
理论上我们可以画出所有的三角形,而不丢弃任何三角形。如果我们按照正确的顺序绘制三角形(从后往前)。那么前面的将会遮挡住后面的部分,这称为画家算法。
一个简单场景的渲染
假设现在有一个场景中包含了三个三角形,摄像机将从上往下看过去,我们将彩色三角形投影到白色屏幕上。
渲染结果如下:
如果现在我们提问,蓝色三角形在红色三角形之前还是之后,我们无法作答,这时画家算法不起作用,我们当然可以将三角形一分为二,一半在红色前面,一半在红色后面,但是对于大量的三角形,这会造成很大的计算资源开销。
一种更简单的方法:Y-buffer
我们首先对场景进行降维,仅考虑黄色切面的渲染情况。
现在在白色屏幕渲染得到的结果应该一条线段(即单排的像素点)。
现在我们的场景已经降维成二维,因此我们可以用line()函数简单的绘制出场景,场景的侧面图如下所示:
{
TGAImage scene(width, height, TGAImage::RGB);
// scene "2d mesh"
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.write_tga_file("scene.tga");
}
场景的侧视图如下所示:
对于这个场景,我们需要定义一个y-buffer,用于存储最上方的线段颜色,具体代码如下:
void yBuffer(Vec2i v0, Vec2i v1, int* buffer, TGAImage& image, TGAColor color) {
//从x小的开始
if (v0.x > v1.x)
std::swap(v0, v1);
for (int i = v0.x; i <= v1.x; i++) {
//遍历每一段线条,
float t = (i - v0.x) / float(v1.x - v0.x);
int y = (1.- t) * v0.y + t * v1.y;
if (buffer[i] < y) {
buffer[i] = y;
image.set(i, 0, color);
}
}
}
运行代码如下:
void yBuffer(Vec2i v0, Vec2i v1, int* buffer, TGAImage& image, TGAColor color) {
//从x小的开始
if (v0.x > v1.x)
std::swap(v0, v1);
for (int i = v0.x; i <= v1.x; i++) {
float t = (i - v0.x) / float(v1.x - v0.x);
int y = (1.- t) * v0.y + t * v1.y;
if (buffer[i] < y) {
buffer[i] = y;
image.set(i, 0, color);
}
}
}
int main() {
TGAImage scene(width, height, TGAImage::RGB);
// scene "2d mesh"
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);
// screen line
line(Vec2i(10, 10), Vec2i(790, 10), scene, white);
scene.write_tga_file("scene.tga");
TGAImage render(width, 16, TGAImage::RGB);
int ybuffer[width]{};
for (int i = 0; i < width; i++)
ybuffer[i] = INT_MIN;
yBuffer(Vec2i(20, 34), Vec2i(744, 400), ybuffer, render, red);
yBuffer(Vec2i(120, 434), Vec2i(444, 400), ybuffer, render, green);
yBuffer(Vec2i(330, 463), Vec2i(594, 200), ybuffer, render, blue);
//只有一行像素很难看清楚,复制几行显示清晰一些
for (int i = 0; i < width; i++)
for (int j = 0; j < 16; j++)
render.set(i, j, render.get(i, 0));
render.write_tga_file("render.tga");
}
迭代v0.x和v1.x之间的所有x坐标,并计算线段的相应y坐标。然后我检查我们在数组ybuffer中得到的当前x索引。如果当前的y值比ybuffer中的值更接近相机,那么我将其绘制在屏幕上并更新ybuffer。
绘制到3D平面
对于3D平面我们也可以用同样的方式建立一个z-buffer,由于在三维空间,那么z-buffer的长度应该为屏幕的面积,即:
int *zbuffer = new int [width * height];
这里我们将二维的zbuffer展平成一维,zbuffer对应的索引值与平面的坐标值对应关系如下:
int x = index % width;
int y = index / width;
然后我们将遍历所有三角形,更新zbuffer,现在唯一困难的点在于我们怎么确定z值呢,在二维中,我们使用下面的方式:
int y = v0.y*(1.-t) + v1.y*t;
那么对于二维线段来说,t就是线段上的重心坐标,那么在三角形中,同样的也是重心坐标,我们的想法是采用三角形光栅化的重心坐标版本,对于我们想要绘制的每个像素,只需将其重心坐标乘以我们光栅化的三角形顶点的z值。代码如下:
我们将画线、渲染三角形以及判断重心坐标点代码抽离出来得到line.cpp
#include "line.h"
/*
* 绘制两点间的直线
*/
void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {
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) {
image.set(y, x, color);
}
else {
image.set(x, y, color);
}
error2 += derror2;
if (error2 > dx) {
y += (y1 > y0 ? 1 : -1);
error2 -= dx * 2;
}
}
}
/*
* 重载line的Vec2i版本
*/
void line(Vec2i v0, Vec2i v1, TGAImage& image, TGAColor color)
{
line(v0.x, v0.y, v1.x, v1.y, image, color);
}
/*
* 获得三角形的重心坐标
*/
Vec3f barycentric(Vec2i* pts, Vec2i P) {
//uAB + vAC + PA = 0
//将上述向量的x分量和y分量拆开,并且做叉积,可以得到[u, v, 1](还需要归一化)
Vec3f u = Vec3f(pts[1].x - pts[0].x, pts[2].x - pts[0].x, pts[0].x - P.x) ^ Vec3f(pts[1].y - pts[0].y, pts[2].y - pts[0].y, pts[0].y - P.y);
if (std::abs(u.z) < 1) return Vec3f(-1, 1, 1);
//返回[u,v,1]
return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
}
//改进的三角形渲染算法
void drawTriangle(Vec2i* pts, TGAImage& image, TGAColor color)
{
//确定三角形的包围框
Vec2i bboxmin{ image.height() - 1, image.width() - 1 };
Vec2i bboxmax{ 0 ,0 };
Vec2i bboxclamp{ image.height() - 1, image.width() - 1 };
//遍历三角形顶点,确定四边形边框
for (int i = 0; i < 3; i++)
{
bboxmin.x = std::max(0, std::min(bboxmin.x, pts[i].x));
bboxmin.y = std::max(0, std::min(bboxmin.y, pts[i].y));
bboxmax.x = std::max(0, std::max(bboxmax.x, pts[i].x));
bboxmax.y = std::max(0, std::max(bboxmax.y, pts[i].y));
}
//遍历框内像素,确定像素点是否在三角形内部(使用重心坐标判断)
for (int i = bboxmin.x; i <= bboxmax.x; i++) {
for (int j = bboxmin.y; j <= bboxmax.y; j++) {
Vec3f bc_screen = barycentric(pts, { i,j });
if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
image.set(i, j, color);
}
}
}
Vec3f barycentric(Vec3f* pts, Vec3f P) {
//uAB + vAC + PA = 0
//将上述向量的x分量和y分量拆开,并且做叉积,可以得到[u, v, 1](还需要归一化)
Vec3f u = Vec3f(pts[2].x - pts[0].x, pts[1].x - pts[0].x, pts[0].x - P.x) ^ Vec3f(pts[2].y - pts[0].y, pts[1].y - pts[0].y, pts[0].y - P.y);
//注意这里变成了float
if (std::abs(u.z) <= 1e-2) return Vec3f(-1, 1, 1);
//返回[u,v,1]
return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
}
/*
* 带z-buffer的三角形渲染算法
*/
void drawTriangle(Vec3f* pts, float* zbuffer, TGAImage& image, TGAColor color)
{
//确定三角形的包围框
Vec2f bboxmin { std::numeric_limits<float>::max(), std::numeric_limits<float>::max() };
Vec2f bboxmax { std::numeric_limits<float>::min(), std::numeric_limits<float>::min() };
Vec2f bboxclamp{ float(image.width() - 1), float(image.height() - 1) };
//遍历三角形顶点,确定四边形边框
for (int i = 0; i < 3; i++)
{
bboxmin.x = std::max(0.f, std::min(bboxmin.x, pts[i].x));
bboxmin.y = std::max(0.f, std::min(bboxmin.y, pts[i].y));
bboxmax.x = std::min(bboxclamp.x, std::max(bboxmax.x, pts[i].x));
bboxmax.y = std::min(bboxclamp.y, std::max(bboxmax.y, pts[i].y));
}
//遍历框内像素,确定像素点是否在三角形内部(使用重心坐标判断)
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 bc_screen = barycentric(pts, p);
//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坐标值
p.z = 0;
for (int i = 0; i < 3; i++) p.z += pts[i].z * bc_screen[i];
//更新z-buffer,并且只有更新的时候,需要渲染更近的像素颜色值
if (zbuffer[int(p.x * 800 + p.y)] < p.z) {
zbuffer[int(p.x * 800 + p.y)] = p.z;
image.set(p.x, p.y, color);
}
}
}
}
main.cpp
#include "line.h"
#include "model.h"
#include "TGAImage.h"
int main() {
int width = 800;
int height = 800;
TGAImage image(width, height, TGAImage::RGB);
Vec3f lightVec{ 0, 0, 1 };
Model* head = new Model("./obj/african_head/african_head.obj");
//zbuffer的初始化也会对渲染结果产生影响,这里不能使用FLT_MIN
/*float* zbuffer = new float[width * height];
for (int i = width * height; i--; zbuffer[i] = FLT_MIN);*/
float* zbuffer = new float[width * height];
for (int i = width * height; i--; zbuffer[i] = -std::numeric_limits<float>::max());
for (int i = 0; i < head->nfaces(); i++)
{
std::vector<int> face = head->face(i);
//顶点世界坐标
Vec3f world_coords[3]{};
//顶点屏幕坐标
Vec3f screen_coords[3]{};
for (int j = 0; j < 3; j++) {
world_coords[j] = head->vert(face[j]);
screen_coords[j] = Vec3f(int((world_coords[j].x + 1.) * width / 2.), int((world_coords[j].y + 1.) * height / 2.), world_coords[j].z);
}
//三角形法线向量,三角形两边叉乘得到法线向量
Vec3f n = (world_coords[0] - world_coords[1]) ^ (world_coords[1] - world_coords[2]);
n.normalize();
float intensity = lightVec * n;
Vec3f* p_coords = &screen_coords[0];
drawTriangle(p_coords, zbuffer, image, TGAColor{ std::uint8_t(intensity * 255), std::uint8_t(intensity * 255), std::uint8_t(intensity * 255), 255 });
}
image.write_tga_file("render_result.tga");
delete head;
}
渲染结果:
添加纹理
添加纹理的过程非常简单,我们读取obj文件,其中vt行代表每个三角形顶点对应二维纹理的坐标,在三角形渲染过程中,我们可以直接使用重心坐标,三角形内部的点可以看作三个顶点纹理插值的结果。代码如下:
/*
* 带z-buffer,以及纹理的三角形渲染算法
*/
void drawTriangle(Vec3f* pts, Vec2f* tex_coords, float* zbuffer, TGAImage& image, TGAImage& texture)
{
//确定三角形的包围框
Vec2f bboxmin{ std::numeric_limits<float>::max(), std::numeric_limits<float>::max() };
Vec2f bboxmax{ std::numeric_limits<float>::min(), std::numeric_limits<float>::min() };
Vec2f bboxclamp{ float(image.width() - 1), float(image.height() - 1) };
//遍历三角形顶点,确定四边形边框
for (int i = 0; i < 3; i++)
{
bboxmin.x = std::max(0.f, std::min(bboxmin.x, pts[i].x));
bboxmin.y = std::max(0.f, std::min(bboxmin.y, pts[i].y));
bboxmax.x = std::min(bboxclamp.x, std::max(bboxmax.x, pts[i].x));
bboxmax.y = std::min(bboxclamp.y, std::max(bboxmax.y, pts[i].y));
}
//遍历框内像素,确定像素点是否在三角形内部(使用重心坐标判断)
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 bc_screen = barycentric(pts, p);
//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坐标值
p.z = 0;
for (int i = 0; i < 3; i++) p.z += pts[i].z * bc_screen[i];
//插值计算纹理坐标
Vec2f p_tex{};
for (int i = 0; i < 3; i++) {
p_tex.x += tex_coords[i].x * bc_screen[i] * texture.height();
p_tex.y += tex_coords[i].y * bc_screen[i] * texture.width();
}
TGAColor color = texture.get(p_tex.x, p_tex.y);
//更新z-buffer,并且只有更新的时候,需要渲染更近的像素颜色值
if (zbuffer[int(p.x * 800 + p.y)] < p.z) {
zbuffer[int(p.x * 800 + p.y)] = p.z;
image.set(p.x, p.y, color);
}
}
}
}
#include "line.h"
#include "model.h"
#include "TGAImage.h"
int main() {
int width = 800;
int height = 800;
TGAImage image(width, height, TGAImage::RGB);
TGAImage TextureImage;
//纹理图像
TextureImage.read_tga_file("./obj/african_head/african_head_diffuse.tga");
Model* head = new Model("./obj/african_head/african_head.obj");
//zbuffer的初始化也会对渲染结果产生影响,这里不能使用FLT_MIN
float* zbuffer = new float[width * height];
for (int i = width * height; i--; zbuffer[i] = -std::numeric_limits<float>::max());
for (int i = 0; i < head->nfaces(); i++)
{
std::vector<int> face = head->face(i);
std::vector<int> face_texture = head->f_texture(i);
//顶点世界坐标
Vec3f world_coords[3]{};
//顶点屏幕坐标
Vec3f screen_coords[3]{};
Vec2f texture[3]{};
for (int j = 0; j < 3; j++) {
world_coords[j] = head->vert(face[j]);
screen_coords[j] = Vec3f(int((world_coords[j].x + 1.) * width / 2.), int((world_coords[j].y + 1.) * height / 2.), world_coords[j].z);
texture[j] = head->texture(face_texture[j]);
}
//三角形法线向量,三角形两边叉乘得到法线向量
Vec3f n = (world_coords[0] - world_coords[1]) ^ (world_coords[1] - world_coords[2]);
Vec3f* p_coords = &screen_coords[0];
TGAColor TexColor;
drawTriangle(p_coords, texture, zbuffer, image, TextureImage);
}
image.write_tga_file("render_result_texture2.tga");
delete head;
}
得到结果如下: