像素风格贴图采样抗锯齿

最近在使用wgpu做一个类似MineCraft的demo,当进行贴图采样时发现了一些问题。

由于texture很小(16px*16px)的缘故,默认的双线性采样、mipmap、各项异性抗锯齿都会有许多问题。一开始我想到了两个方案:

  1. 近处使用临近采样,远处使用双线性采样并开启mipmap和各项异性采样。
  2. 在把texture传入显存时对它做上采样到足够大。

方案一的效果很差,远处很模糊,抖动也很明显。

方案二更是要至少扩大为8倍的贴图才能有较好的结果,而这个预计会浪费64倍的显存,显然时不可接受的。

近处的狗牙,侧看时会更加显著

在网上搜索、询问一番后,我找到了合理的方法。

方法概述

本质也是邻近采样和双线性采样的结合,而且没有太多额外的开销。

在近处,大部分像素风格的贴图上的纹素都是显著大于屏幕像素的,此时大部分的像素可以采用邻近采样的方式直接获取颜色,而在纹素之间的像素则可以用插值的方法来做平滑过渡消除锯齿。而在足够远的地方就会退化为双线性采样并获得正确的mipmap和各项异性效果。

简单实现

先只考虑贴图正对屏幕的情况,WGSL的Shder如下:

@fragment
fn fs_main(
    in: VertexOutput
) -> @location(0) vec4f {

    //使用了texture array
    let texture: texture_2d<f32> = texture_array[in.texture_index];
    let texture_size = vec2<f32>(textureDimensions(texture));


    let tx = frac(in.uv * texture_size);
    let tx_offset = clamp(tx * pixelPertexels, 0.0, 0.5) 
        + clamp((tx - 1.0) * pixelPertexels + 0.5 , 0.0, 0.5);
    let uv = (floor(in.uv) + tx_offset) / texture_size;

    return textureSample(
        texture, 
        anisotropy_sampler, 
        uv, 
    );
}

其中in.uv * texture_size是为了把uv从归一化坐标,转化为以纹素为单位的坐标,人话就是这个屏幕像素在贴图的哪里。再对其取小数部分,得到像素pixel中心在纹素texel区域中的相对位置信息。比如(0.5,0.5)是在纹素中央的像素,(0,0.25)是在左下角边缘四分之一处的像素。

pixelPertexels可以用导数计算或者uniform传参等形式传入,在近处一般很大;比如6.6,代表每6.6个像素才有一个纹素那么大。

tx_offset是用来判断当前像素是否处于两个纹素的交界处的,也就是否需要插值,还是直接采样中心的纯色。tx * pixelPertexels当像素足够贴近左下边缘时会得到接近0的数值,说明处于边缘位置;另一边是同理的。我个人觉得这个乘法挺难懂得,可以写成if的形式帮助理解这里的含义:

result.x = 0.0;
//靠近左边缘了需要插值
if(tx.x < 1.0/pixelPertexels){
    //得到靠近的比例
    let r = tx.x * pixelPertexels;
    result.x += r;
}
else{
    //左下完全可以看成是在纹素内的
    result.x += 0.5;
}

由于采样器是正常的双线性采样器,在我们偏移了UV之后,除了纹素中心的位置(0.5,0.5)的颜色其它颜色都是已经插值过的,其插值的比例也会映射到对应的偏移上,非常巧妙的实现了边缘色彩的插值。而当整数缩放时则完全不会有边缘插值的情况。

适配更多情况

但是很显然,在我们的需求中,纹理不可能永远完全正对着屏幕。处理斜着的纹理需要一些近似,WGSL代码如下:

@fragment
fn fs_main(
    in: VertexOutput
) -> @location(0) vec4f {
    let texture: texture_2d<f32> = texture_array[in.texture_index];
    let texture_size = vec2<f32>(textureDimensions(texture));

    //导数用来计算不是正对着的情况下的包围框的大小
    let dx = dpdx(in.uv);
    let dy = dpdy(in.uv);

    let box_size = clamp(((dx + dy) * texture_size), vec2f(1e-5), vec2f(1.0));

    let tx = in.uv * texture_size - 0.5 * box_size;
    //let tx_offset = clamp((fract(tx) - (vec2(1.0) - box_size)/box_szie),
    //    vec2(0.0), vec2(1.0));
    let tx_offset = smoothstep(1.0 - box_size, vec2(1.0), fract(tx));

    let uv = (floor(tx) + 0.5 + tx_offset) / texture_size;

    return textureSampleGrad(
        texture, 
        anisotropy_sampler, 
        uv, 
        dx, 
        dy
    );
}

dpdx(in.uv)和dpdy(in.uv)是uv对于屏幕x和y的变化率的导数,将他们相加,当正对着屏幕时可以看作是texel per pixel(纹素每像素);不是正对着时,可以看作是投影到屏幕的uv的包围框,其单位是和uv本身一致的。

将上述数据乘以texture_size得到像素在纹理中的大小,也就是box_size。clamp一下防止除0异常或太大了。

tx是像素中心点在纹理里的坐标,由uv转换到纹理在减掉半个像素(在纹理里的)大小得到。

tx_offset仍然是用来当前像素是否处于两个纹素的交界处的,这里有一些简化计算具体可以看参考资料里的视频,用smoothstep可以得到更好一些的边缘采样效果。

textureSampleGrad则是保证各项异性采样正确的。

最终结果如图所示:

参考

评论

  1. 苏石头
    4月前
    2024-5-25 13:05:47

    看不懂啊

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