如何手动包装纹理坐标?

时间:2015-02-11 02:47:18

标签: c++ filter textures hlsl

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

在坐标被“包裹”到0-1范围后,它们将被旋转,所以我不能简单地使用纹理采样器AddressU和AddressV属性来设置包装,因为它们需要被包裹然后旋转,所以它无法在采样器内完成。

这里的解决方案很简单,只需使用纹理坐标的小数部分,它就会换行。

以下是将纹理平铺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);

这会对纹理进行平铺,但会在纹理包裹的边界处产生问题。瓷砖必须均匀地划分到它们正在显示的空间中,或者它形成了边界相遇的接缝。绘制纹理的方块是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,这导致其向后插值穿过纹理。但是,当我旋转纹理坐标时,根本没有任何变化。即使每个瓷砖都应用了随机旋转。在这种情况下,边缘具有彼此接壤的各种不同的值,不仅左侧的值较高而右侧的值较小,而且接缝的区域不会发生变化。

3 个答案:

答案 0 :(得分:2)

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

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

enter image description here

如果纹理变得失真,最高级别的采样将导致过采样(像工件那样的点过滤)。为了对抗过采样,纹理查找根据屏幕空间中纹理坐标的变化选择适当的mipmaplevel。在你的情况下,边框在一个小的地方是一个非常高的变化,这导致使用最低的mipmap(这是你看到一个小的红点,这是红色边框的原因)。

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

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。这应该删除你的文物。

如果纹理不需要mipmapping,因为渲染纹理屏幕并且纹理大小不会导致过采样,您可以使用更简单的替代sampleLevel({{3你可以在那里传递一个参数,它可以选择一个特定的mipmap,你可以自己决定。

答案 1 :(得分:1)

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

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

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

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

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

答案 2 :(得分:0)

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);

如果你根本不需要mipmapping,那么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);