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

AIG*_*110 17 opengl projection linear-interpolation fragment-shader pixel-shading

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

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

ybu*_*ill 28

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

从着色器执行得到顶点的剪辑坐标,从而产生顶点坐标gl_Position.

剪辑坐标上的透视分割产生标准化的设备坐标,然后是视口变换(参见第13.6.1节),将这些坐标转换为窗口坐标.

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

device.xyz = gl_Position.xyz / gl_Position.w
Run Code Online (Sandbox Code Playgroud)

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

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

gl_FragCoord.xyz = device.xyz scaled to viewport
gl_FragCoord.w = 1 / gl_Position.w
Run Code Online (Sandbox Code Playgroud)

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

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

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

P(u,v) = P0 + (P1 - P0)u + (P2 - P0)v
Run Code Online (Sandbox Code Playgroud)

gl_FragCoord,1 / gl_Position.w是设备坐标.替换x,y,z的表达式并求解(s,t).表示P(b0,b1,b2)并替换z,s和t.如果我们都没有犯错误,你应该得到

f(u,v) = f0 + (f1 - f0)u + (f2 - f0)v
Run Code Online (Sandbox Code Playgroud)

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

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

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

P(b0,b1,b2) = P0*b0 + P1*b1 + P2*b2
f(b0,b1,b2) = f0*b0 + f1*b1 + f2*b2
Run Code Online (Sandbox Code Playgroud)

1/gl_Position.w从定义中可以看出哪里.因此,我们将(s,t)表示为(u,v)的函数,每个片段有一个除法,并且有许多muls和add.

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

