文章目录
前言
上节课我们已经可以绘制直线了,但是还不够,模型都是由三角面构成的,所以这次我们要绘制三角面,以及剔除我们看不到的面。
Dmitry V. Sokolov
gamma公式
Games101-光栅化(三角形的离散化)
Games101-着色(插值、高级纹理映射)
本系列主要参考Gams101的相关课程和课件以及Dmitry V. Sokolov的软渲染教程,详细教程可阅读参考链接。推荐看闫老师的课程,因为讲非常清楚,浅显易懂。
一、绘制三角面
绘制一个三角面其实比绘制一条直线还要简单很多,原理也很简单,判断一个点是否在三角形内,是就绘制,不是则跳过。
1.1 判断点是否在三角形内-叉乘法
叉乘是判断点是否在三角形内最简单的方法。 将三角形三个顶点以及我们要判定的点,互相连接成向量,然后通过比较叉乘出来的三条法向量的z值
的方向性来判断是否在三角形内。若方向性都相同,那么点在三角形内,若有一条不相同,那么不在三角形内。
struct triangle
{
triangle() {};
triangle(Vec3f _0, Vec3f _1, Vec3f _2) : p0(_0), p1(_1), p2(_2) {};
Vec3f vec[3];
Vec3f& p0 = vec[0];
Vec3f& p1 = vec[1];
Vec3f& p2 = vec[2];
};
bool inside(const triangle& tri, const Vec3f& point)
{
Vec3f AB = tri.p1 - tri.p0;
Vec3f BC = tri.p2 - tri.p1;
Vec3f CA = tri.p0 - tri.p2;
Vec3f AP = point - tri.p0;
Vec3f BP = point - tri.p1;
Vec3f CP = point - tri.p2;
float a = (AB ^ AP).z;
float b = (BC ^ BP).z;
float c = (CA ^ CP).z;
if ((a < 0 && b < 0 && c < 0) || (a > 0 && b > 0 && c > 0)) return true;
return false;
}
1.2 判断点是否在三角形内-重心法
三角形的重心可以用来插值点,UV,颜色,法向等等三角形的相关图像属性…
由于三角形重心的可以用来插值这个特性,所以我建议利用重心来判断点是否在三角形中,同时也可以利用重心插值出其他我们要的属性。
三角形重心公式:
V
=
α
V
A
+
β
V
B
+
γ
V
C
V = \alpha V_A + \beta V_B + \gamma V_C
V=αVA+βVB+γVC
然后同时闫老师告诉我们可以利用面积之比可以计算三个变量的值。
那么就可以写出对应的代码了。
Vec3f barycentricCoord(const triangle& tri, const Vec2f& point)
{
float alpha = (-(point.x - tri.p1.x) * (tri.p2.y - tri.p1.y) + (point.y - tri.p1.y) * (tri.p2.x - tri.p1.x)) / (-(tri.p0.x - tri.p1.x) * (tri.p2.y - tri.p1.y) + (tri.p0.y - tri.p1.y) * (tri.p2.x - tri.p1.x));
float beta = (-(point.x - tri.p2.x) * (tri.p0.y - tri.p2.y) + (point.y - tri.p2.y) * (tri.p0.x - tri.p2.x)) / (-(tri.p1.x - tri.p2.x) * (tri.p0.y - tri.p2.y) + (tri.p1.y - tri.p2.y) * (tri.p0.x - tri.p2.x));
float gama = 1 - alpha - beta;
return Vec3f(alpha, beta, gama);
}
这里要特别注意的是,返回的三个值不能随便调换位置,它们是一一对应到公式上的,否则会出现图像bug。例如下面眼睛周围。
2.1 包围盒
能判断点是否在三角内还不够,我们还得知道三角形的范围,不然难道要遍历所有的像素么,那这也太浪费时间了。
获取三角形的范围也很简单,只需要知道三角形的水平的最大最小值和垂直的最大最小值即可。
int x_boundingBoxMax = (int)(std::max((std::max(tri.p0.x, tri.p1.x)), tri.p2.x));
int x_boundingBoxMin = (int)(std::min((std::min(tri.p0.x, tri.p1.x)), tri.p2.x));
int y_boundingBoxMax = (int)(std::max((std::max(tri.p0.y, tri.p1.y)), tri.p2.y));
int y_boundingBoxMin = (int)(std::min((std::min(tri.p0.y, tri.p1.y)), tri.p2.y));
到这里其实是可以渲染出我们的mesh了,但是还不够好,因为我们把背面的三角形也给渲染出来了,这个是我们不需要的。我们得想办法剔除。
3.1 剔除三角形
画家算法,这个算法是从远到近,层层叠加的。听上去好像可行,但是还是存在着判断远近问题,以及无法解决三角面相交的情况的。有兴趣的直接搜索,这里不详细提了。
zbuffer,zbuffer是一种更加常用的剔除三角形的方式。通过记录要绘制点的z值来判断是否绘制该点到像素的方法。详细也直接搜索吧。
这里要解决的是如何计算除三角形顶点外的z值
,例如三角形内部的点如何去计算?答案就在上面,上面我们说了,三角形的重心是可以插值出很多属性的,其中当然包括z值了
float zOrder = A.z * bc.x + B.z * bc.y + C.z * bc.z;
到此我们终于绘制出来我们的模型了!!
二、完整代码
Vec3f barycentricCoord(const triangle& tri, const Vec2f& point)
{
float alpha = (-(point.x - tri.p1.x) * (tri.p2.y - tri.p1.y) + (point.y - tri.p1.y) * (tri.p2.x - tri.p1.x)) / (-(tri.p0.x - tri.p1.x) * (tri.p2.y - tri.p1.y) + (tri.p0.y - tri.p1.y) * (tri.p2.x - tri.p1.x));
float beta = (-(point.x - tri.p2.x) * (tri.p0.y - tri.p2.y) + (point.y - tri.p2.y) * (tri.p0.x - tri.p2.x)) / (-(tri.p1.x - tri.p2.x) * (tri.p0.y - tri.p2.y) + (tri.p1.y - tri.p2.y) * (tri.p0.x - tri.p2.x));
float gama = 1 - alpha - beta;
return Vec3f(alpha, beta, gama);
}
void drawTriangle(const triangle& tri,const TGAColor& color,TGAImage &image)
{
int x_boundingBoxMax = (int)(std::max((std::max(tri.p0.x, tri.p1.x)), tri.p2.x));
int x_boundingBoxMin = (int)(std::min((std::min(tri.p0.x, tri.p1.x)), tri.p2.x));
int y_boundingBoxMax = (int)(std::max((std::max(tri.p0.y, tri.p1.y)), tri.p2.y));
int y_boundingBoxMin = (int)(std::min((std::min(tri.p0.y, tri.p1.y)), tri.p2.y));
for (int x = x_boundingBoxMin; x <= x_boundingBoxMax; x++)
{
for (int y = y_boundingBoxMin; y <= y_boundingBoxMax; y++)
{
Vec3f bc = barycentricCoord(tri, Vec2f(x, y));
if (bc.x < 0 || bc.y < 0 || bc.z < 0) continue;// 跳过不在三角形内的点
float zOrder = tri.p0.z * bc.x + tri.p1.z * bc.y + tri.p2.z * bc.z;
int index = x + y * width;
if (depthBuffer[index] < zOrder)
{
depthBuffer[index] = zOrder;
image.set(x, y, color);
}
}
}
}
Vec3f world2screen(Vec3f v)
{
return Vec3f((v.x + 1.f) * width / 2.f, (v.y + 1.f) * height / 2.f, v.z);
}
int main(int argc, char** argv) {
TGAImage image(width, height, TGAImage::RGB);
depthBuffer = new float[width * height];
for (int i = width * height; i--; depthBuffer[i] = -std::numeric_limits<float>::max());
if (2==argc) {
model = new Model(argv[1]);
} else {
model = new Model("obj/african_head.obj");
}
triangle tri;
Vec3f worldCoords[3];
Vec3f lightDir(0.f, 0.f, -1.f);
// draw line
/*for (int i = 0; i < model->nfaces(); i++) {
std::vector<int> face = model->face(i);
for (int j = 0; j < 3; j++)
{
tri.vec[j] = world2screen(model->vert(face[j]));
}
drawLine(tri, white, image);
}*/
// drawTriangle
for (int i = 0; i < model->nfaces(); i++)
{
std::vector<int> face = model->face(i);
for (int j = 0; j < 3; j++)
{
tri.vec[j] = world2screen(model->vert(face[j]));
worldCoords[j] = model->vert(face[j]);
}
Vec3f vertexNormal = ((worldCoords[2] - worldCoords[0]) ^ (worldCoords[1] - worldCoords[0])).normalize();
float indensity = std::max(0.f,vertexNormal * lightDir);
drawTriangle(tri, TGAColor(indensity * 255, indensity * 255, indensity * 255, 255), image);
}
image.flip_vertically(); // i want to have the origin at the left bottom corner of the image
image.write_tga_file("output.tga");
delete model;
delete depthBuffer;
return 0;
}