· 5 分钟阅读 · 1784 字

重复纹理接缝突然变糊:取小数后的 UV 丢失了连续导数

一面重复铺砖的墙,绝大部分区域清晰,整数 UV 接缝处却周期性出现模糊带。材质里看起来很自然的一句 frac(uv),已经足以改变 Mip 选择:两个相邻片元的 U 从 0.999 走到 1.001,原本只跨过 0.002;取小数后变成 0.999 与 0.001,差值突然接近整张纹理。

连续细线跨过重复纸面图案接缝的摄影概念图

本文固定一张 1024×1024 的方形纹理,只沿 U 方向变化,V 方向梯度为零。连续坐标给出的足迹约为 2.048 纹素,对应 LOD 1.034216;折回单周期再求差,足迹变成 1021.952 纹素,对应 LOD 9.997112。纹理像素和 UV 重复频率没有变化,变化发生在采样器收到的梯度。这里需要修复的是足迹信息;加大纹理尺寸会让两个 LOD 同时上移,并不消除它们之间约九级的差距。

同一个采样位置,可以携带不同的足迹

重复寻址把任意坐标映射到单周期,数学定义为 u-floor(u)。它只回答读哪一段纹理;Mip 层级还需要知道屏幕上的一步对应多少纹素。对本文只有单一坐标分量变化、方形纹理、各向同性估计的输入,可写成:

rho = max(abs(du/dx), abs(du/dy)) * textureSize
lod = max(0, log2(max(rho, 1)))

实际二维 UV 要分别计算 x、y 屏幕方向的二维纹素梯度长度,矩形纹理还要分别乘宽、高。各向异性过滤、LOD 偏置和采样器上下限会继续影响最终取样。本篇数值用于隔离重复接缝造成的错误,不把这条标量公式当成完整硬件采样模型。

代码位于 $WORK_DIR/RepeatFootprint.cs,Compare 同时保留连续差值和折回后的差值。这里 $WORK_DIR 指本篇独立研究探针目录,输入均为有限双精度数值:

public static class RepeatFootprint
{
    public static double Wrap(double coordinate) => coordinate - Math.Floor(coordinate);

    public static double EstimateLod(double dx, double dy, int textureSize)
    {
        if (!double.IsFinite(dx) || !double.IsFinite(dy) || textureSize <= 0)
            throw new ArgumentOutOfRangeException(nameof(textureSize));
        double footprint = Math.Max(Math.Abs(dx), Math.Abs(dy)) * textureSize;
        return Math.Max(0, Math.Log2(Math.Max(footprint, 1)));
    }

    public static (double Correct, double Wrapped) Compare(
        double left, double right, double verticalStep, int textureSize)
    {
        return (
            EstimateLod(right - left, verticalStep, textureSize),
            EstimateLod(Wrap(right) - Wrap(left), verticalStep, textureSize));
    }
}

把同样的 0.002 差值放在周期内部,0.499→0.501 的两种算法都得到 LOD 1.034216。跨越整数边界才出现异常,说明“纹理本身太糊”不能解释这种空间分布。负坐标也一样:-0.001→0.001 折回后是 0.999→0.001,同样得到约 1022 纹素的错误足迹。用向零截断或余数替换 floor,还会额外破坏负坐标的重复规则。

连续UV与折回UV的固定输入、足迹及LOD对照

在折回之前保留梯度

如果只是重复整张纹理,优先把连续 UV 直接交给配置为 Repeat 的采样器。让寻址阶段负责重复,材质不必自己制造坐标断点。若某条材质路径确实需要显式折回,必须先从连续坐标取得梯度,再把折回后的采样位置与原梯度一起传给显式梯度采样。

下面的 HLSL 函数表达这一接口合同。tiles 在导数邻域内为常量,sourceUv 连续;梯度在片元阶段、分歧分支之前计算。纹理与采样器由调用方传入,采样器使用 Repeat 寻址:

