画空心三角形
根据之前的画线算法,可以很简单画出一个空心三角形,对三角形三个顶点,按顺序分别首尾画连线就可以
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);
}
// ...
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);
填充三角形
好的三角形算法需要具备:
- 简单并快速
- 对称的,图片不依赖于传给绘图函数的顶点的顺序(顺时针画和逆时针画是一样的三角形)
- 如果两个三角形有共同顶点,由于光栅化舍入,他们之间应该没有孔
传统方法是扫线法:
- 对三角形的顶点按y排序
- 同时栅格化三角形左右两边
- 在左右边界点间画线
三个点t0,t1,t2按y排序后,以中间顶点t1做一条水平线可以将三角形分为上下两个部分,都是三角形,并且以水平横线分割。这样可以对上下两个三角形做相同的画横线填充处理
填充横线是按y轴递增,根据y轴递增值求对应左右两个边界的x值(线性求值)
注意三角形里画横线自己实现,不要调用line函数,因为横线不需要去求多余的斜率增长那些,只要x轴水平递增,y不变
按自己的思路实现了一下
void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage& image, TGAColor color, bool isSolid) {
if (isSolid) {
if (t0.y > t1.y) std::swap(t0, t1);
if (t0.y > t2.y) std::swap(t0, t2);
if (t1.y > t2.y) std::swap(t1, t2);
for (int y = t0.y; y < t1.y; y++) {
float ratioA = (y - t0.y) / (float)(t2.y - t0.y + 1);
float ratioB = (y - t0.y) / (float)(t1.y - t0.y + 1);
Vec2i tA = t0 + (t2 - t0) * ratioA;
Vec2i tB = t0 + (t1 - t0) * ratioB;
if (tA.x < tB.x) {
for (int x = tA.x; x <= tB.x; x++) {
image.set(x, tA.y, color);
}
}
else {
for (int x = tB.x; x <= tA.x; x++) {
image.set(x, tA.y, color);
}
}
}
for (int y = t1.y; y <= t2.y; y++) {
float ratioA = (y - t0.y) / (float)(t2.y - t0.y + 1);
float ratioB = (y - t1.y) / (float)(t2.y - t1.y + 1);
Vec2i tA = t0 + (t2 - t0) * ratioA;
Vec2i tB = t1 + (t2 - t1) * ratioB;
if (tA.x < tB.x) {
for (int x = tA.x; x <= tB.x; x++) {
image.set(x, tA.y, color);
}
}
else {
for (int x = tB.x; x <= tA.x; x++) {
image.set(x, tA.y, color);
}
}
}
image.set(t2.x, t2.y, color);
}
else {
line(t0.x, t0.y, t1.x, t1.y, image, color);
line(t1.x, t1.y, t2.x, t2.y, image, color);
line(t2.x, t2.y, t0.x, t0.y, image, color);
}
}
和官方代码对比了一下,逻辑是一样的
void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
if (t0.y==t1.y && t0.y==t2.y) return; // I dont care about degenerate triangles
// sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!)
if (t0.y>t1.y) std::swap(t0, t1);
if (t0.y>t2.y) std::swap(t0, t2);
if (t1.y>t2.y) std::swap(t1, t2);
int total_height = t2.y-t0.y;
for (int i=0; i<total_height; i++) {
bool second_half = i>t1.y-t0.y || t1.y==t0.y;
int segment_height = second_half ? t2.y-t1.y : t1.y-t0.y;
float alpha = (float)i/total_height;
float beta = (float)(i-(second_half ? t1.y-t0.y : 0))/segment_height; // be careful: with above conditions no division by zero here
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i B = second_half ? t1 + (t2-t1)*beta : t0 + (t1-t0)*beta;
if (A.x>B.x) std::swap(A, B);
for (int j=A.x; j<=B.x; j++) {
image.set(j, t0.y+i, color); // attention, due to int casts t0.y+i != A.y
}
}
}
triangle(t0[0], t0[1], t0[2], image, red, false);
triangle(t1[0], t1[1], t1[2], image, white, true);
triangle(t2[0], t2[1], t2[2], image, green, true);
使用Bounding Box填充三角形
找到三角形的包围盒,及对比三个顶点的minX,minY,maxX,maxY。
遍历包围盒里的每个点,是否在三角形内,确定是否填充颜色
伪代码
triangle(vec2 points[3]) {
vec2 bbox[2] = find_bounding_box(points);
for (each pixel in the bounding box) {
if (inside(points, pixel)) {
put_pixel(pixel);
}
}
}
找包围盒,只需要三角形三个顶点分别对比获取最大最小的x,y就行。
判断点是否在三角形内,官方用的是重心坐标的性质判断
Vec3f barycentric(Vec2i *pts, Vec2i P) {
Vec3f u = Vec3f(pts[2][0]-pts[0][0], pts[1][0]-pts[0][0], pts[0][0]-P[0])^Vec3f(pts[2][1]-pts[0][1], pts[1][1]-pts[0][1], pts[0][1]-P[1]);
/* `pts` and `P` has integer value as coordinates
so `abs(u[2])` < 1 means `u[2]` is 0, that means
triangle is degenerate, in this case return something with negative coordinates */
if (std::abs(u.z)<1) return Vec3f(-1,1,1);
return Vec3f(1.f-(u.x+u.y)/u.z, u.y/u.z, u.x/u.z);
}
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++) {
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::min(clamp.x, std::max(bboxmax.x, pts[i].x));
bboxmax.y = std::min(clamp.y, std::max(bboxmax.y, pts[i].y));
}
Vec2i 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);
if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue;
image.set(P.x, P.y, color);
}
}
}
我这里直接用的是向量叉乘判断
bool insideTriangle(Vec2i* pts, int x, int y) {
Vec2f a = Vec2f(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
Vec2f b = Vec2f(pts[2].x - pts[1].x, pts[2].y - pts[1].y);
Vec2f c = Vec2f(pts[0].x - pts[2].x, pts[0].y - pts[2].y);
Vec2f pa = Vec2f(x - pts[0].x, y - pts[0].y);
Vec2f pb = Vec2f(x - pts[1].x, y - pts[1].y);
Vec2f pc = Vec2f(x - pts[2].x, y - pts[2].y);
bool flag = pa.x * a.y - pa.y * a.x < 0;
if (flag != pb.x * b.y - pb.y * b.x < 0) return false;
if (flag != pc.x * c.y - pc.y * c.x < 0) return false;
return true;
}
void triangle(Vec2i* pts, TGAImage& image, TGAColor color) {
int minX = image.get_width() - 1;
int minY = image.get_height() - 1;
int maxX = 0;
int maxY = 0;
for (int i = 0; i < 3; i++) {
minX = std::min(minX, pts[i].x);
maxX = std::max(maxX, pts[i].x);
minY = std::min(minY, pts[i].y);
maxY = std::max(maxY, pts[i].y);
}
minX = std::max(0, minX);
maxX = std::min(maxX, image.get_width() - 1);
minY = std::max(0, minY);
maxY = std::min(maxY, image.get_height() - 1);
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
if (insideTriangle(pts, x, y)) {
image.set(x, y, color);
}
}
}
}
得到的效果是一样的
平面阴影渲染
用随机颜色填充模型的三角形
用黑白色做为亮度来代替随机色。
定义一束平行光源,水平朝-z的方向
根据叉乘求出模型每个三角形的法线向量。根据光源方向和法线的夹角求出光照强度
根据点乘定义,光源向量点乘三角形法线向量,越接近0,说明光源越和法线垂直,则越和三角形平面平行,三角形越暗。反之越接近于1,越亮。
(类似布林冯漫反射方程原理,只是
k
d
k_d
kd系数为白色,忽略了光强随距离的减弱)
根据原理实现了一下代码
Model* model = new Model("obj/african_head.obj");
float maxNum = model->getMaxNum();
Vec3f lightDir(0, 0, -1);
for (int i = 0; i < model->nfaces(); i++) {
std::vector<int> face = model->face(i);
Vec2i vp[3];
Vec3f vf[3];
for (int j = 0; j < 3; j++) {
Vec3f v0 = model->vert(face[j]);
vp[j] = Vec2i((v0.x / maxNum + 1.) * 800 / 2., (v0.y / maxNum + 1.) * 800 / 2.);
vf[j] = v0;
}
//triangle(vp, image, TGAColor(rand() % 255, rand() % 255, rand() % 255, 255));
Vec3f n = (vf[1] - vf[0]) ^ (vf[2] - vf[0]);
n.normalize();
float I = n * lightDir;
if (I < 0) {
triangle(vp, image, TGAColor(-I * 255, -I * 255, -I * 255, 255));
}
注意我这里和官方代码不同的是
I
<
0
I < 0
I<0, 说明
>
0
>0
>0 时三角形背向光源方向,直接剔除
并且颜色为
−
I
×
255
-I{\times}255
−I×255
因为我求的三角形是按逆时针排序才法线朝外
Vec3f n = (vf[1] - vf[0]) ^ (vf[2] - vf[0]);
官方是
Vec3f n = (world_coords[2]-world_coords[0])^(world_coords[1]-world_coords[0]);
正好相反
效果如下
发现三角形边缘有黑边显示,怀疑是之前的判断点是否在三角形内,只有
<
0
<0
<0,没有
≤
0
{\leq}0
≤0,所以在三角形边上的点被忽略了,改下叉乘结果判断
bool flag = pa.x * a.y - pa.y * a.x <= 0;
if (flag != pb.x * b.y - pb.y * b.x <= 0) return false;
if (flag != pc.x * c.y - pc.y * c.x <= 0) return false;
最后正常了
其他模型效果
项目跟随练习代码地址