OpenGL如何精确地透视校正线性插值? [英] How exactly does OpenGL do perspectively correct linear interpolation?

查看:163
本文介绍了OpenGL如何精确地透视校正线性插值?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如果在OpenGL管线的栅格化阶段发生线性插值,并且顶点已经转换为屏幕空间,则用于透视正确插值的深度信息从何而来?

If linear interpolation happens during the rasterization stage in the OpenGL pipeline, and the vertices have already been transformed to screen-space, where does the depth information used for perspectively correct interpolation come from?

有人能详细说明OpenGL如何从屏幕空间基元变成具有正确插值的片段吗?

Can anybody give a detailed description of how OpenGL goes from screen-space primitives to fragments with correctly interpolated values?

推荐答案

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

The output of a vertex shader is a four component vector, vec4 gl_Position. From Section 13.6 Coordinate Transformations of core GL 4.4 spec:

着色器执行的顶点结果的

修剪坐标,产生顶点坐标gl_Position.

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

剪辑片段的透视划分产生归一化的设备坐标,然后进行 viewport 转换(请参见第13.6.1节),以将这些坐标转换为窗口坐标.

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将视角划分为

OpenGL does the perspective divide as

device.xyz = gl_Position.xyz / gl_Position.w

但是将1 / gl_Position.w保留为gl_FragCoord的最后一个组成部分:

But then keeps the 1 / gl_Position.w as the last component of gl_FragCoord:

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

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

This transform is bijective, so no depth information is lost. In fact as we see below, the 1 / gl_Position.w is crucial for perspective correct interpolation.

给出三角形(P0,P1,P2)的一种方法是对三角形内的点进行参数化,方法是选择一个顶点(此处为P0),并将每个点表示为:

Given a triangle (P0, P1, P2) one way to parametrize the points inside the triangle is by choosing one vertex (here P0) and expressing each other point as:

P(u,v) = P0 + (P1 - P0)u + (P2 - P0)v

其中u≥0,v≥0且u + v≤1.给定三角形顶点的属性(f0,f1,f2),我们可以使用u,v在内部进行插值:

where u ≥ 0, v ≥ 0 and u + v ≤ 1. Given an attribute (f0, f1, f2) on the vertices of the triangle, we can use u, v to interpolate it over the interior:

f(u,v) = f0 + (f1 - f0)u + (f2 - f0)v

所有数学运算都可以使用上述参数化来完成,实际上,由于计算速度更快,因此有时更可取.但是,它不太方便,并且存在数字问题(例如P(1,0)可能不等于P1).

All math can be done using the above parametrization, and in fact is sometimes preferable due to faster calculations. However it is less convenient and has numerical issues (e.g. P(1,0) might not equal P1).

通常使用重心坐标.三角形内的每个点都是顶点的加权和:

Instead barycentric coordinates are usually used. Every point inside the triangle is a weighted sum of the vertices:

P(b0,b1,b2) = P0*b0 + P1*b1 + P2*b2
f(b0,b1,b2) = f0*b0 + f1*b1 + f2*b2

其中b0 + b1 + b2 = 1,b0≥0,b1≥0,b2≥0是三角形中点的重心坐标.每个bi都可以认为是必须混入多少Pi".所以b =(1,0,0),(0,1,0)和(0,0,1)是三角形的顶点,(1/3,1/3,1/3)是重心,等等.

where b0 + b1 + b2 = 1, b0 ≥ 0, b1 ≥ 0, b2 ≥ 0 are the barycentric coordinates of the point in the triangle. Each bi can be thought of as 'how much of Pi has to be mixed in'. So b = (1,0,0), (0,1,0) and (0,0,1) are the vertices of the triangle, (1/3, 1/3, 1/3) is the barycenter, and so on.

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

So let's say we fill a projected 2D triangle on the screen. For every fragment we have its window coordinates. First we calculate its barycentric coordinates by inverting the P(b0,b1,b2) function, which is a linear function in window coordinates. This gives us the barycentric coordinates of the fragment on the 2D triangle projection.

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

Perspective correct interpolation of an attribute would vary linearly in the clip coordinates (and by extension, world coordinates). For that we need to get the barycentric coordinates of the fragment in clip space.

发生时(请参见 [1] [2] ),片段在窗口坐标中不是线性的,但逆深度(1/gl_Position.w)是线性的.因此,属性和剪辑空间重心坐标在被深度反比加权时,在窗口坐标中呈线性变化.

As it happens (see [1] and [2]), the depth of the fragment is not linear in window coordinates, but the depth inverse (1/gl_Position.w) is. Accordingly the attributes and the clip-space barycentric coordinates, when weighted by the depth inverse, vary linearly in window coordinates.

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

Therefore, we compute the perspective corrected barycentric by:

     ( 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

然后使用它从顶点内​​插属性.

and then use it to interpolate the attributes from the vertices.

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

Note: GL_NV_fragment_shader_barycentric exposes the device-linear barycentric coordinates through gl_BaryCoordNoPerspNV and the perspective corrected through gl_BaryCoordNV.

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

Here is a C++ code that rasterizes and shades a triangle on the CPU, in a manner similar to OpenGL. I encourage you to compare it with the shaders listed below:

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 = calloc(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);

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

OpenGL着色器

这是用于生成参考图像的OpenGL着色器.

OpenGL shaders

Here are the OpenGL shaders used to generate the reference image.

顶点着色器:

#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]);
}

片段着色器:

#version 450 core
layout(location = 0) in PerVertex {
    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[0] < 0.5) != (wrapped[1] < 0.5);
    if(!brighter)
        OUT.rgb *= 0.5;
}

结果

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

Results

Here are the almost identical images generated by the C++ (left) and OpenGL (right) code:

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

The differences are caused by different precision and rounding modes.

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

For comparison, here is one that is not perspective correct (uses barycentric instead of perspective for the interpolation in the code above):

这篇关于OpenGL如何精确地透视校正线性插值?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