float4 SampleRepeated(
    Texture2D<float4> colorTexture,
    SamplerState repeatSampler,
    float2 sourceUv,
    float2 tiles)
{
    float2 continuousUv = sourceUv * tiles;
    float2 gradientX = ddx(continuousUv);
    float2 gradientY = ddy(continuousUv);
    return colorTexture.SampleGrad(
        repeatSampler, frac(continuousUv), gradientX, gradientY);
}

tiles 必须进入求导前的乘法。只对原始 sourceUv 求导,再用乘过平铺次数的坐标采样,会低估缩小比例;若 tiles 本身随片元变化,梯度还必须包含它的变化。工程上应把“连续坐标及其梯度”视为同一份材质数据,坐标经过缩放、旋转或其他连续变换时一起更新,直到真正需要周期化的位置才拆开处理。

这段着色器是对应数值合同的接入实现,当前探针没有编译或运行 GPU Shader。CPU 测试验证了固定邻域的差值与 LOD 估计;具体图形 API 的导数分组、边缘辅助片元和过滤结果仍需在目标渲染管线中验收。对于全周期纹理,保留原始连续 UV 并使用 Repeat 更直接,也减少了材质图中坐标与梯度分离后被误接的机会。

强制采样第零级会让模糊带消失,却同时删除正常缩小时的过滤。另一个固定输入 0.99→1.01 覆盖 20.48 纹素,连续梯度应得到 LOD 4.356144;强行选择 LOD 0 会把远处细节重新送回一个像素中。修复接缝时必须保留这类正常缩小行为,不能以局部清晰代替正确的屏幕足迹。

回归同时检查接缝与正常缩小

Program.cs 的 Check 对周期内部、正负整数接缝、零梯度、放大、分辨率翻倍与非法输入执行断言。关键回归直接使用 RepeatFootprint.Compare 的输出:

var seam = RepeatFootprint.Compare(0.999, 1.001, 0, 1024);
Check("seam correct lod", Near(seam.Correct, Math.Log2(2.048)));
Check("wrapped seam lod", Near(seam.Wrapped, Math.Log2(1021.952)));
var minified = RepeatFootprint.Compare(0.99, 1.01, 0, 1024);
Check("minification preserved", Near(minified.Correct, Math.Log2(20.48)));

2026-10-29 在 .NET 9 Release 环境执行:

dotnet build $WORK_DIR/RepeatGradientLab.csproj -c Release
dotnet run --project $WORK_DIR/RepeatGradientLab.csproj -c Release --no-build

构建退出码 0,零警告、零错误;运行退出码 0,通过 19 项、失败 0、跳过 0,探针内部计时 38.8735 ms。固定输入的决定性输出为:

texture_size = 1024
correct_footprint_texels = 2.048
wrapped_footprint_texels = 1021.952
seam_correct_lod = 1.034216
seam_wrapped_lod = 9.997112
minified_correct_lod = 4.356144
samples = 10000
mismatch = 0
wrappedFailures = 10000

扫描的 10,000 组输入以整数 1 为中心,步长从 0.0001009 到 0.0091;连续差值与给定步长的参考 LOD 在 1e-9 容差内全部一致,折回差值则全部高估超过五级。把两端同时减去整数 7,连续算法最大 LOD 差异约为 1.31e-12。这些检查验证重复相位不应影响足迹;计时仅记录本次执行,不用于评价 Shader 性能。

这个修复只适用于周期内容与连续参数化之间的接缝。若图像左右边缘本来不相容,保留梯度仍会看见内容断裂;若把多个不相关图块放在图集中,Repeat 寻址也不能替代图块边界处理。材质评审应分开检查三件事:重复内容是否连续、采样位置是否正确、梯度是否仍描述变换后的连续表面。前两项正确而第三项丢失时,增加分辨率或调整 Mip 偏置都只能移动症状出现的位置。