Unity Shader学习笔记(二) - 纹理与透明效果

本文最后更新于 2025年1月30日 下午

实用且有趣,美味又易燃。

纹理

在Properties语义块中使用2D、3D或Cube类型定义纹理属性:

1
2
3
_2D("2D",2D) = ""{}
_3D("3D",3D) = "white"{}
_Cube("Cube",Cube) = "white"{}

随后在CG块中使用sampler定义纹理变量:

1
2
3
sampler2D _2D;
sampler3D _3D;
samplerCUBE _Cube;

对于需要进行平移、缩放变换的纹理(即,在检视器上可以操作Tilling和Offset),需要在Properties块中定义属性的基础上,在CG块中定义float4 _变量名_ST变量。该变量xy分量表示Tilling值,zw分量表示Offset值。

应用平移/缩放时,进行如下操作:

uv = uv.xy * _变量名_ST.xy + _变量名_zw

也可以直接使用TRANSFORM_TEX来操作。该宏的第一个参数为纹理坐标,第二个参数为纹理名。

使用tex2Dtex3DtexCUBE函数分别对2D、3D和Cube纹理采样。前两者使用纹理坐标采样,CubeMap则使用方向向量采样。

导入属性

image-20250112144051782

如图。常用的导入属性如下:

属性 作用
Texture Type 纹理类型。包含Default、Normal Map、Sprite等
Texture Shape 纹理形状,包括2D、3D、2DArray、Cube等
sRGB 是否启用sRGB。对于颜色贴图需要勾选,对于数据贴图(如法线贴图)则取消勾选
Alpha Source Alpha值的来源。包括Input Texture Alpha和From Grey Scale。前者直接导入透明通道,后者从灰度值生成透明通道
Wrap Mode 环绕模式。当纹理坐标超出[0,1]范围时,纹理该如何平铺。Repeat则重复平铺,Clamp则边缘像素拉伸,Mirror则镜像平铺,Mirror Once则镜像平铺一次随后边缘拉伸
Filter Mode 过滤模式。Point则最邻近,Linear则双线性,Trilinear则三线性。
Aniso Level 各向异性等级。越高,则斜视下清晰度越好,但性能开销越大
Max Size 纹理的最大尺寸。超过此大小时进行缩放
Resize Algorithm 尺寸超过MaxSize时,缩放使用的算法
Format 纹理存储格式。一般为Automatic
Compression 压缩质量
Use Crunch Compression 启用Crunch压缩,进一步减少文件大小,但加载速度减慢

除此之外,还有一些高级导入选项位于Advanced下拉栏内。常用的有Generate Mipmaps,启用纹理的Mipmap,提高占用空间的同时改善纹理缩小时的过滤效果。

纹理的大小最好为2的幂,否则加载速度会下降。

凹凸映射

凹凸映射(Bump Mapping)包含位移贴图(Displacement Map,使用Height Map模拟顶点偏移)和法线贴图(Normal Map)。

对于位移贴图技术,其使用的高度图为灰度图,存储强度值,表示模型表面局部的高度。颜色越浅则越凸。该技术由于无法直接得到偏移后的发现,如果要得到发现则需要复杂计算,消耗性能。因此,往往将其与法线贴图结合。

法线贴图

法线贴图上采样得到的三维向量取值范围为[0,1],而实际法线的范围为[-1,1]。因此,需要通过乘以二,减去一,然后通过TBN矩阵变换的方式处理采样值。

直接采样法线贴图得到的法线位于切线空间(Tangent Space)。切线空间的优点是,在制作法线贴图时,无需考虑顶点本身的模型空间坐标,让所有像素在同一坐标系内;同时,顶点无关性则能让同一张法线贴图应用到不同的物体。

切线空间的原点顶点Z轴顶点原本的法线X轴顶点切线Y轴顶点副切线

