这一次我们将使用光栅化与Z-Buffer来填充我们之前所画的线框模型中的三角形面。
光栅化
我们模型经过投影变换后的仍然只是几个顶点的位置信息而已,如果我们画三角形,想要的不仅仅是三个顶点,还想要确定填充三角形所需要的所有像素的信息(连续信息描述变换为离散信息描述),总结来说,光栅化就是确定一个几何图元所覆盖的所有像素及其信息。
如下图所示:
接下来我们将实现基于线扫描的光栅化三角形算法。
首先:
三角形一共大致分为以下4种:
其实只有3.4两种,1.2是特殊的3.4罢了。
而且3.4可以看作1.2组成的那么我们先光栅化情况1.2,后考虑3.4
以下参考:http://blog.csdn.net/cppyin/article/details/6232453
http://www.cnblogs.com/bubbler/p/3443982.html
对于情况2.平底三角形
光栅化此三角形就是从上往下画横线。在图中取任意一光栅化直线,这条直线左边的端点x值为XL,右边的为XR。y值就不用考虑了,因为这些线是从上往下画的,所以y就是从y0一直++,直到y1或者y2。
所以实行起来就是每次增加一个y就需要计算一下XL与XR,然后调用画线算法来画线。
怎么求XL和XR呢?
直线有很多种形式可以表示,因为我们现在知道顶点的坐标,所以最直观的表示形式就是两点式:
对于已知直线上的两点(x1,y1)和(x2,y2)有:
(y-y1) / (y2-y1)= (x-x1) / (x2-x1) (相似三角形)
因为y已知,我们变换一下公式,表示为x的值为:
x = (y-y1) *(x2-x1) / (y2-y1) + x1
Code:
Void Draw_Triangle(intx1, int y1, int x2, int y2, int x3, int y3, UINT color)
// 画实心平底三角形
{
for (int y = y1; y <= y2; ++y)
{
int xs, xe;
xs = (y - y1) * (x2 - x1) / (y2 -y1) + x1 + 0.5;
xe = (y - y1) * (x3 - x1) / (y3 -y1) + x1 + 0.5;
DrawLine(xs, y, xe, y, color);
}
}
对于情况1平顶三角形与上类似.
接下来讨论情况3,
其实,明显就是将此三角形视为平底三角形与另一个平顶三角形的组合,在光栅化图形时也是如此分为二段操作,以(xmiddle,ymiddle)作为中间节点。
下面介绍另一种扫描线写法,原理相同,这里的是采用y轴逐渐增加,x轴线性插值。
代码中表现为坐标点y++,x线性插值
1. 先将三角形三顶点按y从小到大排序(屏幕坐标系,左上角原点),设为pa,pb,pc;
2. 判断中间点在pa->pc的左侧或者右侧(对应三角形情况3.4)
设线段端点为从 pa(x1,y1)到 pc(x2, y2), 线外一点 Pb(x0,y0),
判断该点位于有向线 pa->pc的那一侧。
a = ( x2-x1, y2-y1)
b = (x0-x1, y0-y1)
a x b = | a | | b | sinφ (φ为两向量的夹角)
| a | | b | ≠ 0 时, a x b 决定点 P的位置
所以 ax b 的 z 方向大小决定 P位置
(x2-x1)(y0-y1) – (y2-y1)(x0-x1) > 0 左侧
(x2-x1)(y0-y1) – (y2-y1)(x0-x1) < 0 右侧
(x2-x1)(y0-y1) – (y2-y1)(x0-x1) = 0 线段上
即
(x2-x1)/(y2-y1) >(x0-x1)/(y0-y1) 左侧
(x2-x1)/(y2-y1) < (x0-x1)/(y0-y1) 右侧
(x2-x1)/(y2-y1) = (x0-x1)/(y0-y1) 线段上
表现在代码中
先求
dPaPb = (pb.x - pa.x) / (pb.y - pa.y);
dPaPc = (pc.x - pa.x) / (pc.y - pa.y);
所以
if (dPaPb > dPaPc) {
//Pb 在Pa->Pc左侧。
//你可能疑问依据上面的结论应为右侧,
//这里是因为屏幕坐标系与我们常用的坐标系的x轴方向正好相反
}
3. 依据判断设置扫描线扫描每一行时起始与终止的点所在的两个三角形的边
Code:
voidProcessScanLine(inty, Vector4D &pa, Vector4D &pb, Vector4D &pc, Vector4D &pd, UINT32& color) {
//pa,pb,pc,pd四点组成三角形,pa = pd ?
//借助三角形相似由简单线性插值来确定扫描线
//下面的梯度因子完成了归一化,也就是可以适用于平顶或者是平底三角形。
float gradient_s = pa.y != pb.y ? (y - pa.y) / (pb.y - pa.y) : 1;
float gradient_e = pc.y != pd.y ? (y - pc.y) / (pd.y - pc.y) : 1;
int sx = int(pa.x + (pb.x - pa.x) * gradient_s);
int ex = int(pc.x + (pd.x - pc.x) * gradient_e);
//画出扫描线
for (int x = sx; x <ex;x++) {
PutPixel(x,y, color);
}
}
void DrawTriangle(Vector4D &pa, Vector4D&pb, Vector4D &pc, UINT32 color) {
//光栅化画三角形
//首先将a,b,c按照行从小到大排列,且当pb与某一点位于同一水平线上时,使b在左侧
Vector4Dtmp;
if(pa.y > pb.y) {
tmp= pa;
pa= pb;
pb= tmp;
}
if(pa.y > pc.y) {
tmp= pa;
pa= pc;
pc= tmp;
}
if(pb.y > pc.y) {
tmp= pb;
pb= pc;
pc= tmp;
}
//2种特殊
//平顶
if(pa.y == pb.y) {
if(pa.x < pb.x) {
tmp= pa;
pa= pb;
pb= tmp;
}
for(int row = (int)pa.y; row <= (int)pc.y; row++)
ProcessScanLine(row,pb, pc, pa, pc, color);
return;
}
//平底
if(pc.y == pb.y) {
if(pc.x < pb.x) {
tmp= pb;
pb= pc;
pc= tmp;
}
for(int row = (int)pa.y; row <= (int)pc.y; row++)
ProcessScanLine(row,pa, pb, pc, pa, color);
return;
}
floatdPaPb, dPaPc;
if(pb.y - pa.y > 0)
dPaPb= (pb.x - pa.x) / (pb.y - pa.y);
else
dPaPb= 0;
if(pc.y - pa.y > 0)
dPaPc= (pc.x - pa.x) / (pc.y - pa.y);
else
dPaPc= 0;
//Pa
//- Pb
//Pc
if(dPaPb > dPaPc)
{
for(int row = (int)pa.y; row <= (int)pc.y; row++)
{
if(row < pb.y)
{
ProcessScanLine(row,pa, pc, pb, pa, color);
}
else
{
ProcessScanLine(row,pa, pc, pb, pc, color);
}
}
}
// Pa
//Pb-
// Pc
else
{ //dPaPb <= dPaPc
for(int row = (int)pa.y; row <= (int)pc.y; row++)
{
if(row < pb.y)
{
ProcessScanLine(row,pa, pb, pc, pa, color);
}
else
{
ProcessScanLine(row,pb, pc, pa, pc, color);
}
}
}
}
结果
明显上面出现了本应该被遮住的一面,接下来我们用深度测试来除去它。
Z-Buffer
我们借助于Z-buffer来完成深度测试达到遮挡效果
这部分内容比较简单,就是在内存中开辟出一块地方,存放每个像素点的深度,深度来自于该点所对应的三维数据的Z值,然后当多个像素点都想渲染在屏幕点(x,y)时,我们选择那个Z值最小(距离摄像头最近)的来渲染。
所以需要在代码中添加:
1. 深度内存置默认最大值
2. 计算某像素点的深度值
3. PutPixel需要能进行深度检测,当目前像素深度大于原本保留像素深度时,应不作处理。
在2步,我们采用计算整个3D填充三角形面的方程,然后求解其上某点的深度值。
平面方程参考:http://blog.csdn.net/hb707934728/article/details/72772443
Code:
void PutPixel(int x, int y,float z, UINT32&color) {
//含有深度测试的像素填充函数,z用作深度测试
if(((UINT32)x) < (UINT32)width && ((UINT32)y) < (UINT32)height) {
if(z >= zbuffer[y][x]) return;
zbuffer[y][x]= z;
framebuffer[y][x]= color;
/*if(ZBuffer[y*width + x] == 0 || z <= ZBuffer[y*width + x]) {
ZBuffer[y*width+ x] = z;
framebuffer[y][x]= color;
}*/
}
}
void ProcessScanLine(int y, Vector4D&pa, Vector4D &pb, Vector4D &pc, Vector4D &pd, UINT32&color) {
//pa,pb,pc,pd四点组成三角形,
//借助三角形相似由简单线性插值来确定扫描线
//默认Vector4D.w = 1.0
//下面的梯度因子完成了归一化,也就是可以适用于平顶或者是平底三角形。
floatgradient_s = pa.y != pb.y ? (y - pa.y) / (pb.y - pa.y) : 1;
floatgradient_e = pc.y != pd.y ? (y - pc.y) / (pd.y - pc.y) : 1;
intsx = int(pa.x + (pb.x - pa.x) * gradient_s);
intex = int(pc.x + (pd.x - pc.x) * gradient_e);
//求解深度平面方程
floatA = (pb.y - pa.y)*(pc.z - pa.z) - (pb.z - pa.z)*(pc.y - pa.y);
floatB = (pb.z - pa.z)*(pc.x - pa.x) - (pb.x - pa.x)*(pc.z - pa.z);
floatC = (pb.x - pa.x)*(pc.y - pa.y) - (pc.x - pa.x)*(pb.y - pa.y);
floatD = - (A * pa.x + B * pa.y + C * pa.z);
floatC_inv = 1.0 / C;
//画出扫描线
for(int x = sx; x <=ex; x++) {
floatz = (-D - A*x - B*y)*C_inv;
PutPixel(x,y, z, color);
}
}
void Render(Mesh &mesh) {
//开始渲染,
Vector4Dre,re2,re3,re4;
UINT32color_point = 0xc0c0c0; //点的颜色
UINT32colorarry[] = { 0x00ff0000 ,0x0000ff00,0x000000ff,0x00ffff00,
0x00efefef,0x00eeffcc,0x00cc00ff,0x0015ffff,
0x00121212,0x00001233,0x5615cc,0x353578};
transform.world.Set_As_Rotate(mesh.Rotation.x,mesh.Rotation.y,
mesh.Rotation.z,mesh.Rotation.w);
//transform.world.Set_Identity();
transform.Update();
Clear(0);
for(int i = 0; i <12; i++) {
transform.Apply(mesh.vertices[mesh.faces[i].v1],re);
transform.Homogenize(re,re2);
transform.Apply(mesh.vertices[mesh.faces[i].v2],re);
transform.Homogenize(re,re3);
transform.Apply(mesh.vertices[mesh.faces[i].v3],re);
transform.Homogenize(re,re4);
DrawTriangle(re2,re3, re4, colorarry[i]);
/*DrawLine(re2.x,re2.y, re3.x, re3.y, re2.z, re3.z, color_point);
DrawLine(re2.x,re2.y, re4.x, re4.y,re2.z,re4.z, color_point);
DrawLine(re3.x,re3.y, re4.x, re4.y,re3.z,re4.z, color_point);*/
}
}
效果图:
Ps:这一部分可折腾了我,之前没仔细检错,结果出现了深度测试失效,经过Debug后发现问题有2。
1光栅化三角形函数有问题,对于部分特殊三角将无法正常渲染,导致出现伪深度测试失效。
2深度测试的深度计算,之前计算为了简单采用线性插值,效果不好,后改为求解深度平面方程,解决,其中还一个问题是在输入求解深度方程的三个点必须不同,之前测试时没注意输入,点重复导致求解平面失败。
以及实际最终我们设置Z_buffer使用是依据距离摄像头的距离。
Ps的Ps:光栅化部分相当耗费时间,在这里有大幅度优化可能。