画一个三角形有多种方式,但究其本质,还是要去判断一个点是否在三角形内
判断点是否在三角形内有很多种方法:
将三角形三个点由y值进行排序,从高到低,形成三条边,这时我们可以考虑三角形的左右,然后同一y值得左右两个点确定过一条直线,在这两个点之间的直线都进行填充,就得到了三角形的面片。
面积法:将三个点其中两个组合得与点连接形成三角形,计算各三角形面积和,看两者是否相等。面积计算。三边长+海伦公式,效率不高
重心坐标方法,通过三角形的三个顶点,我们可以得出给定的某一点的重心坐标,如果这个点的重心坐标(1-u-v, u, v)三通道均大于0,那么就可以认为该点在三角形内;
向量叉乘算法/顺时针逆时针判定法,其实究其本质我觉得跟重心坐标很像,但是这种方法并不需要准确求出重心坐标,而是通过判断将三角形边形成的逆时针方向向量与p点到向量起始点的向量叉乘,判断三者正负即可判断。
这里一般是采用重心插值算法,虽然计算中心坐标用来判断点在三角形内有点大材小用,但因为只需要一次叉乘就可求出重心坐标。并且该重心坐标还可以用于对顶点属性插值,而这是现代渲染管线都需要的。
数学推导:
这部分参考day3:画一个三角形 | 微信公众号@卤蛋实验室 (supercodepower.com)的推导,人家已经写的很好了。
总结是我们仅需要进行一次叉乘就可以得到我们想要数据了。此外这种方法对并行度很高的gpu来说很友好
代码如下:
Vec3fbarycentric(Vec3i* pts, Vec2i P){
Vec3iv1 = Vec3i(pts[1].x - pts[0].x, pts[2].x - pts[0].x, pts[0].x - P.x);
Vec3iv2 = Vec3i(pts[1].y - pts[0].y, pts[2].y - pts[0].y, pts[0].y - P.y);
Vec3iv = v1 ^ v2;
if(std::abs(v.z) < 1) return Vec3f(-1, 1, 1);
returnVec3f(1.0f - (v.x + v.y) / (float)v.z, v.x / (float)v.z, v.y / (float)v.z);
}
voidtriangle_barycentric(Vec3i* pts, std::vector<int>& zBuffer,TGAImage& image, const TGAColor& color) {
//计算bounding box
Vec2ibbox_min = Vec2i(width - 1, height - 1);
Vec2ibbox_max = Vec2i(0, 0);
for(int i = 0; i < 3; i++ ) {
bbox_max.x= std::min(width - 1, std::max(bbox_max.x, pts[i].x));
bbox_max.y= std::min(height - 1, std::max(bbox_max.y, pts[i].y));
bbox_min.x= std::max(0, std::min(bbox_min.x, pts[i].x));
bbox_min.y= std::max(0, std::min(bbox_min.y, pts[i].y));
}
//遍历包围盒,判定在三角内的被着色
for(int i = bbox_min.x; i <= bbox_max.x; i++) {
for(int j = bbox_min.y; j <= bbox_max.y; j++) {
Vec3fbaryc = barycentric(pts, Vec2i(i, j));
if(baryc.x >= 0 && baryc.y >= 0 && baryc.z >= 0) {
intdepth = baryc.x * pts[0].z + baryc.y * pts[1].z + baryc.z * pts[2].z;
if(depth > zBuffer[i + j * width - 1]) {
image.set(i,j, color);
zBuffer[i+ j * width - 1] = depth;
}
}
}
}
}
//让我们再加上渲染模型的函数
voidrenderModel(Model& model, TGAImage& img) {
std::vector<int>zBuffer(width * height, INT32_MIN);
for(int i = 0; i < model.nfaces(); i++) {
autoface = model.face(i);
Vec3fworldspace_coord[3];
Vec3iscreenspace_coord[3];
for(int j = 0; j < face.size(); j++) {
//这里将存储在本地空间的模型(这里存的ndc)坐标变换到屏幕坐标,做了个简单的视口变换
worldspace_coord[j]= model.vert(face[j]);
screenspace_coord[j]= Vec3i((worldspace_coord[j].x + 1) * width / 2, (worldspace_coord[j].y + 1) *height / 2, (worldspace_coord[j].z + 1)* width / 2);
}
//三角形随机颜色绘制
//triangle_barycentric(pts,img, TGAColor(rand() % 255, rand() % 255, rand() % 255, 255));
//计算三角形法线
Vec3fnormal = ((worldspace_coord[1] - worldspace_coord[0]) ^ (worldspace_coord[2] -worldspace_coord[0])).normalize();
float intensity = -normal * lightDir; //简易的漫反射计算
//绘制三角形
if(intensity> 0)
triangle_barycentric(screenspace_coord,zBuffer, img, TGAColor(intensity * 255, intensity * 255, intensity * 255,255));
}
}
注意这里用到了一个简单的正交投影+视口变换,用一个简易的点乘代表简单的漫反射计算。可以看到嘴巴怪怪的,下一章会解决。
另外解释下这里用的是flatshading,也就是每个三角形面片计算了一个法线,用来着色,在这个三角形面片内都是同一个颜色,着色频率很低。
随机颜色三角形面片
Flatshading