把它们放在一起

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

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

  2. 表示gl_BaryCoordNV.

  3. 计算

         ( b0 / gl_Position[0].w, b1 / gl_Position[1].w, b2 / gl_Position[2].w )
    B = -------------------------------------------------------------------------
          b0 / gl_Position[0].w + b1 / gl_Position[1].w + b2 / gl_Position[2].w
    
    Run Code Online (Sandbox Code Playgroud)

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

  4. 对于每个顶点,计算

    struct Renderbuffer {
        int w, h, ys;
        void *data;
    };
    
    struct Vert {
        vec4f position;
        vec4f texcoord;
        vec4f color;
    };
    
    struct Varying {
        vec4f texcoord;
        vec4f color;
    };
    
    void vertex_shader(const Vert &in, vec4f &gl_Position, Varying &out)
    {
        out.texcoord = in.texcoord;
        out.color = in.color;
        gl_Position = { in.position[0], in.position[1], -2*in.position[2] - 2*in.position[3], -in.position[2] };
    }
    
    void fragment_shader(vec4f &gl_FragCoord, const Varying &in, vec4f &out)
    {
        out = in.color;
        vec2f wrapped = vec2f(in.texcoord - floor(in.texcoord));
        bool brighter = (wrapped[0] < 0.5) != (wrapped[1] < 0.5);
        if(!brighter)
            (vec3f&)out = 0.5f*(vec3f&)out;
    }
    
    void store_color(Renderbuffer &buf, int x, int y, const vec4f &c)
    {
        // can do alpha composition here
        uint8_t *p = (uint8_t*)buf.data + buf.ys*(buf.h - y - 1) + 4*x;
        p[0] = linear_to_srgb8(c[0]);
        p[1] = linear_to_srgb8(c[1]);
        p[2] = linear_to_srgb8(c[2]);
        p[3] = lrint(c[3]*255);
    }
    
    void draw_triangle(Renderbuffer &color_attachment, const box2f &viewport, const Vert *verts)
    {
        Varying perVertex[3];
        vec4f gl_Position[3];
    
        box2f aabbf = { viewport.hi, viewport.lo };
        for(int i = 0; i < 3; ++i)
        {
            // invoke the vertex shader
            vertex_shader(verts[i], gl_Position[i], perVertex[i]);
    
            // convert to device coordinates by perspective division
            gl_Position[i][3] = 1/gl_Position[i][3];
            gl_Position[i][0] *= gl_Position[i][3];
            gl_Position[i][1] *= gl_Position[i][3];
            gl_Position[i][2] *= gl_Position[i][3];
    
            // convert to window coordinates
            auto &pos2 = (vec2f&)gl_Position[i];
            pos2 = mix(viewport.lo, viewport.hi, 0.5f*(pos2 + vec2f(1)));
            aabbf = join(aabbf, (const vec2f&)gl_Position[i]);
        }
    
        // precompute the affine transform from fragment coordinates to barycentric coordinates
        const float denom = 1/((gl_Position[0][0] - gl_Position[2][0])*(gl_Position[1][1] - gl_Position[0][1]) - (gl_Position[0][0] - gl_Position[1][0])*(gl_Position[2][1] - gl_Position[0][1]));
        const vec3f barycentric_d0 = denom*vec3f( gl_Position[1][1] - gl_Position[2][1], gl_Position[2][1] - gl_Position[0][1], gl_Position[0][1] - gl_Position[1][1] );
        const vec3f barycentric_d1 = denom*vec3f( gl_Position[2][0] - gl_Position[1][0], gl_Position[0][0] - gl_Position[2][0], gl_Position[1][0] - gl_Position[0][0] );
        const vec3f barycentric_0 = denom*vec3f(
            gl_Position[1][0]*gl_Position[2][1] - gl_Position[2][0]*gl_Position[1][1],
            gl_Position[2][0]*gl_Position[0][1] - gl_Position[0][0]*gl_Position[2][1],
            gl_Position[0][0]*gl_Position[1][1] - gl_Position[1][0]*gl_Position[0][1]
        );
    
        // loop over all pixels in the rectangle bounding the triangle
        const box2i aabb = lrint(aabbf);
        for(int y = aabb.lo[1]; y < aabb.hi[1]; ++y)
        for(int x = aabb.lo[0]; x < aabb.hi[0]; ++x)
        {
            vec4f gl_FragCoord;
            gl_FragCoord[0] = x + 0.5;
            gl_FragCoord[1] = y + 0.5;
    
            // fragment barycentric coordinates in window coordinates
            const vec3f barycentric = gl_FragCoord[0]*barycentric_d0 + gl_FragCoord[1]*barycentric_d1 + barycentric_0;
    
            // discard fragment outside the triangle. this doesn't handle edges correctly.
            if(barycentric[0] < 0 || barycentric[1] < 0 || barycentric[2] < 0)
                continue;
    
            // interpolate inverse depth linearly
            gl_FragCoord[2] = dot(barycentric, vec3f(gl_Position[0][2], gl_Position[1][2], gl_Position[2][2]));
            gl_FragCoord[3] = dot(barycentric, vec3f(gl_Position[0][3], gl_Position[1][3], gl_Position[2][3]));
    
            // clip fragments to the near/far planes (as if by GL_ZERO_TO_ONE)
            if(gl_FragCoord[2] < 0 || gl_FragCoord[2] > 1)
                continue;
    
            // convert to perspective correct (clip-space) barycentric
            const vec3f perspective = 1/gl_FragCoord[3]*barycentric*vec3f(gl_Position[0][3], gl_Position[1][3], gl_Position[2][3]);
    
            // interpolate the attributes using the perspective correct barycentric
            Varying varying;
            for(int i = 0; i < sizeof(Varying)/sizeof(float); ++i)
                ((float*)&varying)[i] = dot(perspective, vec3f(
                    ((const float*)&perVertex[0])[i],
                    ((const float*)&perVertex[1])[i],
                    ((const float*)&perVertex[2])[i] 
                ));
    
            // invoke the fragment shader and store the result
            vec4f color;
            fragment_shader(gl_FragCoord, varying, color);
            store_color(color_attachment, x, y, color);
        }
    }
    
    
    
    int main()
    {
        Renderbuffer buffer = { 512, 512, 512*4 };
        buffer.data = malloc(buffer.ys * buffer.h);
        memset(buffer.data, 0, buffer.ys * buffer.h);
    
        // interleaved attributes buffer
        Vert verts[] = {
            { { -1, -1, -2, 1 }, { 0, 0, 0, 1 }, { 0, 0, 1, 1 } },
            { { 1, -1, -1, 1 }, { 10, 0, 0, 1 }, { 1, 0, 0, 1 } },
            { { 0, 1, -1, 1 }, { 0, 10, 0, 1 }, { 0, 1, 0, 1 } },
        };
    
        box2f viewport = { 0, 0, buffer.w, buffer.h };
        draw_triangle(buffer, viewport, verts);
    
        lodepng_encode32_file("out.png", (unsigned char*)buffer.data, buffer.w, buffer.h);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    这使它在屏幕上的位置.

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

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

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

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

      #version 450 core
      layout(location = 0) in vec4 position;
      layout(location = 1) in vec4 texcoord;
      layout(location = 2) in vec4 color;
      
      out gl_PerVertex {
          vec4 gl_Position;
      };
      
      layout(location = 0) out PerVertex {
          vec4 texcoord;
          vec4 color;
      } OUT;
      
      void main() {
          OUT.texcoord = texcoord;
          OUT.color = color;
          gl_Position = vec4(position[0], position[1], -2*position[2] - 2*position[3], -position[2]);
      }
      
      Run Code Online (Sandbox Code Playgroud)
    3. 使用输入A(正确插值)执行片段着色器.


der*_*ass 11

您将在GL规范中找到的公式(请参阅第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
Run Code Online (Sandbox Code Playgroud)

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

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

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

  1. 请注意一些有效实现的方法.1/w_i可以按顶点预先计算反演(让我们q_i在下面调用它们),不必为每个片段重新计算它.它完全免费,因为w无论如何我们进入NDC空间时,所以我们可以保存这个值.GL规范从未描述过如何在内部实现某个特征,但是屏幕空间坐标可以访问glFragCoord.xyz,并且gl_FragCoord.w保证给出(线性插值的)1/w 剪辑空间坐标的事实在这里是非常明显的.每片段1_w值实际上是上面给出的公式的分母.

  2. 的因素a/w_a,b/w_bc/w_c各自的公式中使用两次.对于任何属性值,这些也是常量,现在要插入多少属性.因此,每个片段,就可以计算出a'=q_a * a,b'=q_b * bc'=q_c和获得

      a' * f_a + b' * f_b + c' * f_c
    f=------------------------------
               a' + b' + c'
    
    Run Code Online (Sandbox Code Playgroud)

所以透视插值归结为

  • 另外3次乘法,
  • 另外2个补充,和
  • 另外1个师

每个片段.