如何通过 OpenGL 直接使用屏幕空间坐标?

Rad*_*yan 7 c# opengl shader glsl opentk

我已经按照我在https://learnopengl.com/上找到的指南进行操作,并设法在窗口上渲染了一个三角形,但整个过程对我来说似乎过于复杂。一直在互联网上搜索,发现了很多类似的问题,但它们似乎都过时了,而且没有一个给出满意的答案。

我已经阅读了上面提到的网站上的指南Hello Triangle,其中有一张解释图形管道的图片,由此我了解了为什么在屏幕上绘制三角形这样看似简单的任务需要如此多的步骤。

我还阅读了同一站点的坐标系统指南,它告诉我什么是 OpenGL 使用的“奇怪的(对我来说)坐标”(NDC)以及它为什么使用它。

C

(这是上面提到的指南中的图片,我认为它对于描述我的问题很有用。)


问题是:我可以直接使用最终的 SCREEN SPACE 坐标吗?

我想要的只是进行一些 2D 渲染(无 z 轴)并且屏幕尺寸已知(固定),因此我不明白为什么我应该使用标准化坐标系而不是绑定到屏幕的特殊坐标系。

例如:在 320x240 屏幕上,(0,0)代表左上角像素,(319,239)代表右下角像素。它不需要完全像我所描述的那样,其想法是每个整数坐标 = 屏幕上的相应像素。

我知道可以设置这样一个坐标系供我自己使用,但是坐标会四处变换并最终返回到屏幕空间 -这就是我首先拥有的。所有这些对我来说似乎都是浪费工作,而且坐标转换时不会导致精度损失吗?


引用坐标系指南(上图):

  1. 坐标位于视图空间后,我们希望将它们投影到剪辑坐标。剪辑坐标被处理为 -1.0 和 1.0 范围,并确定哪些顶点将最终出现在屏幕上。

因此,考虑在 1024x768 屏幕上,我将剪辑坐标定义为 (0,0) 到 (1024,678),其中:

(0,0)--------(1,0)--------(2,0)  
  |            |            |    
  |   First    |            |    
  |   Pixel    |            |    
  |            |            |    
(0,1)--------(1,1)--------(2,1)        . . .
  |            |            |    
  |            |            |    
  |            |            |    
  |            |            |    
(0,2)--------(1,2)--------(2,2)  

              .
              .
              .
                                          (1022,766)---(1023,766)---(1024,766)
                                               |            |            |
                                               |            |            |
                                               |            |            |
                                               |            |            |
                                          (1022,767)---(1023,767)---(1024,767)
                                               |            |            |
                                               |            |   Last     |
                                               |            |   Pixel    |
                                               |            |            |
                                          (1022,768)---(1023,768)---(1024,768)
Run Code Online (Sandbox Code Playgroud)

假设我想在 处放置一张图片Pixel(11,11),那么该图片的剪辑坐标就是Clip(11.5,11.5)该坐标,然后将其处理为 -1.0 和 1.0 范围:

11.5f * 2 / 1024 - 1.0f = -0.977539063f // x
11.5f * 2 /  768 - 1.0f = -0.970052063f // y
Run Code Online (Sandbox Code Playgroud)

我有NDC(-0.977539063f,-0.970052063f)

  1. 最后,我们将剪辑坐标转换为屏幕坐标,这个过程称为视口转换,将坐标从 -1.0 和 1.0 转换为 glViewport 定义的坐标范围。然后将所得坐标发送到光栅器以将其转换为片段。

因此,获取 NDC 坐标并将其转换回屏幕坐标:

(-0.977539063f + 1.0f) * 1024 / 2 = 11.5f        // exact
(-0.970052063f + 1.0f) *  768 / 2 = 11.5000076f  // error
Run Code Online (Sandbox Code Playgroud)

x 轴是准确的,因为 1024 是 2 的幂,但由于 768 不准确,所以 y 轴不准确。误差很小,但不完全是 11.5f,所以我猜会发生某种混合,而不是原始图片的 1:1 表示?


为了避免上面提到的舍入误差,我做了这样的事情:

首先,我将视口大小设置为大于窗口的大小,并将宽度和高度设置为 2 的幂:

