从渲染管线学 GLES2.0(五)----裁剪

1、为什么要裁剪

OpenGL 规定了我们可见的顶点都应该在标准视体之内,也就是在标准化设备坐标(NDC)中,每个顶点的 x、y、z 坐标都应该在 -1.0 到 1.0 之间,对应的 xyz 范围就是 -1 <= x,y,z <= 1,超出这个范围的顶点都将不可见。

假设在局部空间中也就是在应用程序中传递的坐标 p(x, y, z, 1),经过模型变换为 (x_model, y_model, z_model, 1),再经过视图变换为 (x_view, y_view, z_view, 1),最后经过透视投影坐标就变成了 (x_clip, y_clip, z_clip, -z_view),裁剪空间坐标除以 w = -z_view 变换到 NDC 空间中 (x_clip/w, y_clip/w, z_clip/w, 1),那么所有的顶点都应该满足 -1 <= x_clip/w, y_clip/w, z_clip/w <= 1,也就是 -w <= x_clip, y_clip, z_clip <= w,不满足这个条件的点,理论上来说都应该被裁剪掉。

2、裁剪平面

根据 -w <= x_clip, y_clip, z_clip <= w 这个条件,我们可以得到六个边界条件

左:-w <= x,边界为 -w = x,也就是  x + w = 0
右: x <= w,边界为  x = w,也就是 -x + w = 0
下:-w <= y,边界为 -w = y,也就是  y + w = 0
上: y <= w,边界为  y = w,也就是 -y + w = 0
近:-w <= z, 边界为 -w = z,也就是  w + z = 0
远: z <= w,边界为  z = w,也就是 -z + w = 0

我们可以将 w 当作一个常数,那么这六个边界条件实际上就对应着六个裁剪平面,满足六个条件的区域是一个立方体,完全在立方体之内的三角形可以正常进行渲染,完全在立方体之外的三角形可以直接丢弃,而部分在外的三角形,就需要通过裁剪将顶点裁到边界平面之内。

三维平面的一般形式方程为 Ax + By + Cz + D,其中 n = (A, B, C) 为平面法线,对于任意一点 P(x, y, z),其到平面的距离 d = Ax + By + Cz + D,如果 d = 0 则点在平面上,d > 0 认为在平面内,d < 0 认为在平面外。

那么根据六个边界条件,我们可以得到六个平面方程

左: x + w = 0, A = 1,  B = 0,  C = 0,  D = w
右:-x + w = 0, A = -1, B = 0,  C = 0,  D = w
下: y + w = 0, A = 0,  B = 1,  C = 0,  D = w
上:-y + w = 0, A = 0,  B = -1, C = 0,  D = w
近: w + z = 0, A = 0,  B = 0,  C = 1,  D = w
远:-z + w = 0, A = 0,  B = 0,  C = -1, D = w

这里有一个容易困惑的点,我们默认的是 -w < w 的,但是如果 w <= 0,那么就变成了 -w >= w 了,所以在进行实际裁剪的时候,要先处理掉所有 w <= 0 的情况,也就是在观察者身后的点,因为在观察者身后时 z_view 会大于 0,那么透视之后的 w 就会小于 0。这里再看一下之前提到过的透视投影矩阵。

\begin{bmatrix} 1\over(aspect * tan(fov/2))&0&0&0\\ 0&1\over(tan(fov/2))&0&0\\ 0&0&(near+far)\over(near-far)&(2*near*far)\over(near-far)\\ 0&0&-1&0\\ \end{bmatrix}

 当 z_view > 0,那么 w = -z_view,那么 w 就一定是小于 0 的,那么对应的 z_clip 就等于

z_clip = z_view * (near+far)/(near-far) + (2*near*far)/(near-far)
       = (z_view * (near+far) + (2*near*far)) / (near-far)

因为 near-far 是小于 0 的,当 z_view 大于 0 时,z_clip 肯定也是小于 0 的,那么 z_clip + w_clip 一定是小于 0 的,那么近平面的判断一定不成立,所以先进行近平面的裁剪就可以裁剪掉 w <= 0 的情况,还有我们可以看透视投影的平截头体

 摄像机的位置就是观察者的位置,近平面肯定是在观察者前面的,如果一个点坐标在观察者身后,那么肯定在近平面之外的,所以先进行近平面的裁剪就会将所有观察者身后的点裁掉。

3、线段与平面的交点

有两点 p1(x1, y1, z1) 和 p2(x2, y2, z2) 分别在平面的两侧,p1 在平面内侧,p2 在平面外侧,d1 代表 p1 到平面的距离,那么 d1 > 0,d2 代表 p2 到平面的距离,那么 d2 < 0,那么可以通过权重 d = d1 / (d1 - d2),计算出来线段与平面的交点 p3。

P3 = p2 * d + (1 - d) * p1

如下图所示

这里为啥是 p2 乘以 d,而不是 p1 乘以 d,这是因为 d1 越大,就离平面越远,占比就要越小,但是 d1 / (d1 - d2) 就越大,所以这里 p1 需要乘以的是 1 - d。

 4、Sutherland-Hodgeman 裁剪算法

又称为逐边裁剪算法,它的算法原理是每次使用裁剪框的一条边去裁剪多边形的每一条边,算法流程如下

1. 将多边形所有顶点作为一个输入数据进行原理输入

std::vector<Point> input{p1, p2, p3};

2. 将每两个顶点组成一条线段来进行每一个平面的裁剪

