如何手动包装纹理坐标?

How to wrap texture coordinates manually?

我正在使用 C++ 和 HLSL,需要让我的纹理坐标换行,以便纹理平铺在一个三角形上。

坐标"wrapped"进入0-1范围后,它们会旋转,所以我不能简单地使用纹理采样器AddressU和AddressV属性设置为wrap,因为它们需要被wrapping然后旋转,所以它不能在采样器内完成。

这里的解决方法很简单,只需要使用纹理坐标的小数部分就可以了。

下面是一个像素着色器的示例,它将纹理平铺 36 次 (6 * 6):

input.tex *= 6.0f; //number of times to tile ^ 2
input.tex = frac(input.tex); //wraps texCoords to 0-1 range
return shaderTexture.Sample(SampleType, input.tex);

这确实会平铺纹理,但会在纹理环绕的边界处产生问题。瓷砖必须均匀地分成 space 它们正在显示的区域,否则它会在边界相遇的地方创建一个接缝。我的纹理绘制到的正方形是 800x600 像素,所以平铺 5 将均匀划分但 6 不会并且会导致沿 Y 轴的接缝。

我尝试使用模数运算符 input.tex = input.tex % 1 来包装坐标,但得到的结果完全相同。我还尝试过更改纹理过滤方法以及 AddressU 和 AddressV 属性以及无数种不同的调试方法。

我很幸运地使用了这段代码。如果 x 坐标太高,则设置为 0,如果太低,则设置为 1。

input.tex *= 6.0f;
input.tex = frac(input.tex);
if (input.tex.x > 0.999f) input.tex.x = 0;
if (input.tex.x < 0.001f) input.tex.x = 1;
return shaderTexture.Sample(SampleType, input.tex);

虽然这只能解决某些地方的问题,但绝对不是解决方案。

这是一张显示纹理(左)和手动包裹后的样子(右)的图片。可以看到并不是所有的寄宿生接触到的地方都有这个错误。

我也尝试过不将纹理坐标更改为 0-1 范围,而是围绕每个图块的中心旋转它们而不是 (0.5, 0.5),但我得到了相同的结果。此外,我的纹理坐标完全独立于顶点并在像素着色器内部计算。

我所看到的与此问题相关的任何事情都与在一个像素处具有高值然后在下一个像素处具有低值有关,例如 u = 0.95 和下一个像素 u = 0.03,这导致它在纹理上向后插值。但是当我旋转我的纹理坐标时,什么都没有改变。即使每个图块都应用了随机旋转。在这种情况下,边缘具有各种不同的值彼此相邻,不仅仅是左侧的高值和右侧的低值,但接缝出现的区域永远不会改变。

此处的代码导致在单个像素的范围内对整个纹理进行采样。例如,对于接缝处的两个相邻像素,一个 'u' 样本可能是 1.0-eps,下一个样本将是 0.0+eps,其中 eps 是一个小于宽度的数字纹素的。对输出像素进行插值时,您将从 1.0 .. 0.0 进行插值,对这两个样本之间的整个纹理进行采样。整个纹理的平均会导致 'greyness',即使您的输入纹理实际上不包含任何完全灰色的像素。

如果您需要在每个范围内旋转纹理坐标(例如 0..1、1..2 独立旋转),有几种方法可以解决这个问题。首先,您可以将插值从线性更改为点,这将避免纹素之间的插值。但是,如果您需要双线性插值,这可能是不可接受的。在这种情况下,您可以构建一个 'grid' 网格,并将输入纹理 0..1 映射到每个图块,图块纹理坐标在着色器中独立旋转。

另一个可能的解决方案是将坐标转换为 0..1 space,执行旋转,然后将它们转换回原来的 space。例如,你会这样做:

// pseudo-code:
int2 whole;
input.tex = modf(input.tex, whole);
input.tex = Rotate(input.tex); // do whatever rotation is needed
input.tex += whole;

这将确保包装没有任何间断。或者,您可以让您的旋转代码考虑非统一 space 纹理坐标。

正如 MuertoExcobito 所说,主要问题是在边界处,纹理坐标在单个像素中从 1 跳到 0。从语义上讲,整个纹理在这个像素中得到平均是正确的,但这不是由在这个像素中将纹理从 1 插值到 0 造成的。真正的原因是 mipmapping。

对于您的纹理,在加载它时会生成 mipmap。这意味着纹理获得多个 mipmap 级别,这些级别都是之前的一半大小。

如果纹理变得扭曲,最高级别的采样将导致过采样(像伪像一样的点过滤)。为了对抗过采样,纹理查找根据屏幕中纹理坐标的变化选择合适的 mipmaplevel space。在你的情况下,边界在一个小地方发生了非常大的变化,这导致尽可能使用最低的 mipmap(就像你看到的一个小红点,这就是红色边界的原因)。

回到您的问题,您应该通过使用纹理查找方法 SampleGrad (Docs) 来控制 mipmap。要获取像素纹理坐标的当前变化,您可以使用内部方法 ddxddy。它们 return 用于着色器中的任意变量,它如何在局部更改为相邻像素(正确的解释将深入本主题)。所以使用下面的代码不应该改变任何东西,因为它在语义上应该是相同的:

input.tex *= 6.0f; //number of times to tile ^ 2
input.tex = frac(input.tex); //wraps texCoords to 0-1 range
float2 xchange = ddx(input.tex);
float2 ychange = ddy(input.tex);
return shaderTexture.SampleGrad(SampleType, input.tex, xchange, ychange);

现在您可以应用您的代码来防止 xchange 和 ychange 发生大的变化,以强制图形设备使用更高的 mipmap。这应该会消除您的伪影。

如果您的纹理不需要 mipmap,因为您正在渲染纹理屏幕对齐并且纹理大小不会导致过采样,您可以使用更简单的替代方法 sampleLevelDocs ) 那里可以传递一个参数,它会选择一个特定的mipmap,你可以自己确定。

Gnietschow 发布了这个问题的答案,但我将添加一个答案来准确显示我是如何使用该答案的。 实际上,我什至不确定为什么会这样,我只知道即使使用其他倍数的平铺、各种纹理和随机旋转,它也能起作用。

input.tex *= 6.0f; //number of times to tile ^ 2
input.tex = frac(input.tex); //wraps texCoords to 0-1 range
float2 xchange = ddx(input.tex);
float2 ychange = ddy(input.tex);
if ((xchange.x < 0)) xchange = float2(0, 0);
if ((ychange.y < 0)) ychange = float2(0, 0);
return shaderTexture.SampleGrad(SampleType, input.tex, xchange, ychange);

如果您根本不需要 mipmap,Gniet 提到的其他方法也可以正常工作

input.tex *= 6.0f; //number of times to tile ^ 2
input.tex = frac(input.tex); //wraps texCoords to 0-1 range
return shaderTexture.SampleLevel(SampleType, input.tex, 0);