opengl - OpenGL到底是如何进行透视校正线性插值的?

标签 opengl graphics projection fragment-shader linear-interpolation

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

任何人都可以详细描述 OpenGL 如何从屏幕空间基元到具有正确内插值的片段吗?

最佳答案

顶点着色器的输出是一个分量向量,vec4 gl_Position。来自核心 GL 4.4 规范的第 13.6 节坐标转换:

Clip coordinates for a vertex result from shader execution, which yields a vertex coordinate gl_Position.

Perspective division on clip coordinates yields normalized device coordinates, followed by a viewport transformation (see section 13.6.1) to convert these coordinates into window coordinates.

OpenGL 将透视划分为

device.xyz = gl_Position.xyz / gl_Position.w

但随后将 1/gl_Position.w 保留为 gl_FragCoord 的最后一个组件:

gl_FragCoord.xyz = device.xyz scaled to viewport
gl_FragCoord.w = 1 / gl_Position.w

此变换是双射的,因此不会丢失深度信息。事实上,正如我们在下面看到的,1/gl_Position.w 对于透视正确插值至关重要。

<小时/>

重心坐标简介

给定一个三角形(P0,P1,P2),我们可以通过顶点的线性组合来参数化三角形内的所有点:

P(b0,b1,b2) = P0*b0 + P1*b1 + P2*b2

其中 b0 + b1 + b2 = 1 且 b0 ≥ 0、b1 ≥ 0、b2 ≥ 0。

给定三角形内的点 P,满足上式的系数 (b0, b1, b2) 称为该点的重心坐标。对于非退化三角形,它们是唯一的,并且可以计算为以下三角形面积的商:

b0(P) = area(P, P1, P2) / area(P0, P1, P2)
b1(P) = area(P0, P, P2) / area(P0, P1, P2)
b2(P) = area(P0, P1, P) / area(P0, P1, P2)

每个 bi 可以被认为是“必须混合多少 Pi”。所以 b = (1,0,0), (0,1,0) 和 (0,0,1) 是三角形的顶点,(1/3, 1/3, 1/3) 是重心,等等。

给定三角形顶点上的属性 (f0, f1, f2),我们现在可以将其插值到内部:

f(P) = f0*b0(P) + f1*b1(P) + f2*b2(P)

这是 P 的线性函数,因此它是给定三角形上的唯一线性插值。数学也适用于 2D 或 3D。

透视正确插值

假设我们在屏幕上填充了一个投影的 2D 三角形。对于每个片段,我们都有它的窗口坐标。首先,我们通过反转 P(b0,b1,b2) 函数来计算其重心坐标,该函数是窗口坐标中的线性函数。这为我们提供了二维三角形投影上片段的重心坐标。

属性的透视正确插值将在剪辑坐标(以及扩展的世界坐标)中线性变化。为此,我们需要获取片段在剪辑空间中的重心坐标。

事实上(参见 [1][2] ),片段的深度在窗口坐标中不是线性的,而是深度逆 (1/gl_Position.w)是。因此,当通过深度倒数加权时,属性和剪辑空间重心坐标在窗口坐标中线性变化。

因此,我们通过以下方式计算校正重心的透视:

     ( 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

然后用它来插入顶点的属性。

注意: GL_NV_fragment_shader_barycentric通过 gl_BaryCoordNoPerspNV 公开设备线性重心坐标,并通过 gl_BaryCoordNV 校正透视。

实现

这里是一个 C++ 代码,它以类似于 OpenGL 的方式在 CPU 上对三角形进行光栅化和着色。我鼓励您将其与下面列出的着色器进行比较:

struct Renderbuffer { int w, h, ys; void *data; };
struct Vert { vec4 position, texcoord, color; };
struct Varying { vec4 texcoord, color; };

void vertex_shader(const Vert &in, vec4 &gl_Position, Varying &OUT) {
    OUT.texcoord = in.texcoord;
    OUT.color = in.color;
    gl_Position = vec4(in.position.x, in.position.y, -2*in.position.z - 2*in.position.w, -in.position.z);
}

void fragment_shader(vec4 &gl_FragCoord, const Varying &IN, vec4 &OUT) {
    OUT = IN.color;
    vec2 wrapped = IN.texcoord.xy - floor(IN.texcoord.xy);
    bool brighter = (wrapped[0] < 0.5) != (wrapped[1] < 0.5);
    if(!brighter)
        OUT.rgb *= 0.5f;
}

// render output unit/render operations pipeline
void rop(Renderbuffer &buf, int x, int y, const vec4 &c) {
    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] = lround(c[3]*255);
}