for (int i = 0; i < plane_num; ++i) {
   for (int i = 0; i < input.size(); ++i) {
       // input[i] input[(i+1)&input.size()] 
   }
}

3. 线段和裁剪面的关系可能有以下几种

都在可见面内,那么将第一个顶点 A 放入输出 output 数组

都在可见点外部,不做处理

第一个点在内部,第二个点在外部,将 A 点放入 output,以及 AB 与可见面的交点 P 也放入 output

第一个点在外部,第二个点在内部,将交点 P 放入 output

 4. 将 output 数组,作为下一个裁剪面的输入,继续进行第 2 步,直到所有裁剪面都遍历结束

举个例子

假设三角形的三个顶点分别为 A、B、C 的位置如下

接下来我们分别对四个平面(left、right、top、bottom)进行裁剪

1. 判断 AB,BC,CA 三条线段和 left 平面的位置关系

判断 AB:A 在内,B 在外,那么将 A 点放入 output 数组,并将交点 D 放入 output 数组

判断 BC:B 在外,C 在内,将 BC 交点 E 放入 output

判断 CA:C 在内,A 在内,将 C 点放入 output 数组

第一次裁剪完 left 平面,output 数组内容如下

output = {A, D, E, C}

就变成了下面这样

2. 接下来进行 right 平面的裁剪

判断 AD:A 在内,D 在内,将 A 放入 output

判断 DE:D 在内,E 在内,将 D 放入 output

判断 EC:E 在内,C 在外,将 E 放入 output,并将交点 F 放入 output

判断 CA:C 在外,A 在内,将交点 H 放入 output

裁剪完后的 output 变为

output = {A, D, E, F, H}

 

3. 接下来进行 top 的裁剪

判断 AD:A 在外,D 在内,将交点 I 放入 putput

判断 DE:D 在内,E 在内,将交点 D 放入 putput

判断 EF:E 在内,F 在内,将交点 E 放入 putput

判断 FH:F 在内,H 在内,将交点 F 放入 putput

判断 HA:H 在内,A 在外,将 H 放入 output,并将交点 J 放入 output

裁剪完后的 output 变为

output = {I, D, E, F, H, J}

4. 进行 bottom 的裁剪,都在 bottom 内部,裁剪完顶点没有变化

5. 组装三角形,这个时候的顶点为

output = {I, D, E, F, H, J}

这个时候按照 GL_TRIANGLE_FAN 的方式组装三角形,那么三角形分别为

{I, D, E} 
{I, E, F} 
{I, F, H} 
{I, H, J}

也就是下面这样

5、代码实现

六个平面系数如下

std::vector<std::vector<int>> planes{
    { 0,  0,  1, 1},   // near
    { 0,  0, -1, 1},   // far
    { 1,  0,  0, 1},   // left
    {-1,  0,  0, 1},   // right
    { 0,  1,  0, 1},   // bottom
    { 0, -1,  0, 1},   // top
    
};

判断点是不是在平面内

bool Inside(const Point& point, const std::vector<int>& plane) {
    return point.x * plane[0] + point.y * plane[1] + point.z * plane[2] + point.w * plane[3] >= 0;
}

计算交点

Point Intersect(const Point& p1, const Point& p2, const std::vector<int>& plane) {
    float d1 = p1.x * plane[0] + p1.y * plane[1] + p1.z * plane[2] + p1.w * plane[3];
    float d2 = p2.x * plane[0] + p2.y * plane[1] + p2.z * plane[2] + p2.w * plane[3];
    float d = d1 / (d1 - d2);
    Point p3 = p1 * (1 - d) + p2 * d;
    return p3;
}

完整代码

bool Outside(const Point& point, const std::vector<int>& plane) {
    return !Inside(point, plane);
}

void ClipTriangle(const std::vector<Point>& points) {
    std::vector<Point> output = points;
    for (const auto& type : planes) {
        std::vector<Point> input(output);
        output.clear();
        for (size_t i = 0; i < input.size(); ++i) {
            if (Outside(input[i], type) && Inside(input[(i + 1) % input.size()], type)) {
                Point p = Intersect(input[i], input[(i + 1) % input.size()], type);
                output.push_back(p);
            } else if (Inside(input[i], type) && Outside(input[(i + 1) % input.size()], type)) {
                Point p = input[i];
                output.push_back(p);
                p = Intersect(input[i], input[(i + 1) % input.size()], type);
                output.push_back(p);
            } else if (Inside(input[i], type) && Inside(input[(i + 1) % input.size()], type)) {
                Point p = input[i];
                output.push_back(p);
            }
        }
    }
    for (int i = 0; i < output.size() - 2; ++i) {
        for (size_t j = i; j < i + 3; ++j) {
            int index = j;
            if (j == i) {
                index = 0;
            }
            std::cout << output[index].x << " " << output[index].y << std::endl;
        }
        std::cout << "=====" << std::endl;
    }
}

6、其它图元裁剪

在上面我们说了三角形的裁剪,对于线段来说,和三角形的裁剪类似,区别就是线段没有最后的图元组装,对于点来说,如果点在远近平面之外,那么直接丢弃,其它情况放在光栅化阶段做裁剪,因为点有 pointsize。其实对于三角形和线段来说,这里也可以只做远近面的裁剪,上下左右的裁剪都可以在光栅化阶段进行。

7、参考文章

一篇文章彻底弄懂齐次裁剪 - 知乎

  • 13
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值