百人计划-纹理

参考资料:
百人计划-纹理基础
《Unity Shader入门精要》 第7章 基础纹理

代码标注为C++只是为了会有高亮好看,实际是ShaderLab

纹理是什么

纹理可以避免复杂的建模,给予模型相对较好的细节的一种技术。通过纹理映射的方式纹理贴图上的细节“黏”在模型上,伪造出丰富的细节。纹理直观来讲是一张图片,但是纹理说到底是一个容器,因为纹理不仅仅能存储颜色信息,还可以存储深度信息,法线信息等等,还可以存储一个函数来动态计算。

纹理过大过小问题

这部分更详细的讲解在 Games101-着色 的笔记

纹理分辨率过小

几种采样方法:

  1. 最近邻
  2. 双线性插值
  3. 三线性插值

纹理分辨率过大

Mipmap

多级渐远纹理技术将原纹理提前用滤波处理来得到很多更小的图像, 形成了一个图像金字塔,每一层都是对上一层图像降采样的结果。 这样在实时运行时, 就可以快速得到结果像素, 如当物体远离摄像机时,可以直接使用较小的纹理。但缺点是需要使用一定的空间用于存储这些多级渐远纹理,通常会多占用33%的内存空间。

Ripmap

凹凸映射(Bump mapping)

凹凸映射的目的是使用 张纹理来修改模型表面的法线,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”。

法线贴图(Normal Map)

法线贴图记录了表面的法线信息,法线方向的分量范围是 [-1, 1] ,像素分量的范围是 [0, 1] ,要把纹理信息记录在图片上就需要做一个映射:$pixel = (normal + 1)/2$,在shader中就需要做一个反映射:$normal = pixel*2-1$。
法线贴图分为定义在模型空间下的法线纹理和定义在切线空间下的法线纹理两种。

一般来说使用切线空间下的法线纹理,对于模型的每个顶点,都有一个自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴就是顶点的法线方向,因此对于大部分顶点来说,其法线坐标是(0,0,1),转换到像素存储到贴图中就是(0.5,0.5,1)就是看上去的浅蓝色。这种法线纹理其实就是存储了每个点在各自的切线空间中的法线 “扰动” 方向。

切线空间下的法线纹理的优点:

  1. 模型空间下的法线纹理记录绝对法线信息,只可以用于创建时的模型,切线空间下的法线纹理可以通用
  2. 可以进行UV动画
  3. 可以重用法线纹理
  4. 可以压缩,由于切线空间下的法线纹理中法线的方向总是正方向,因此我们可以仅存储 XY 方向,而推导得到 Z 方向。而模型空间下的法线纹理由于每个方向都是可能的,因此必须存储3个方向的值,不可压缩。

Shader中的实现方法

在shader中计算光照时,需要让法线,光源方向,观察方向都在同一个坐标系中(无论在哪一个坐标系中,都不会对计算结果产生影响),因此针对切线空间下的法线纹理,就有两种实现方式:将所有向量转换到切线空间下将所有向量转换到世界空间下

  1. 转换到切线空间:这种方法可以在顶点着色器中实现,将光源方向和观察方向转换到切线空间即可,这种方法由于在顶点着色器中实现所以计算量较小
  2. 转换到世界空间:这种方法要将切线空间的法线转换到世界空间,因此必须在片元着色器中对法线贴图采样后进行一次矩阵乘法,效率上比较低,但是通用性更好,有时我们需要在世界空间下进行一些计算,如Cubemap。在构建转换矩阵时一定要注意顺序必须是:切线,副切线,法线 的顺序!(因为这是切线空间对xyz轴的定义,见上图,如果顺序不对在转换时就会计算错误),按列构建矩阵(即从世界空间到切线空间矩阵的转置)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void vert(in a2v v, out v2f o){
    //...
    /*
    构建从切线空间转换到世界空间的转换矩阵
    */
    o.TtoW0 = float3(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
    o.TtoW1 = float3(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
    o.TtoW2 = float3(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
    //...
    }
    fixed4 frag(in v2f i) : SV_TARGET{
    //...
    fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv)); // 解包法线
    bump = fixed3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)) // 转换到世界空间
    //...
    }

在对使用法线贴图时,需要先对法线贴图进行解包(从像素颜色转换到法线方向):

1
2
// 在Unity中设置纹理为法线贴图,可以使用内置函数否则要手动解包:normal = pixel*2-1
fixed3 normal = UnpackNormal(tex2D(_NormalMap, i.uv.zw));

上面介绍了切线下的法线贴图可以压缩,我们一般只存储法线的xy分量,z分量通过法线是单位向量的性质计算得出(当在Unity中将纹理属性设置为Normal Map后直接使用内置函数UnpackNormal解包后直接使用),下面的代码是UnpackNormal的实现,对不同平台对法线有不同的压缩方式,所以还是尽量使用UnpackNormal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
inline fixed3 UnpackNorma1DXT5nm (fixed4 packednormal) 
{
fixed3 normal;
normal.xy = packednormal.wy * 2 - l;
normal.z = sqr-t(l - saturate(dot(normal.xy, normal.xy)));
return normal ;
}
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined (UNITY NO DXT5nm)
return packednormal.xyz * 2 - l;
#else
return UnpackNorrna1DXT5nm(packednormal);
#endif
}

