我在https://learnopengl.com/上找到的指南帮助我成功在窗口上渲染了一个三角形,但整个过程对我来说似乎过于复杂。我在互联网上搜索了很多类似的问题,但它们都似乎已经过时了,并没有给出令人满意的答案。
我阅读了上面提到的网站的Hello Triangle指南,其中有一张图解释了图形管线,这让我理解了为什么一个看似简单的任务(在屏幕上绘制一个三角形)需要那么多步骤。
我还阅读了同一网站的Coordinate Systems指南,它告诉我OpenGL使用的是“奇怪(对我来说)的坐标”(NDC),以及它为什么使用它。
(这是上述指南中的图片,我认为它有助于描述我的问题。)
问题是:我能直接使用最终的SCREEN SPACE坐标吗?
我想做一些2D渲染(没有z轴),并且屏幕尺寸已知(固定),因此我看不出为什么应该使用归一化坐标系而不是绑定到我的屏幕的特殊坐标系。
例如:在320x240的屏幕上,(0,0)
表示左上角的像素,(319,239)
表示右下角的像素。它不需要完全按照我描述的那样,每个整数坐标=屏幕上对应的像素。
我知道可以为自己设置这样一个坐标系统,但坐标将被转换到各个方向,最终又回到屏幕空间 - 这就是我一开始拥有的。所有这些似乎都是白费力气,而且当坐标被转换时,它们是否会引入精度损失?
来自Coordinate Systems指南的引用(上面的图片):
- 在坐标处于视图空间之后,我们想要将它们投影到剪辑坐标中。 剪辑坐标在-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)
假设我想在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
我有一个NDC(-0.977539063f,-0.970052063f)
- 最后,我们将裁剪坐标转换为屏幕坐标,这个过程称为视口变换,它将坐标从-1.0和1.0转换为由glViewport定义的坐标范围。 然后将结果坐标发送到光栅化器以将其转换为片段。
因此,将NDC坐标转换回屏幕坐标:
(-0.977539063f + 1.0f) * 1024 / 2 = 11.5f // exact
(-0.970052063f + 1.0f) * 768 / 2 = 11.5000076f // error
由于1024是2的幂次方,因此x轴非常准确,但由于768不是,所以y轴有些偏差。误差非常小,但并不完全是11.5f,所以我猜原始图片可能会进行某种混合处理,而不是1:1的表示。
为避免上述提到的舍入误差,我做了如下操作:
首先,我将视口大小设置为比窗口大的大小,并使宽度和高度成为2的幂次方:
GL.Viewport(0, 240 - 256, 512, 256); // Window Size is 320x240
然后我设置了顶点的坐标,如下:
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
};
我手动在顶点着色器中进行转换:
#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);
}
最后我画了一个四边形,结果如下: 这似乎产生了正确的结果(四边形大小为320x240),但我想知道是否有必要这样做。
以下是我的问题:
- 我的方法有什么缺点?
- 是否有更好的方法来实现我所做的事情?
似乎以线框模式渲染时隐藏了问题。我试图将纹理应用到我的四边形上(实际上我切换为两个三角形),在不同的GPU上得到了不同的结果,并且其中没有一个看起来正确:
左:Intel HD4000 | 右:Nvidia GT635M(optimus)
我将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
};
现在我完全卡住了。
我认为我将四边形的边缘放在精确的像素中心(.5),并且我也在精确的像素中心(.5)处采样纹理,但两张图片给我两个不同的结果,而且都不正确(放大后可以看到图像中心略微模糊,没有明显的棋盘格纹)
我错过了什么吗?
我想我现在知道该怎么做了,我已经发布了解决方案作为答案,并将这个问题留在这里供参考。