void draw_triangle(Renderbuffer &color_attachment, const box2 &viewport, const Vert *verts) {
    auto area = [](const vec2 &p0, const vec2 &p1, const vec2 &p2) { return cross(p1 - p0, p2 - p0); };
    auto interpolate = [](const auto a[3], auto p, const vec3 &coord) { return coord.x*a[0].*p + coord.y*a[1].*p + coord.z*a[2].*p; };

    Varying perVertex[3];
    vec4 gl_Position[3];

    box2 aabb = { viewport.hi, viewport.lo };
    for(int i = 0; i < 3; ++i) {
        vertex_shader(verts[i], gl_Position[i], perVertex[i]);

        // convert to normalized device coordinates
        gl_Position[i].w = 1/gl_Position[i].w;
        gl_Position[i].xyz *= gl_Position[i].w;

        // convert to window coordinates
        gl_Position[i].xy = mix(viewport.lo, viewport.hi, 0.5f*(gl_Position[i].xy + 1.0f));
        aabb = join(aabb, gl_Position[i].xy);
    }

    const float denom = 1/area(gl_Position[0].xy, gl_Position[1].xy, gl_Position[2].xy);

    // loop over all pixels in the rectangle bounding the triangle
    const ibox2 iaabb = lround(aabb);
    for(int y = iaabb.lo.y; y < iaabb.hi.y; ++y)
    for(int x = iaabb.lo.x; x < iaabb.hi.x; ++x)
    {
        vec4 gl_FragCoord;
        gl_FragCoord.xy = vec2(x, y) + 0.5f;

        // fragment barycentric coordinates in window coordinates
        const vec3 barycentric = denom*vec3(
            area(gl_FragCoord.xy, gl_Position[1].xy, gl_Position[2].xy),
            area(gl_Position[0].xy, gl_FragCoord.xy, gl_Position[2].xy),
            area(gl_Position[0].xy, gl_Position[1].xy, gl_FragCoord.xy)
        );

        // discard fragment outside the triangle. this doesn't handle edges correctly.
        if(barycentric.x < 0 || barycentric.y < 0 || barycentric.z < 0)
            continue;

        // interpolate inverse depth linearly
        gl_FragCoord.z = interpolate(gl_Position, &vec4::z, barycentric);
        gl_FragCoord.w = interpolate(gl_Position, &vec4::w, barycentric);

        // clip fragments to the near/far planes (as if by GL_ZERO_TO_ONE)
        if(gl_FragCoord.z < 0 || gl_FragCoord.z > 1)
            continue;

        // convert to perspective correct (clip-space) barycentric
        const vec3 perspective = 1/gl_FragCoord.w*barycentric*vec3(gl_Position[0].w, gl_Position[1].w, gl_Position[2].w);

        // interpolate attributes
        Varying varying = {
            interpolate(perVertex, &Varying::texcoord, perspective),
            interpolate(perVertex, &Varying::color, perspective),
        };

        vec4 color;
        fragment_shader(gl_FragCoord, varying, color);
        rop(color_attachment, x, y, color);
    }
}

int main(int argc, char *argv[]) {
    Renderbuffer buffer = { 512, 512, 512*4 };
    buffer.data = calloc(buffer.ys, buffer.h);

    // VAO 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 } },
    };

    box2 viewport = { 0, 0, buffer.w, buffer.h };
    draw_triangle(buffer, viewport, verts);

    stbi_write_png("out.png", buffer.w, buffer.h, 4, buffer.data, buffer.ys);
}

OpenGL 着色器

以下是用于生成引用图像的 OpenGL 着色器。

顶点着色器:

#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 Varying { vec4 texcoord; vec4 color; } OUT;
void main() {
    OUT.texcoord = texcoord;
    OUT.color = color;
    gl_Position = vec4(position.x, position.y, -2*position.z - 2*position.w, -position.z);
}

片段着色器:

#version 450 core
layout(location = 0) in Varying { vec4 texcoord; vec4 color; } IN;
layout(location = 0) out vec4 OUT;
void main() {
    OUT = IN.color;
    vec2 wrapped = fract(IN.texcoord.xy);
    bool brighter = (wrapped.x < 0.5) != (wrapped.y < 0.5);
    if(!brighter)
        OUT.rgb *= 0.5;
}

结果

以下是由 C++(左)和 OpenGL(右)代码生成的几乎相同的图像:

差异是由不同的精度和舍入模式引起的。

为了进行比较,下面是一个透视不正确的透视图(在上面的代码中使用 barycentric 而不是 perspective 进行插值):

关于opengl - OpenGL到底是如何进行透视校正线性插值的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24441631/

相关文章:

c++ - Phong-Alpha Material 透明度

c++ - Mac 上的自制 GLFW/GLEW

Android Bitmap.createScaledBitmap 在 Jelly Bean 4.1 上抛出 java.lang.OutOfMemoryError moSTLy

c# - 如何在 C# 中克隆图形?

cocoa OpenGL : draw texture and round corners

c++ - 在 C++ 中制作 3d 文本编辑器

OpenGL 碰撞检测

javascript - 墨卡托经度和纬度计算到裁剪 map (英国)上的 x 和 y

openlayers - 如何将 OpenLayers 3 与 Proj4js 一起使用

algorithm - 从向量的大小中减去一个常数(缩短向量)而不使用平方根?