为了从法线贴图采样值推导出对应的真实法线,我们通常在片段着色器中使用TBN矩阵进行变换。具体操作如下:

首先,在appdata结构体中,新增float4类型变量tagent,通过TAGENT语义标记;

tagent.w用于决定副切线的方向性

然后,在v2f结构体中,新增三个float4类型变量TtoWx,以TEXCOORDx语义标记,用于:首先在VS中计算TBN矩阵,然后将其传递给FS。

TBN矩阵为三维矩阵,为了充分利用寄存器空间,我们将三个空闲的w用于存储worldPos

VS中计算TBN矩阵的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord.xy, _BumpMap);
float3 worldPos = UnityObjectToWorldPos(v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBiNormal = cross(worldNormal, worldTangent) * v.tangent.w; //副切线
o.ToW0 = float4(worldTagent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.ToW1 = float4(worldTagent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.ToW2 = float4(worldTagent.z, worldBinormal.z, worldNormal.z, worldPos.z); // TBN矩阵为ToW左上角的3x3矩阵。切线、副切线和法线分别填充0、1、2列。
}

FS中对法线贴图采样值变换的代码如下:

1
2
3
4
5
6
7
8
9
10
11
fixed4 frag(v2f i) : SV_Target{
float3 worldPos = float3(i.ToW0.w, i.TtoW.w, i.TtoW2.w);
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); // UnpackNormal为内置函数,对法线贴图进行采样、解码
bump.xy *= _BumpScale;
// 法线贴图可以仅存储x、y方向的扰动,z通过计算得到,因为法线必定为单位向量,其模长始终为1。
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.ToW1.xyz, bump), dot(i.TtoW2.xyz,bump)));
...
}

若要将高度图作为法线贴图,需要在导入后,在导入设置内,将纹理类型改为Normal Map,同时勾选Create From GrayScale。

渐变纹理

渐变纹理类似于一张LUT,将光照强度映射到LUT的不同颜色区域上,主要用于NPR。关键代码如下:

1
2
fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5; // 经典的Half Lambert光照计算
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb; // 将Half Lambert光强作为UV,对渐变纹理(_RampTex)采样,并与材质表面颜色相乘,得到满反射颜色

渐变纹理的环绕模式应当设置为Clamp

遮罩纹理

遮罩纹理(Mask Texture)用于精细地控制各光分量,例如,可以让物体某些特定区域的高光减弱。

透明效果

实现透明效果有两种方案:透明度测试(Alpha Test)和透明度混合(Alpha Blend)。

  • 透明度测试中,会设置一个阈值。只要某个片段的alpha值小于该阈值,就会被剔除。它不是一种真正的半透明渲染。使用透明度测试实现透明效果的材质无需关闭深度写入

Alpha Test使用内置函数clip实现。当其参数小于0时,裁剪该像素。

此外,使用Alpha Test的着色器需要使用Transparent/Cutoff/VertexLit作为Fallback。

  • 透明度混合中,会使用当前片段的alpha值作为混合因子,与颜色缓冲中的颜色进行Lerp。但是,Alpha Blend需要关闭深度写入,即使用该方法的半透明材质的片段不会更新深度缓冲,只会读取深度缓冲并进行深度剔除。

之所以关闭深度写入,是因为如果不关闭,那么当半透明物体比不透明物体距离相机更近时,由于深度缓冲已经被半透明物体更新,不透明物体会由于深度测试而被直接剔除。

也就是说,我们始终应该在渲染完所有不透明物体之后渲染半透明物体,这样既能保证位于不透明物体之后的透明物体被深度剔除,又能保证位于半透明物体之后的不透明物体都能被正常混合。

在透明度混合中,半透明物体的渲染顺序十分重要。我们必须先渲染距离摄像机远的半透明物体,再渲染离摄像机近的半透明物体。

ShaderLab中的透明度混合

ShaderLab中开启混合的语义如下:

