OpenGL中的2D绘图:在本地大小下使用像素精度的线性过滤。

18

简短版:

在OpenGL中,是否有一种通用方法可以实现从图集中准确绘制大小相同(包括边缘像素)的纹理的2D绘制,并在过滤到非原生大小时实现良好的质量缩放?

问题的详细说明:

假设您正在编写某种系统,以便从纹理图集中绘制精灵,使用户可以指定要绘制的精灵的位置、大小,还可以只绘制精灵的子矩形。

例如,这是一个64x64的棋盘格:

checkerboard

...以及它在纹理图集中的样子:

checkerboard in texture atlas

注意: 我们将假设视口设置为1:1映射到设备像素。

方法

为清晰起见:请注意下面的前两种方法不会产生预期的结果,仅用于概述不起作用的内容。

1. 仅使用GL_NEAREST在原生大小下绘制精灵

  • 设置OpenGL使用GL_NEAREST min/mag过滤
  • 从(x,y)到(x+64,y+64)放置顶点以绘制
  • 使用纹理坐标(0,0)->(64/atlas_size,64/atlas_size)

这对于在原生大小下绘制是可以的,但是当用户以不同于64x64的大小绘制对象时,NEAREST过滤会产生较差的结果。它还会强制将纹素对齐到像素网格,导致在非整数像素位置绘制对象时效果不佳:

使用gl_nearest在像素偏移0.25处绘制 使用gl_nearest在像素偏移0.5处绘制

2. 启用GL_LINEAR

  • 对于线性过滤,我们需要将uv坐标移到纹素中心:(0.5/atlas_size,0.5/atlas_size)(63.5/atlas_size,63.5/atlas_size)。否则,对于最外层的像素,线性过滤会从纹理集合中采样精灵相邻的纹素。
  • 我们还需要修改顶点,因为继续使用从(0,0) 到 (64,64)的顶点会在两个方向上拉伸纹理1px,如下所示:

uv坐标向内移动0.5纹素导致拉伸

  • 因此,我们需要使用从(0.5,0.5)到(63.5,63.5)的顶点。通常,在以其原生大小(a,b)的比例绘制纹理时,我们需要将顶点向“内部”移动,我认为是(a / 2,b / 2)。这将产生以下结果(在紫色背景上):

uv向内移动0.5纹素,顶点向内移动0.5像素

请注意,我们获得了像素精确的绘制,除了边缘像素会与背景混合,因为顶点边界正好位于两个像素之间。

编辑:还要注意,此行为还取决于是否启用抗锯齿。如果没有启用,则先前的方法实际上可以提供像素完美的渲染,但在精灵从像素对齐到亚像素位置移动时不会提供良好的转换。而且在许多情况下,抗锯齿是必需的。

3. 在纹理集中填充精灵

解决边缘像素问题的两个明显方法是在纹理集中的精灵边缘填充1px的边框。您可以选择以下其中之一:

  • 用1层透明像素填充,并将顶点 扩展 (0.5a,0.5b) 而不是收缩它们
  • 用精灵最外层像素的第二个副本进行填充,并返回从 (0,0) 到 (64/atlas_size, 64/atlas_size) 对纹理进行采样

这基本上为我们提供了具有线性缩放的像素准确绘制。然而,如果我们允许用户只绘制精灵的子矩形,这两种方法都会失败,因为子矩形显然没有所需的填充。

我有漏掉了什么吗?是否有一个通用的解决方案来解决这个问题?


1
我与业内的每个人交谈时,他们总是告诉我填充是最好的方法;我没有听说过更优雅的解决方案。 - Matt Kline
我同意,填充是最好的选择。我也不知道如何处理子矩形,但这与纹理集无关。 - Andreas Haferburg
感谢Slavik和Andreas,我也是这么想的。Andreas,你说得很对,我只是在考虑地图集方面的问题!当然,在处理整个纹理绘制时,您可以使用CLAMP_TO_EDGE。与子矩形一样,使用地图集精灵时也会出现同样的问题,但在前者的情况下,填充可作为可能的解决方案(而在后者中显然不行!) - Benji XVI
2
你可以使用数组纹理 (http://www.opengl.org/wiki/Array_Texture) 吗?如果你正在使用着色器,并且所有精灵都是相同大小的,那么它们具有一个优点,即你可以使用 GL_CLAMP_TO_EDGE (或 GL_REPEAT),而精灵将永远不会从其他精灵借用纹素。 - GuyRT
谢谢,Texture Arrays 看起来是一个有趣的解决方案,适用于相当数量的纹理(在这个 MBP 上查询 GL_MAX_ARRAY_TEXTURE_LAYERS 返回 512)。然而,在 OpenGL ES 上它们不可用,这使得它对我的情况不适用。填充似乎是必要的解决方法。 - Benji XVI
1个回答

2
很难确切地知道你正在寻找的“正确方式”是什么。 如果您有一个64x64的精灵,并且想将其缩放到65x65或63x63,无论如何都不会使其看起来好看。 当您将抗锯齿功能加入混合时,请记住多重采样不是超级采样,因此您的纹理不会神奇地变得更柔和。
话虽如此,真正的非最近过滤应该可以在多重采样中轻松工作。 我认为您的GL_LINEAR方法是正确的,但我认为您可能遇到了实现问题。
特别是,线性过滤应该在沿着纹素边界过滤时进行。 例如,如果您有两个相邻的三角形,则会发生这种情况。 实际上,您应该期望在精灵边缘沿着线性过滤,这种过滤是正确的。
您不应该尝试通过调整纹理坐标(因此是顶点)来纠正这个问题,因为这会错误地缩放纹理坐标跨越纹理。 相反,我建议在您的着色器中将纹理坐标夹紧到所需范围加/减0.5 / texture_res。
您会发现,这解决了本地缩放的像素完美结果以及良好质量的放大问题。 缩小看起来还不错,但我建议使用三线性(mipmap)过滤以获得最佳效果。

非最近邻过滤应该能够轻松地使用。 - genpfault
感谢您的反馈,Ian。该问题特别涉及像素完美的2D渲染,以及如何在启用线性过滤时实现最佳效果。(2D是唯一一个“原生尺寸下像素完美渲染”真正有意义的地方)就像您所说,我们希望在精灵边缘进行过滤-因为纹理单元必须在其中心进行采样,但在呈现时映射到顶点上时,在缩放级别为1.0时,这些顶点被定位在屏幕像素的边缘处。因此,为了实现像素完美的2D渲染,您可以使用透明像素填充纹理并相应地调整坐标。 - Benji XVI
“我建议在着色器中将纹理坐标夹紧到所需范围加减0.5/texture_res。你能详细说明一下如何实现吗?我大概明白你的意思,但不是很确定。” - Benji XVI
1
类似于 texture2D(tex,clamp(uv,0.5/tex_res,vec2(1)-0.5/tex_res));,其中 uv 是纹理坐标,tex_res 是纹理的分辨率。 - geometrian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接