软件光栅器实现(三、裁剪)
本节介绍软件光栅器的裁剪,转载请注明出处。
光栅化的裁剪是为了对于将视口内不可见的部分裁剪掉,思路是对与裁剪立方体有交点的线进行求交,具体是通过编码的算法将三角形每条边与裁剪立方体的两个面的交点算出来,然后连接成新的三角形,这种情况下,不与裁剪立方体相交的线不会有变化,但是发生相交的线会被切割,并重新连接成新的三角形(图中紫红色方框内所示的屏幕上缘区域,就是新生成的三角形)。裁剪是发生在VS阶段之后,进行了透视矫正和透视除法之后进行的,裁剪之后则是三角形的连接及扫描阶段。
求交点的算法是通过Cohen-SutherLand算法(编码算法)求出来的,可参考https://www.jianshu.com/p/d512116bbbf3
不过请注意这类网上一般都是二维情况下的编码,所以都是分为9个编码区域,但是对于三维空间中的裁剪立方体,需要划分为3×9个也就是27个编码区域。这样就可以算出与裁剪立方体上、下、左、右、前、后6个裁剪面相交的顶点连线。
求交点的代码如下,算出来的交点暂时存储到一个数组中:
vector<VertexOut> Tiny3DDeviceContext::GetCrossVertex(VertexOut v0, VertexOut v1){ vector<VertexOut> res; VertexOut vout, vin; v0.posH.x /= v0.posH.w; v0.posH.y /= v0.posH.w; v0.posH.z /= v0.posH.w; v0.posH.w /= v0.posH.w;//透视除法 v1.posH.x /= v1.posH.w; v1.posH.y /= v1.posH.w; v1.posH.z /= v1.posH.w; v1.posH.w /= v1.posH.w; OutCode outcode0 = ComputeOutCode(v0.posH.x, v0.posH.y, v0.posH.z, v0.posH.w); OutCode outcode1 = ComputeOutCode(v1.posH.x, v1.posH.y, v1.posH.z, v1.posH.w); while (true){ if ((outcode0 | outcode1) == 0){//相或为0,都在立方体内,接受并且退出循环 res.push_back(v0); res.push_back(v1); if (v0.posH.x > 1 || v1.posH.y > 1) int test = 1; return res; } else if (outcode0 & outcode1){// 相与为1,都在立方体外的一侧,拒绝且退出循环 return res; } else{ //找出在界外的点 OutCode outcodeOut = outcode0 ? outcode0 : outcode1;//outcodeOut是在外头点的编码 vin = (outcode0 == outcodeOut) ? v1 : v0; vout = (outcode0 == outcodeOut) ? v0 : v1; float x1 = vin.posH.x, y1 = vin.posH.y, z1 = vin.posH.z, w1 = vin.posH.w; float x2 = vout.posH.x, y2 = vout.posH.y, z2 = vout.posH.z, w2 = vout.posH.w; VertexOut crossPoint; // 找出和边界相交的点 if (outcodeOut & TOP){ if (y2 == y1) return res; crossPoint.posH.x = (w2 - y1) *(x2 - x1) / (y2 - y1) + x1; crossPoint.posH.y = w2; crossPoint.posH.z = (w2 - y1) *(z2 - z1) / (y2 - y1) + z1; crossPoint.posH.w = w2; } else if (outcodeOut & BOTTOM){ if (y2 == y1) return res; crossPoint.posH.x = (-w2 - y1) *(x2 - x1) / (y2 - y1) + x1; crossPoint.posH.y = -w2; crossPoint.posH.z = (-w2 - y1) *(z2 - z1) / (y2 - y1) + z1; crossPoint.posH.w = w2; } else if (outcodeOut & LEFT){ if (x2 == x1) return res; crossPoint.posH.x = -w2; crossPoint.posH.y = (-w2 - x1)*(y2 - y1) / (x2 - x1) + y1; crossPoint.posH.z = (-w2 - x1)*(z2 - z1) / (x2 - x1) + z1; crossPoint.posH.w = w2; } else if (outcodeOut & RIGHT){ if (x2 == x1) return res; crossPoint.posH.x = w2; crossPoint.posH.y = (w2 - x1)*(y2 - y1) / (x2 - x1) + y1; crossPoint.posH.z = (w2 - x1)*(z2 - z1) / (x2 - x1) + z1; crossPoint.posH.w = w2; } else if (outcodeOut & FRONT){ if (z2 == z1) return res; crossPoint.posH.x = (0 - z1)*(x2 - x1) / (z2 - z1) + x1; crossPoint.posH.y = (0 - z1)*(y2 - y1) / (z2 - z1) + y1; crossPoint.posH.z = 0; crossPoint.posH.w = 0; } else if (outcodeOut & BEHIND){ if (z2 == z1) return res; crossPoint.posH.x = (w2 - z1)*(x2 - x1) / (z2 - z1) + x1; crossPoint.posH.y = (w2 - z1)*(y2 - y1) / (z2 - z1) + y1; crossPoint.posH.z = w2; crossPoint.posH.w = w2; } //交叉点各属性插值 float dy = crossPoint.posH.y - v0.posH.y; float t = dy / (v1.posH.y - v0.posH.y); crossPoint.tex = MathUtil::Lerp(v0.tex, v1.tex, t); crossPoint.normal = MathUtil::Lerp(v0.normal, v1.normal, t); crossPoint.color = MathUtil::Lerp(v0.color, v1.color, t); crossPoint.oneDivZ = MathUtil::Lerp(v0.oneDivZ, v1.oneDivZ, t); crossPoint.lightInTangent = MathUtil::Lerp(v0.lightInTangent, v1.lightInTangent, t); crossPoint.viewInTangent = MathUtil::Lerp(v0.viewInTangent, v1.viewInTangent, t); // 为什么继续循环,因为另一个端点都有可能也在外面,需要将原来外面的点用算出来的交点取代,然后再进行一次判断,直到accept if (outcodeOut == outcode0) { v0 = crossPoint; outcode0 = ComputeOutCode(v0.posH.x, v0.posH.y, v0.posH.z, v0.posH.w); } else { v1 = crossPoint; outcode1 = ComputeOutCode(v1.posH.x, v1.posH.y, v1.posH.z, v1.posH.w); } } } }
其中ComputeOutCode()方法是算出顶点在编码算法下的编码值函数:
OutCode Tiny3DDeviceContext::ComputeOutCode(double x, double y, double z, double w){//判断顶点区域码 OutCode code; code = INSIDE; // x,y,z==w 也算在里面 if (x < -w) // to the left of clip window code |= LEFT; else if (x > w) // to the right of clip window code |= RIGHT; if (y < -w) // below the clip window code |= BOTTOM; else if (y > w) // above the clip window code |= TOP; if (z < -w) code |= FRONT; else if (z > w) code |= BEHIND; return code; }
在软光栅渲染的过程中,通过裁剪的方法可以把大量不必要渲染的部分剔除掉,这一部分的顶点不会进入到PS(像素着色器)阶段,从而提高程序效能、帧数。
另外,对于大型游戏,现在厂商引擎不单单是简单利用编码裁剪的方法对视口外的部分剔除,而是设定一个比屏幕大一圈的裁剪框,在这个裁剪框中不进行裁剪,这是因为:在三角形面片数数量非常大时,如果对屏幕区域所有的三角形进行判断和求交,这样带来的运算量可能比不裁剪而直接带入到PS中运算更大,所以一般会先预设一个较大的裁剪视口,才该视口内不裁剪,只有比这个视口大时,才进行裁剪。
下一节(四、OBJ文件的加载):https://www.cnblogs.com/zeppelin5/p/10067683.html