图形学程序员和其他软件程序员有一些不同,就是很多图形api是硬件实现的,除非很深入的研究,否则一些问题总是建立在猜测上,不利于理解问题的本质。本系列文章会整理我学习中遇到的一些问题,来揭示图形api的内部实现,材料来源于网络,如有错误,欢迎指正。
纹理采样时如何自动获取特定的mipmap等级
我们知道如果纹理开启了mipmap,离相机越远的表面,在进行纹理采样时会使用更低的mipmap等级,来提升性能。具体如何提升性能,可以查看笔者关于纹理过滤(Texture filtering)本质的文章。
下图是一个带有mipmap的纹理:
这里我们只关心在cg、glsl、hlsl这些图形着色语言中是怎样进行mipmap采样的。
纹理采样api
-
在cg语言中对于2D纹理采样最常用的api如下:
tex2D(sampler2D textureName ,float2(coord2));
使用该api在采样时会自动选择需要的mipmap level.
-
对应的有另外一个api:
tex2Dlod(sampler2D textureName, float4(coord2, 0, lod))
这个api可以通过参数lod指定采样特定的mipmap level.
什么情况下需要手动计算mipmap level ?
通常情况下我们不需要理解tex2D如何自动确定需要采样的mipmap等级,但是对于如下情况我们需要使用tex2Dlod
来自己计算mipmap等级:
- 顶点着色器里不支持
tex2D
,但是支持tex2Dlod
。(这篇文章会解释为什么顶点着色器不支持tex2D) - 有些图形学算法,不是依赖硬件自动计算mipmaplevel 的方法来确定采样的mipmap等级,而是需要手动计算lod,例如PBR光照模型中,采样预计算的漫反射和高光反射环境贴图,需要根据材质的粗糙度计算要采样的贴图的mipmap等级。
GPU 是如何计算需要的mipmap level的 ?
可以很容易想到,模型表面离相机距离更远或者说在相机视角下更小时需要采样更小的mipmap图,也就是此时mipmap level 要使用更大的值。
例如假定对原本一个铺满屏幕的quad(四方形面片)采样时,mipmap level 为0。当面片缩小,或者放在远离3D相机的位置时,该面片可能只占屏幕的1/4大小,此时需要使用mipmap level 为1也就是1/2长宽的那张图作为纹理采样的图。
那么GPU是如何判断出来此时纹理在屏幕上比例更小了呢?
这里再引入两个api:
ddx(float2(coord2))
ddy(float2(coord2))
在微软关于hlsl的文档中ddx,ddy的作用是分别计算屏幕空间中x轴向和y轴向的给定值的偏导。
在光栅化的时刻,GPUs会在同一时刻并行运行很多Fragment Shader,但是并不是一个pixel一个pixel去执行的,而是将其组织在2x2的一组pixels分块中,去并行执行。
偏导数就正好是计算的这一块像素中的变化率。从下图可以看出来ddx 就是右边的像素块的值减去左边像素块的值,而ddy就是下面像素块的值减去上面像素块的值。其中的x,y代表的是屏幕坐标。
如果我们使用模型表面的UV作为参数,使用ddx和ddy求其偏导,就可以得到在纹理空间下,相邻屏幕像素的UV差值,显而易见,相邻像素的UV差值越大,说明采样的纹理在屏幕上占比越小,对应的就需要使用mipmap level更大的mipmap等级进行纹理采样(原本0-1的UV可以铺满1000x1000的屏幕,相邻像素点UV差值为0.001=1/1000,当只能铺满1/4屏幕,也就是500x500的屏幕时,相邻像素点UV差值为0.002=1/500)。
实际上tex2D
在GPU中逻辑的伪代码如下:
tex2D(sampler2D tex, float2 uv)
{
float dx=ddx(uv);
float dy=ddy(uv);
// texSize.xy为纹理tex的纹素大小
// texSize.xy=1.0/float2(texWidth,texHeight)
float px = texSize.x * dx;
float py = texSize.y * dy;
float lod = 0.5 * log2(max(dot(px, px), dot(py, py)));
uv.w= lod;
return tex2Dlod(tex, uv);
}
到这里基本上就能理解GPU是如何自动计算纹理采样时的mipmap level了。
同时也解释了为什么不能在顶点着色器里使用tex2D 自动计算mipmap level了,因为ddx
,ddy
是在光栅化之后的像素着色器里才有效,因为计算的是相邻像素点的对应变量的偏导数,也就是变化率。而在顶点着色器里是不能调用ddx
和ddy
的。
如果需要在顶点着色器里采样纹理,只能使用tex2Dlod,但是需要自己计算lod,也就是mipmap等级。通常我们可以参考上边计算lod的方式float lod = 0.5 * log2(max(dot(px, px), dot(py, py)));
,使用我们自己的算法,得到纹理UV在x,y两个方向上的变化率px
,py
。
关于ddx,ddy在图形学还有其他用法,例如对世界空间下的坐标求偏导,可以用来计算表面法线;对渲染结果颜色求偏导,可以进行勾边强化。