首页 文章

OpenGL究竟如何透视地校正线性插值?

提问于
浏览
10

如果在OpenGL管道中的光栅化阶段发生线性插值,并且顶点已经转换为屏幕空间,那么用于透视正确插值的深度信息来自何处?

任何人都可以详细描述OpenGL如何从屏幕空间原语转换为具有正确插值的片段吗?

2 回答

  • 6

    顶点着色器的输出是四分量矢量 vec4 gl_Position . 从第13.6节核心GL 4.4规范的坐标转换:

    来自着色器执行的顶点结果的剪辑坐标,其产生顶点坐标gl_Position . 剪辑坐标上的透视分割产生标准化的设备坐标,然后是视口变换(参见第13.6.1节),将这些坐标转换为窗口坐标 .

    考虑具有±1的左,上,右,下和近剪裁平面的典型投影矩阵 . 看起来像:

    [ 1  0  0  0 ]
        [ 0  1  0  0 ]
    P = [ *  *  *  * ]
        [ 0  0 -1  0 ]
    

    我们忽略第三行,因为它仅用于计算z缓冲区的深度值,并且与其余讨论无关 .

    给定世界空间中的顶点(x,y,z,1),顶点着色器将传递该值

    gl_Position = P * vec4(x, y, z, 1) = vec4(x, y, *, -z)
    

    进入光栅化阶段 . 这是透视信息的来源:它位于 gl_Position 的最后一个组件中!请注意,顶点着色器不负责进行透视分割 . 如果这样做,您将得到不正确的插值 .

    如何计算正确的透视插值?

    让我们考虑通过给定三角形的平面 . 我们给它一个参数化(s,t):

    x = s*x0 + t*x1 + x2
    y = s*y0 + t*y1 + y2
    z = s*z0 + t*z1 + z2
    

    u = -x/zv = -y/z 为设备坐标 . 替换x,y,z的表达式并求解(s,t) . 表示 w = -1/z 并替换z,s和t . 如果我们都没有犯错误,你应该得到

    (y0*z1 - z0*y1)*u + (z0*x1 - x0*z1)*v + (x0*y1 - y0*x1)
    w = -1 * -------------------------------------------------------------- .
              (y0*z1 - z0*y1)*x2 + (z0*x1 - x0*z1)*y2 + (x0*y1 - y0*x1)*z2
    

    我们得到的逆深度(w)与设备坐标(u,v)相关 . 因此,我们可以在三角形的顶点计算w,并在内部线性插值 .

    接下来,我们想要插入一些任意属性 . 显然,计算每个片段的(s,t)参数化就足够了 .

    求解(s,t)得到(s,t)得到:

    (x - x2)*y1 - x1*(y - y2)
    s = ---------------------------
               x0*y1 - x1*y0
    
         x0*(y - y2) - (x - x2)*y0
    t = ---------------------------
               x0*y1 - x1*y0
    

    其中 (x,y) = (u,v)/w 来自定义 . 因此,我们将(s,t)表示为(u,v)的函数,每个片段有一个除法,并且有许多muls和add .

    请注意,这是理论部分 . 现实生活中的硬件实现可能会做很多近似,技巧和其他魔术 .

    把它们放在一起

    为简单起见,假设视口转换是一个标识,因此窗口坐标与标准化设备坐标一致 .

    • 让我们考虑单个三角形的光栅化,顶点着色器返回位置P0,P1,P2(这是它们的 gl_Position )和一些我们必须正确插值的属性A0,A1,A2 .

    • 表示 Qi = (Pi.x, Pi.y, -Pi.w) .

    • 计算

    (x0, y0, z0) = Q0 - Q2
    (x1, y1, z1) = Q1 - Q2
    (x2, y2, z2) = Q2 .
    

    (每个三角形执行一次 . )

    • 对于每个顶点,计算
    Ui = Pi.x / Pi.w
    Vi = Pi.y / Pi.w
    

    这使它在屏幕上的位置 .

    • 使用vetices(U0,V0),(U1,V1),(U2,V2)栅格化二维三角形,生成一组位于三角形内的片段位置(u,v) .

    • 对于上述光栅化生成的每个片段(u,v),执行以下操作:

    • 在上一节的公式中替换(u,v),给出片段的(s,t) .

    • (s,t)在世界空间中线性变化(P2处为(0,0),P0处为(1,0),P1处为(0,1)),并且在屏幕空间中透视正确 .

    • 计算

    A = s*(A0 - A2) + t*(A1 - A2) + A2
    
    • 使用输入A(正确插值)执行片段着色器 .
  • 19

    您将在GL specification中找到的公式(请参见第427页;链接是当前的4.4规范,但一直是这样)对于三角形中属性值的透视校正插值是:

    a * f_a / w_a   +   b * f_b / w_b   +  c * f_c / w_c
    f=-----------------------------------------------------
          a / w_a      +      b / w_b      +     c / w_c
    

    其中 a,b,c 表示三角形中点的重心坐标,我们为( a,b,c >=0, a+b+c = 1 ), f_i 顶点 i 处的属性值插值, w_i 顶点 i 的剪辑空间 w 坐标 . 注意,重心坐标仅针对三角形的窗口空间坐标的2D投影计算(因此z被忽略) .

    这就是ybungalowbill在他的精细答案中给出的公式,在一般情况下,归结为具有任意投影轴 . 实际上,投影矩阵的最后一行仅定义了图像平面将与之正交的投影轴,并且剪辑空间 w 分量只是顶点坐标与该轴之间的点积 .

    在典型的情况下,投影矩阵具有(0,0,-1,0)作为最后一行,因此它转换为 w_clip = -z_eye ,这就是ybungalowbill所使用的 . 但是,因为 w 是我们实际将要做的除法(这是唯一的整个转换链中的非线性步骤),这适用于任何投影轴 . 它也适用于正交投影的平凡情况,其中 w 始终为1(或至少是常数) .

    • 注意一些有效实施的事情 . 可以根据每个顶点预先计算反转 1/w_i (让我们在下面将它们称为 q_i ),不必为每个片段重新计算它 . 它完全免费,因为无论如何我们除了 w 进入NDC空间,所以我们可以保存这个值 . GL规范从未描述过如何在内部实现某个特征,但是屏幕空间坐标可以在 glFragCoord.xyz 中访问,并且 gl_FragCoord.w 保证给出(lineariliy interpolated) 1/w 剪辑空间坐标这一事实在这里很有启发性 . 每个片段 1_w 值实际上是上面给出的公式的分母 .

    • 因子 a/w_ab/w_bc/w_c 在公式中分别使用了两次 . 对于任何属性值,这些也是常量,现在要插入多少属性 . 所以,每个片段,你可以计算 a'=q_a * ab'=q_b * bc'=q_c 并得到

    a' * f_a + b' * f_b + c' * f_c
    f=------------------------------
               a' + b' + c'
    

    所以透视插值归结为

    • 3次额外乘法,

    • 2个额外的补充,和

    • 1个额外师

    每个片段 .

相关问题