语义 描述
Blend Off 关闭混合
Blend SrcFactor DstFactor 开启混合,设置混合因子。此片段产生的颜色会乘以SrcFactor,颜色缓冲中的颜色会乘以DstFactor。该语义最常用。
Blend SrcFactor DstFactor, SrcFactorA DstFactorA 使用不同因子混合RGB通道和A通道
BlendOp BlendOperation 使用Blend Operation对源、目标颜色进行其他操作。

使用透明度混合时,最终输出的颜色为:

Orgb=SrcFactorSrgb+DstFactorDrgbO_{rgb} = SrcFactor * S_{rgb} + DstFactor * D_{rgb}

Alpha值也类似。

通常,开启透明度混合的语义为:

Blend SrcAlpha OneMinusSrcAlpha

此外,开启透明度混合的着色器应当设置Tag:

  • “Queue” = “Transparent”
  • “IgnoreProjector” = “True”
  • “RenderType” = “Transparent”
  • “LightMode” = “ForwardBase”

同时,还需要设置ZWrite Off

除了SrcAlphaOneMinusSrcAlpha等,还有其他混合因子。如下:

  • One 、Zero:混合因子固定为0/1。
  • SrcColor/DstColor/OneMinusSrcColor/OneMinusDstColor:混合因子为RGB值(但混合Alpha时,使用颜色值的Alpha作为混合因子)
  • SrcAlpha/DstAlpha/OneMinusSrcAlpha/OneMinusDstAlpha:混合因子为alpha值。

对于BlendOp,有以下混合操作:

  • Add:最常用,将乘以各自混合因子的源/目标颜色值相加得到结果。
  • Sub:源-目标
  • RevSub:目标-源
  • Min:min(目标,源),逐分量比较
  • Max:max(目标,源)

常见的混合类型

这里给出常见的混合语义类型及其效果。

image-20250130160645871

  • Blend SrcAlpha OneMinusSrcAlpha:正常的透明度混合
  • Blend OneMinusDstColor One:柔和相加
  • Blend DstColor Zero:正片叠底(等效于目标*源)
  • Blend DstColor StcColor:两倍相乘
  • BlendOp Min Blend One One:变暗
  • BlendOp Max Blend One One:变亮
  • Blend OneMinusDstColor One:滤色
  • Blend One One:线性减淡

开启深度写入的半透明

对于相互重叠的半透明物体,在ZWrite Off的情况下,始终是无法正常渲染的。此时,我们需要使用双Pass渲染来规避这个问题。

所谓双Pass渲染,即:

  • 在渲染半透明物体时,首先进行仅写入深度的Pass,将该模型的深度值写入深度缓冲。
  • 随后进行第二个Pass,此Pass利用第一个Pass的深度信息对半透明物体的片段进行剔除。

大致的代码如下:

1
2
3
4
5
6
7
8
9
10
SubShader{
Tags {"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
Pass{
ZWrite On
ColorMask 0 //用于设置颜色通道写掩码的语义,后可接RGB/A/0/任何RGB的组合,表示仅写入这些通道
}
Pass{
// 正常渲染
}
}

该方法的本质是,针对半透明物体组进行一次单独的深度写入和深度测试。

双面渲染的半透明

对于部分透明的物体,有时需要透过透明部分看到物体内部的需求。此时,需要用到双面渲染。

对于AlphaTest,直接将默认的Cull Back改为Cull Off即可。

对于Alpha Blend,直接Cull Off会导致混合无法工作,因为我们无法保证背面始终在正面之前渲染。因此,我们使用双Pass,第一个Pass渲染背面,第二个Pass渲染正面。

代码很简单,第一个Pass进行Cull Front,第二个Pass进行Cull Back即可。


Unity Shader学习笔记(二) - 纹理与透明效果
http://example.com/2025/01/30/Unity-Shader学习笔记(二)-纹理与透明效果/
作者
Yoi
发布于
2025年1月30日
许可协议