一定注意在导入法线贴图时,要将法线贴图的纹理类型(Texture Type)设置为 Normal map,这是因为如果使用 UnpackNormal 等类似函数解包法线贴图时会按照针对法线贴图的压缩规则: a通道当作x,g通道当作y,而r和b通道不存储任何数据 进行解包。如果没有设置为法线贴图类型,则没有进行压缩操作,最后使用解包函数的结果也当然是错误的。


从左到右:单张纹理,转换到切线空间下,转换到世界空间下

高度贴图(Height Map)

高度图存储的是强度值(intensity),颜色越深该位置越向里凹,颜色越浅越向外凸,高度图不能直接得到法线信息,一般和法线贴图一起使用,用来给出表面凹凸的额外信息。

渐变纹理

人们法线纹理可以用于存储任何表面信息,就想到用纹理存储漫反射光照的结果,这种技术常用来实现卡通化渲染.

在具体实现时,使用half-lambert计算漫反射,因为half-lambert是一个 [0, 1] 的映射,正好可以用来采样RampMap,如果使用lambert那么会有整个半球的颜色我们无法控制,那就背离了我们使用渐变纹理来更灵活的控制光照的初衷。

1
2
3
fixed halfLambert = 0.5 * dot(worldNormal, worldLight)+0.5;
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, 0)).rgb * _Color.rgb;
fixed3 diffuse = diffuseColor * _LightColor0.rgb;

同时要注意在使用渐变纹理时,将纹理的 Wrap Mode 设置为 clamp,这样可以有效防止由于计算精度问题(halfLambert计算结果可能会出现1.0001这种值,如果是 repeat 模式,会舍弃整数部分编程0.0001,就会采样到黑色区域)在高光区域出现的黑点和在背光区域出现的亮点:

![](/article_img/2023-07-11-18-21- 15.png)

遮罩纹理

遮罩允许我们可以保护某些区域,使它们免于某些修改。使用遮罩纹理的流程一般是通过采样得到遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值(例如 texel.r 来与某种表面属性进行相乘,这样,当该通道的值为0时,可以保护表面不受该屈性的影响。

1
2
float specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(lightDir, normal)), _Gloss) * specularMask;


从左至右:无高光遮罩,有高光遮罩,无高光

环境贴图(立方体纹理/Cubemap)

在图形学中,立方体纹理(Cubemap)是环境映射(Environment Mapping)的一种实现方法。 环境映射可以模拟物体周围的环境,而使用了环境映射的物体可以看起来像锁了层金属一样反射出周围的环境。

Cubemap包含了6张图像,就相当于用一个正方体包裹住场景,天空盒一般就用Cubemap来实现,同时,通过cubemap可以实现环境映射,因此一些高级的光线效果得以利用cubemap实现。

反射(Reflection)

反射的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pass{
v2f vert(in a2v v){
v2f o;
// ...
o.worldReflct = reflect(-o.worldView, o.worldNormal);
return o;
}
real4 frag(in v2f i) : SV_TARGET{
// reflection
half3 reflection = SAMPLE_TEXTURECUBE(_Cubemap, sampler_Cubemap, i.worldReflct).rgb * _ReflectColor.rgb;
half atten = mainLight.shadowAttenuation;
half3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten;

return real4(color, 1.0);
}
}

折射(Refraction)

折射的实现需要注意使用 refract() 函数中的参数要先归一化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pass{
v2f vert(in a2v v){
v2f o;
// ...
o.worldRefract = refract(-normalize(o.worldView), normalize(o.worldNormal));
return o;
}
real4 frag(in v2f i) : SV_TARGET{
// refraction
half3 refraction = SAMPLE_TEXTURECUBE(_Cubemap, sampler_Cubemap, i.worldRefract).rgb;
half atten = mainLight.shadowAttenuation;
half3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;

return real4(color, 1.0);
}
}

折射效果很差,就不放图了。。

菲涅尔效应(Fresnel)

菲涅尔效应在实现中通过Schlick近似来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pass{
v2f vert(in a2v v){
v2f o;
// ...
o.worldReflect = reflect(-normalize(o.worldView), normalize(o.worldNormal));
return o;
}
real4 frag(in v2f i) : SV_TARGET{
// fresnel
float3 fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(i.worldView, worldNormal), 5); // schlick
half3 reflection = SAMPLE_TEXTURECUBE(_Cubemap, sampler_Cubemap, worldReflect).rgb;

half3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten;
return real4(color, 1.0);
}
}

渲染纹理(Render Texture)

程序纹理(Procedure Texture)


百人计划-纹理
https://kenny-hoho.github.io/2023/07/15/百人计划-纹理/
作者
Kenny-hoho
发布于
2023年7月15日
许可协议