GL.Viewport(0, 240 - 256, 512, 256); // Window Size is 320x240
Run Code Online (Sandbox Code Playgroud)

然后我像这样设置顶点坐标:

float[] vertices = {
    //  x       y
      0.5f,   0.5f, 0.0f, // top-left
    319.5f,   0.5f, 0.0f, // top-right
    319.5f, 239.5f, 0.0f, // bottom-right
      0.5f, 239.5f, 0.0f, // bottom-left
};
Run Code Online (Sandbox Code Playgroud)

我在顶点着色器中手动转换它们:

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x * 2 / 512 - 1.0, 0.0 - (aPos.y * 2 / 256 - 1.0), 0.0, 1.0);
}
Run Code Online (Sandbox Code Playgroud)

最后画一个四边形,结果是:

渲染结果

这似乎产生了正确的结果(四边形的尺寸为 320x240),但我想知道是否有必要执行所有这些操作。

  1. 我的方法有什么缺点?
  2. 有没有更好的方法来实现我所做的事情?

线框模式下的渲染似乎隐藏了问题。我尝试将纹理应用到我的四边形(实际上我切换到了 2 个三角形),并且在不同的 GPU 上得到了不同的结果,但它们对我来说似乎都不正确:

结果_无_纹理 左:英特尔 HD4000 | 右:Nvidia GT635M(擎天柱)

我设置GL.ClearColor为白色并禁用纹理。

虽然这两个结果都填充了窗口客户端区域 (320x240),但 Intel 给了我一个放置在左上角的正方形大小 319x239,而 Nvidia 给了我一个放置在左下角的正方形大小 319x239。

结果纹理 这就是纹理打开后的样子。

质地:

质地 (我将其垂直翻转,这样我可以更轻松地在代码中加载它)

顶点:

float[] vertices_with_texture = {
    //  x       y           texture x     texture y
      0.5f,   0.5f,        0.5f / 512, 511.5f / 512, // top-left
    319.5f,   0.5f,      319.5f / 512, 511.5f / 512, // top-right
    319.5f, 239.5f,      319.5f / 512, 271.5f / 512, // bottom-right ( 511.5f - height 240 = 271.5f)
      0.5f, 239.5f,        0.5f / 512, 271.5f / 512, // bottom-left
};
Run Code Online (Sandbox Code Playgroud)

现在我完全陷入困境了。

我以为我将四边形的边缘放置在精确的像素中心(.5)上,并且我也在精确的像素中心(.5)处对纹理进行采样,但是两张卡给了我两个不同的结果,并且没有一个是正确的(缩放你可以看到图像的中心有点模糊,不是清晰的棋盘图案)

我缺少什么?


我想我现在知道该怎么做了,我已经发布了解决方案作为答案,并将这个问题留在这里供参考。

Rab*_*d76 4

我可以直接使用最终的 SCREEN SPACE 坐标吗

不,你不能。你必须变换坐标。在 CPU 上或在着色器 (GPU) 中。

如果你想使用窗口坐标,那么你必须设置一个正交投影矩阵,它将坐标从 x: [-1.0, 1.0], y: [-1.0, 1.0] (标准化设备空间)转换为窗口坐标x:[0, 320],y:[240, 0]。

例如通过使用glm::ortho

glm::mat4 projection = glm::orhto(0, 320.0f, 240.0f, 0, -1.0f, 1.0f);
Run Code Online (Sandbox Code Playgroud)

例如OpenTKMatrix4.CreateOrthographic

OpenTK.Matrix4 projection = 
    OpenTK.Matrix4.CreateOrthographic(0, 320.0f, 240.0f, 0, -1.0f, 1.0f);
Run Code Online (Sandbox Code Playgroud)

在顶点着色器中,顶点坐标必须乘以投影矩阵

in vec3 vertex;

uniform mat4 projection;

void main()
{
    gl_Position = projection * vec4(vertex.xyz, 1.0);
}
Run Code Online (Sandbox Code Playgroud)

为了完整起见Legacy OpenGLglOrtho:(
不要使用旧的和已弃用的遗留 OpenGL 功能)

glOrtho(0.0, 320.0, 240.0, 0.0, -1.0, 1.0); 
Run Code Online (Sandbox Code Playgroud)