Unity Shader 获取深度纹理和法线纹理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity Shader 获取深度纹理和法线纹理相关的知识,希望对你有一定的参考价值。

参考技术A

深度纹理实际就是一张渲染纹理,只不过它里面存储的像素值不是颜色值,而是一个高精度的深度值。由于被存储在一张纹理中,深度纹理里的深度值范围是[0, l], 而且通常是非线性分布的。这些深度值来自于顶点变换后得到的归一化的设备坐标 (Normalized Device Coordinates , 简称NDC) 。一个模型要想最终被绘制在屏幕上,需要把它的顶点从模型空间变换到齐次裁剪坐标系下,这是通过在顶点着色器中乘以 MVP 变换矩阵得到的 。在变换的最后一步,我们需要使用一个投影矩阵来变换顶点,当我们使用的是透视投影类型的摄像机时,这个投影矩阵就是非线性的。

下图 显示了Unity 中透视投影对顶点的变换过程。最左侧的图显示了投影变换前,即观察空间下视锥体的结构及相应的顶点位置,中间的图显示了应用透视裁剪矩阵后的变换结果,即顶点着色器阶段输出的顶点变换结果 ,最右侧的图则是底层硬件进行了透视除法后得到的归一化的设备坐标(NDC)。需要注意的是,这里的投影过程是建立在Unity对坐标系的假定上的,也就是说,针对的是观察空间为右手坐标系,使用列矩阵在矩阵右侧进行相乘,且变换到NDC后 z分量范围将在[-1, l] 之间的情况。而在类似 DirectX 这样的图形接口中,变换后z分量范围将在[0, 1]之间 。如果是在其他图形接口下,需要对一些计算参数做出相应变化。

下图 显示了在使用正交摄像机时投影变换的过程。同样,变换后会得到一个范围为 [-1, 1]的立方体。正交投影使用的变换矩阵是线性的。

在得到NDC后,深度纹理中的像素值就可以很方便地计算得到了,这些深度值就对应了NDC中顶点坐标的z分量的值。由于NDC中 z分量的范围在[-1, I], 为了让这些值能够存储在一张图像中,需要使用下面的公式对其进行映射:

其中,d对应了深度纹理中的像素值, 对应了NDC坐标中的z分量的值。

在Unity中,深度纹理可以直接来自于真正的深度缓存,也可以是由一个单独的 Pass 渲染而得,这取决于使用的渲染路径和硬件。通常来讲,当使用延迟渲染路径(包括遗留的延迟渲染路径)时,深度纹理理所当然可以访问到,因为延迟渲染会把这些信息渲染到 G-buffer 。而当无法直接获取深度缓存时,深度和法线纹理是通过一个单独的Pass渲染而得 。具体实现是Unity会使用着色器替换技术选择那些渲染类型(即 SubShader RenderType 标签)为 Opaque 的物体,判断它们使用的渲染队列是否小于等于2500(内置的 Background Geometry AlphaTest 渲染队列均在此范围内),如果满足条件,就把它渲染到深度和法线纹理中。因此,要想让物体能够出现在深度和法线纹理中,就必须在Shader中设置正确的RenderType标签。

在 Unity中,可以选择让一个摄像机生成一张深度纹理或是一张深度+法线纹理。当选择前者,即只需要 张单独的深度纹理时, Unity 会直接获取深度缓存或是按之前讲到的着色器替换技术,选取需要的不透明物体,并使用它投射阴影时使用的 Pass (即 LightMode 被设置为ShadowCaster Pass)来得到深度纹理。如果 Shader 中不包含这样一个 Pass, 那么这个物体就不会出现在深度纹理中(当然,它也不能向其他物体投射阴影)。深度纹理的精度通常24 位或 16 位,这取决于使用的深度缓存的精度。如果选择生成一张深度+法线纹理, Unity 创建一张和屏幕分辨率相同、精度为 32 位(每个通道为 位)的纹理,其中观察空间下的法线信息会被编码进纹理的 通道,而深度信息会被编码进 通道。法线信息的获取在延迟渲染中是可以非常容易就得到的, Unity 只需要合并深度和法线缓存即可。而在前向渲染中,默认情况下是不会创建法线缓存的,因此 Unity 底层使用了一个单独的 Pass 把整个场景再次渲染一遍来完成。这个 Pass 被包含在 Unity 内置的一个 Unity Shader 中,可以在内置的builtin_shaders-xxx/DefaultResources/Camera-DepthNormaITexture.shader 文件中找到这个用于渲染深度和法线信息的 Pass。

获取深度纹理,先设置摄像机的 depthTextureMode:

然后在 Shader 中通过声明_CameraDepthNormalsTexture 变量来访问它。

同理,如果需要获取获取深度+法线纹理,设置摄像机的 depthTextureMode为:

然后在 Shader 中通过声明_CameraDepthN ormalsTexture 变量来访问它。

还可以组合这些模式,让一个摄像机同时产生一张深度和深度+法线纹理:

在 Shader 中访问到深度纹理_CameraDepthTexture 后,我们就可以使用当前像素的纹理坐标对它进行采样。绝大多数情况下,我们直接使用 tex2D 函数采样即可,但在某些平台(例如 PS3 PSP2) 上,我们需要 一些特殊 处理 Unity 为我们提供了一个统一的宏SAMPLE_DEPTH_TEXTURE, 用来处理这些由于平台差异造成的问题。而我们只需要在 Shader中使用 SAMPLE_DEPTH_TEXTURE 宏对深度纹理进行采样,例如:

其中,i.scrPos是在顶点着色器中通过调用ComputeScreenPos(o.pos)得到的屏幕坐标。上述这些宏的定义,可以在Unity 内置的HLSLSupport.cginc文件中找到。

当通过纹理采样得到深度值后,这些深度值往往是非线性的,这种非线性来自于透视投影使用的裁剪矩阵。然而,在我们的计算过程中通常是需要线性的深度值,也就是说,我们需要把投影后的深度值变换到线性空间下,例如视角空间下的深度值,我们只需要倒推顶点变换的过程即可。下面以透视投影为例,推导如何由深度纹理中的深度信息计算得到视角空间下的深度值。

当我们使用透视投影的裁剪矩阵 对视角空间下的一个顶点进行变换后,裁剪空间下顶点的z和w分量为:

其中,Far和Near分别是远近裁剪平面的距离。然后,我们通过齐次除法就可以得到NDC下的z分量:

而深度纹理中的深度值是通过下面的公式由NDC 计算而得的:

由上面的这些式子,可以推导出用d表示而得的 的表达式:

由于在Unity 使用的视角空间中,摄像机正向对应的z值均为负值,因此为了得到深度值的正数表示,我们需要对上面的结果取反,最后得到的结果如下:

它的取值范围就是视锥体深度范围,即[Near, Far]。如果我们想得到范围在[0, l]之间的深度值,只需要把上面得到的结果除以Far即可。这样,0就表示该点与摄像机位于同一位置,1表示该点位于视锥体的远裁剪平面上。结果如下:

其实,Unity提供了两个辅助函数来为我们进行上述的计算过程LinearEyeDepth 和LinearOlDepth。LinearEyeDepth 负责把深度纹理的采样结果转换到视角空间下的深度值,也就是我们上面得到的 。而Linear01Depth则会返回一个范围在[0, 1]的线性深度值,也就是我们上面得到的 。这两个函数内部使用了内置的_ZBufferParams变量来得到远近裁剪平面的距离。
如果需要获取深度+法线纹理,可以直接使用tex2D函数对_CameraDepthNormalsTexture 进行采样,得到里面存储的深度和法线信息。Unity提供了辅助函数来为我们对这个采样结果进行解码,从而得到深度值和法线方向。这个函数是DecodeDepthNormal,它在UnityCG.cginc 里被定义为:

DecodeDepthNormal 的第一个参数是对深度+法线纹理的采样结果,这个采样结果是 Unity 深度和法线信息编码后的结果 它的 xy 分量存储的是视角空间下的法线信息 而深度信息被编码进了 zw 分量。通过调用 DecodeDepthNormal 函数对采样结果解码后, 我们就可 得到解码后的深度值和法线。这个深度值是范围在[O l] 的线性深度值(这与单独的深度纹理中存储 深度值不同),而得到的法线则是视角空间下的法线方向。同样也可以通过调用 DecodeFloatRG和DecodeViewNormalStereo 来解码深度+法线纹理中的深度和法线信息。

Unity Shader ------ 纹理之法线纹理单张纹理及遮罩纹理的实现

笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题。

 

在游戏中,我们除了能看到游戏物体的形体轮廓,还能看到物体的一些具体外观,包括颜色,凹凸等。而实现这一步的就是使用 纹理。与纹理相对应的技术就是 纹理映射技术 ,相当于把一张图贴在物体表面,然后 逐纹素 地控制颜色

 

纹理映射坐标:纹理映射坐标定义了一个顶点在纹理中对应的2D坐标。由于常用 U 来表示横向坐标, V 来表示纵向坐标,所以纹理映射坐标也是我们常常见到的 UV坐标。 顶点 UV 坐标通常会被归一化至 【0,1】范围内。当然纹理采样时使用的坐标也不一定在这个范围内。

另外值得注意的是,OpenGL 与 DirectX 的二维坐标系是不一样的,OpenGL中原点位于左下角,DirectX原点位于左上角。当然Unity 会帮我们处理这个差异,同时一般情况下,Unity 采用的纹理空间是符合 OpenGL传统的。

技术分享图片

需要注意的是:本文着重讲述纹理采样的原理,由于实现的shader中的光照模型计算如同上文中,并不完整。所以不能直接运用于项目

 

一. 单张纹理

先看一下我们要实现的效果

技术分享图片?

shader 的一些书写方式本文便不再赘述,同时本文的计算光照的方式都能够在上一篇文章中找到,如果忘了,可以先复习一下

【Unity Shader】(三) ------ 漫反射和高光反射的实现

 

1.1. 实现单张纹理

新建一个场景,去掉天空盒子;新建一个 Capsule 与 Material,命名为 SingleTexture;

I. 先定义 Properties 语义块

技术分享图片?

其中 _MainTex 的纹理用来表示纹理贴图,这里我们用这张纹理贴图来代替物体的漫反射颜色。

 

II. 为了控制 Properties 中的属性,我们在CG代码片中定义与之相匹配的变量

技术分享图片?

在Unity中,一般使用 纹理名_ST 来代表某个纹理的属性

 _MainTex_ST 代表 _MainTex 这个纹理的属性:S(Scale)缩放,T(Translation)平移。

_MainTex_ST.xy 代表 缩放值;_MainTex_ST.zw 代表 偏移值

技术分享图片?

 

III. 定义输入输出结构体

技术分享图片?

 uv 变量存储了纹理坐标,以便在片元着色器中进行采样

 

IV. 顶点着色器

技术分享图片?

黄色框中,我们使用了 _MainTex_ST 对顶点纹理坐标进行变换,得到最终的纹理坐标。先使用 _MainTex_ST.xy 对顶点纹理坐标进行缩放,然后使用 _MainTex_ST.zw 进行偏移。而 TRANSFORM_TEX 则是封装了这个计算方式的内置函数,我们可以在 UnityCG.cginc 中找到它的定义

技术分享图片?

很显然,参数一为顶点纹理坐标,参数二为纹理名

 

V. 片元着色器

技术分享图片?

此处光照模型使用的是 Blinn-Phong 模型,所以光照计算方面与之前并没有太大的差异, 如果读者对光照模型不太了解,可以翻看我的前一篇文章。

这个片元着色器主要使用 Cg 函数 tex2D(_MainTex,i.uv) 对纹理进行了采样,然后以采样结果与颜色属性相乘,乘积结果作为反射率。其余的光照计算基本无异。而关于tex2D 的解释如下

技术分享图片?

技术分享图片?

完整代码:

 1 Shader "Unity/Custom/01-SingleTexture" 
 2 {
 3     Properties 
 4     {
 5         _Color("Color Tint",Color) = (1,1,1,1)
 6         _MainTex("Main Tex",2D) = "while"{}
 7         _Specular("Specular",Color) = (1,1,1,1)
 8         _Gloss("Gloss",Range(8.0,256)) = 20
 9 
10     }
11     SubShader 
12     {
13         Pass
14         {
15             Tags { "LightMode"="ForwardBase" }
16 
17 
18             CGPROGRAM
19             #pragma vertex vert
20             #pragma fragment frag
21             #include "Lighting.cginc"
22 
23             fixed4 _Color;
24             sampler2D _MainTex;
25             float4 _MainTex_ST;
26             fixed4 _Specular;
27             float _Gloss;
28             
29             struct a2v{
30 
31                 float4 vertex : POSITION;
32                 float3 normal : NORMAL;
33                 float4 texcoord : TEXCOORD0;
34             };
35 
36             struct v2f{
37 
38                 float4 pos : SV_POSITION;
39                 float3 worldnormal : TEXCOORD0;
40                 float3 worldPos : TEXCOORD1;
41                 float2 uv : TEXCOORD2;
42             };
43 
44             v2f vert(a2v v)
45             {
46                 v2f o;
47                 o.pos = UnityObjectToClipPos(v.vertex);
48                 o.worldnormal = UnityObjectToWorldNormal(v.normal);
49                 o.worldPos = UnityObjectToClipPos(v.vertex).xyz;
50 
51                 //o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
52                 o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
53 
54                 return o;
55             }
56 
57             fixed4 frag(v2f i) : SV_Target
58             {
59 
60                 fixed3 worldnormal = normalize(i.worldnormal);
61                 fixed3 worldlight = normalize(UnityWorldSpaceLightDir(i.worldPos));
62 
63                 fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
64 
65                 fixed3 ambient =  UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
66 
67                 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldnormal,worldlight));
68 
69                 fixed3 reflectDir = normalize(reflect(-worldlight,worldnormal));
70                 fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
71                 //计算得到矢量h
72                 fixed3 halfDir = normalize(worldlight + viewDir);
73 
74                 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldnormal,halfDir)),_Gloss);
75 
76 
77                 return fixed4(ambient + diffuse + specular,1.0);    
78             }
79             ENDCG
80 
81         }
82 
83     }
84     
85     FallBack "Specular"
86 }

 

 

保存,进入Unity 查看效果。当然还有附上一张纹理。

 技术分享图片?

 

二. 凹凸映射

另一种常见的纹理应用就是 凹凸映射 。凹凸映射就是为了使用一张纹理来修改模型表面法线,来为模型提供更多的细节。当然,这并不会真的改变模型的顶点位置,仅仅是使得模型看起来是 “不平滑的” ,更加的真实。

凹凸映射常用的两种方法:

  • 高度映射。使用一张高度纹理来模拟表面位移,然后得到修改后的法线值。
  • 法线映射。使用一张法线纹理直接存储表面法线。

 

2.1 高度纹理

高度纹理图存储是强度值,表示表面局部的海拔,颜色越浅表示越向外凸起,颜色越深表示越向内凹进去。这样就可以很形象地看出模型的凹凸,不过这样的计算会更加复杂,在实时计算时并不能直接得到表面法线,而是计算像素的灰度值得到。本文着重讲述的是法线纹理,所以高度映射技术便不再赘述了。

 

2.2 法线纹理

前文已经提到法线纹理存储的是表面法线方向,法线方向的分量范围在【-1.1】,而像素分量范围在【0,1】。所以为了让两者一致,我们需要做一个简单的映射。相必这个方式大家都有学过

                                                  技术分享图片

 那么可以预知的便是,当我们在shader中对法线纹理采样后,就必须对其进行反映射,得到原先的法线方向。

                                                 技术分享图片

需要注意的是,这个方向是有着空间之异的。对于模型自带的顶点法线,则是定义在模型空间的,这个纹理称为 模型空间的法线纹理。不过,一般制作法线纹理时,我们一般会采样 切线空间(tangent space)

切线空间:对于每个顶点,它都有一个属于自己的切线空间,切线空间原点就是该顶点本身,Z 轴则是顶点法线方向。X 轴为切线方向。Y 轴可以由法线和切线叉积而得。也称为 副切线副法线。而存储在切线空间的纹理则称为 切线空间的纹理。

技术分享图片

?上图是一张法线纹理。许多使用过法线纹理但不太了解其原理的朋友或许都有一个疑问:为什么普通的纹理都是红颜六色的,但是法线纹理大都像上图一样是一片蓝色的?

  • 模型空间的法线纹理,所有法线的坐标都是在模型空间,每个点存储的法线方向都是各异的,经过映射之后就变成了RGB(x,y,z) ,而x,y,z并不一致,所以对应着不同的颜色。所以模型空间的法线纹理看起来是五颜六色的。
  • 切线空间的法线纹理,所有法线的坐标都是在各自的切线空间,新的法线方向就是 Z 轴,即(0,0,1),经过映射就是(0.5,0.5,1)浅蓝色。所以切线空间的法线纹理看上去大部分都是蓝色的,这也说明了顶点的大部分法线是和模型本身法线一样的。

 

两种法线纹理的优劣·:

模型空间的法线纹理 :

① 直观,简单

② 可以提供平滑的边界部分。

 

切线空间的法线纹理 :

① 自由度很高:模型空间的法线纹理是 绝对法线信息 ,即只能用于创建它的那个模型,应用于它处就会出错。而切线空间的法线纹理 是 相对法线信息 ,应用于不同的网格都可以得到一个不错的效果

② 可以制作UV动画:可以通过移动UV来实现一个动画,而模型空间下的纹理则会完全错误。

③ 可以压缩。切线空间下的纹理,法线 Z 方向总是正方向,所以只存储XY方向就可以通过推导得到 Z 方向。而模型空间下的纹理则不行

④ 可以重复利用

 

由于法线方向存储于切线空间,所以在实际计算光照时会有两种计算方式:① 把光照方向,视角方向转换至切线空间,进行光照;② 把采样得到的法线方向转换至世界空间,计算光照;从效率角度,① 优于 ② ,从通用性来看,② 优于 ①。

本文会先给出第一种方法的实践,第二种以后我会补充回来,读者也可以自行实现。

 

2.3 切线空间下计算光照

新建一个材质和Capsule,命名为NormalTextureTangentSpace

 

I. 定义 Properties 语义块

技术分享图片?

其中 _BumpMap 表示法线纹理,_BumpScale 控制凹凸程度

 

II. 为了控制 Properties 中的属性,我们定义与之相匹配的变量

 技术分享图片?

 

III. 修改输入输出结构体

 技术分享图片?

技术分享图片?

因为切线空间是由顶点法线与切线构建的,所以在输入结构体添加一个切线变量,使用 TANGENT 语义。

因为我们是在切线空间下计算光照,所以在输出结构体中添加两个变量来存储转换空间后的光照方向和视角方向

 

IV. 定义顶点着色器

技术分享图片?

 我们使用了两张纹理,所以 uv 变量修改为 float4 类型,其中,xy分量存储 _MainTex 的纹理坐标,zw分量存储 _BumpMap 的纹理坐标。然后为了对光照方向和视角方向转换至切线空间,我们需要一个变换矩阵 rotaion,而 TRANGENT_SPACE_ROTATION 则是Unity内帮我们实现了计算过程的内置宏,它会返回我们所需 rotation,我们可以在UnityCG.cginc 中找到它的定义。

 技术分享图片?

 

V. 修改片元着色器

技术分享图片?

我们在顶点着色器中已经对光照方向和视角方向做了转换空间的工作,所以片元着色器中只需要对法线纹理进行采样,然后计算光照就可以了。tex2D 函数的定义在前文已经给出。然后使用Unity内置函数 UnpackNormal 得到正确的法线方向。然后对得到的法线向量的 xy 分量乘于 _BumpScale 就可以得到 法线的 xy 分量。再计算出 z 分量,就得到了正确的法线方向。

 

VI. 保存,查看效果

不同 _BumpScale 下的效果:

 技术分享图片?

 

需要注意的是:

使用法线纹理时,注意其类型是否为 Normal map

技术分享图片

 如果不是,则要在 shader 里面进行以下的更改

?

技术分享图片

更改为

技术分享图片?

 如果不进行修改,Unity 也提醒你

 技术分享图片?

因为如果法线纹理类型不是 Normal map 时,我们需要手动对采样结果的 xy 分量进行反映射。而如果是 Normal map 类型,则使用 UnpackNormal 函数。因为,当法线纹理类型设置成 Normal map 时,Unity 会根据平台的不同而对该法线纹理进行压缩,此时 _BumpMap 的 rgb 分量已经不是切线空间下的 xyz 分量了。所以此时再进行以上的手动计算就会得到错误的结果。

而 UnpackNormal 函数则可以在 UnityCG.cginc 中找到其定义?

 技术分享图片

技术分享图片?

 其中 DXT5nm 是一种压缩格式

 

 那么,完整代码如下:

 

  1 Shader "Unity/Custom/01-NormalTexture-Tangent Space" 
  2 {
  3     Properties 
  4     {
  5         _Color("Color Tint",Color) = (1,1,1,1)
  6         _MainTex("Main Tex",2D) = "while"{}
  7         _BumpMap("Normal Map",2D) = "bump"{}
  8         _BumpScale("Bump Scale",Float) = 1.0
  9         _Specular("Specular",Color) = (1,1,1,1)
 10         _Gloss("Gloss",Range(8.0,256)) = 20
 11 
 12     }
 13     SubShader 
 14     {
 15         Pass
 16         {
 17             Tags { "LightMode"="ForwardBase" }
 18 
 19 
 20             CGPROGRAM
 21             #pragma vertex vert
 22             #pragma fragment frag
 23             #include "Lighting.cginc"
 24             #include "UnityCG.cginc"
 25 
 26             fixed4 _Color;
 27             sampler2D _MainTex;
 28             float4 _MainTex_ST;
 29             sampler2D _BumpMap;
 30             float4 _BumpMap_ST;
 31             float _BumpScale;
 32             fixed4 _Specular;
 33             float _Gloss;
 34             
 35             struct a2v{
 36 
 37                 float4 vertex : POSITION;
 38                 float3 normal : NORMAL;
 39                 float4 tangent : TANGENT;
 40                 float4 texcoord : TEXCOORD0;
 41             };
 42 
 43             struct v2f{
 44 
 45                 float4 pos : SV_POSITION;
 46                 float4 uv : TEXCOORD0;
 47                 float3 lightDir : TEXCOORD1;
 48                 float3 viewDir : TEXCOORD2;
 49             };
 50 
 51             v2f vert(a2v v)
 52             {
 53                 v2f o;
 54                 o.pos = UnityObjectToClipPos(v.vertex);
 55 
 56                 o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
 57                 o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
 58 
 59                 TANGENT_SPACE_ROTATION;
 60 
 61                 o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
 62                 o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;
 63 
 64                 return o;
 65             }
 66 
 67             fixed4 frag(v2f i) : SV_Target
 68             {
 69                 //光源方向归一化
 70                 fixed3 tangentLightDir = normalize(i.lightDir);
 71                 //视角方向归一化
 72                 fixed3 tangentViewDir = normalize(i.viewDir);
 73 
 74                 //对法线纹理取样
 75                 fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw);
 76                 //切线空间下的法线
 77                 fixed3 tangentNormal;
 78 
 79                 //手动反映射
 80                 //tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
 81 
 82                 tangentNormal = UnpackNormal(packedNormal);
 83                 tangentNormal.xy *= _BumpScale;
 84             
 85 
 86 
 87                 tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy)));
 88 
 89                 fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
 90 
 91                 fixed3 ambient =  UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
 92 
 93                 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(tangentNormal,tangentLightDir));
 94 
 95                 //计算得到矢量h
 96                 fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
 97 
 98                 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss);
 99 
100                 return fixed4(ambient + diffuse + specular,1.0);    
101             }
102             ENDCG
103 
104         }
105 
106     }
107     
108     FallBack "Specular"
109 }

 

 

 

三. 遮罩纹理

法线纹理是十分常见且重要的纹理,讲完了法线纹理,我们现在讲另外一种非常有用的纹理:遮罩纹理

 遮罩可以保护某些区域不受修改,比如我们上一篇光照原理中实现的高光反射则是对于所有像素而言的,现在我希望物体某部分更强烈一些,而另一部分则更弱一些,此时我们就可以用到遮罩纹理了。

 遮罩纹理的使用流程:① 采样,得到纹素值  ② 使用其中一个或多个通道的值来与表面属性相乘  ③ 当通道的值为0时,可以保护表面不受该属性影响

现在我们来实现,对高光反射进行遮罩。计算在切线空间,代码与之前相差不多,就不赘述了

完整代码:

  1 Shader "Unity/Custom/01-MaskTexture" 
  2 {
  3     Properties 
  4     {
  5         _Color("Color Tint",Color) = (1,1,1,1)
  6         _MainTex("Main Tex",2D) = "while"{}
  7         _BumpMap("Normal Map",2D) = "bump"{}
  8         _BumpScale("Bump Scale",Float) = 1.0
  9         _SpecularMask("Specular Mask",2D) = "while"{}
 10         _SpecularScale("Specular Scale",Float) = 1.0
 11         _Specular("Specular",Color) = (1,1,1,1)
 12         _Gloss("Gloss",Range(8.0,256)) = 20
 13 
 14     }
 15     SubShader 
 16     {
 17         Pass
 18         {
 19             Tags { "LightMode"="ForwardBase" }
 20 
 21 
 22             CGPROGRAM
 23             #pragma vertex vert
 24             #pragma fragment frag
 25             #include "Lighting.cginc"
 26             #include "UnityCG.cginc"
 27 
 28             fixed4 _Color;
 29             sampler2D _MainTex;
 30             float4 _MainTex_ST;
 31             sampler2D _BumpMap;
 32             float _BumpScale;
 33             sampler2D _SpecularMask;
 34             float _SpecularScale;
 35             fixed4 _Specular;
 36             float _Gloss;
 37             
 38             struct a2v{
 39 
 40                 float4 vertex : POSITION;
 41                 float3 normal : NORMAL;
 42                 float4 tangent : TANGENT;
 43                 float4 texcoord : TEXCOORD0;
 44             };
 45 
 46             struct v2f{
 47 
 48                 float4 pos : SV_POSITION;
 49                 float2 uv : TEXCOORD0;
 50                 float3 lightDir : TEXCOORD1;
 51                 float3 viewDir : TEXCOORD2;
 52             };
 53 
 54             v2f vert(a2v v)
 55             {
 56                 v2f o;
 57                 //o.pos = UnityObjectToClipPos(v.vertex);
 58                 o.pos = UnityObjectToClipPos(v.vertex);
 59 
 60                 o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
 61 
 62                 TANGENT_SPACE_ROTATION;
 63 
 64                 o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
 65                 o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;
 66 
 67                 return o;
 68             }
 69 
 70             fixed4 frag(v2f i) : SV_Target
 71             {
 72                 //光源方向归一化
 73                 fixed3 tangentLightDir = normalize(i.lightDir);
 74                 //视角方向归一化
 75                 fixed3 tangentViewDir = normalize(i.viewDir);
 76 
 77                 //对法线纹理贴图取样
 78                 fixed4 packedNormal = tex2D(_BumpMap,i.uv);
 79                 //切线空间下的法线
 80                 fixed3 tangentNormal;
 81 
 82                 tangentNormal = UnpackNormal(packedNormal);
 83                 tangentNormal.xy *= _BumpScale;
 84                 //反映射
 85                 tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy)));
 86 
 87 
 88 
 89                 fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
 90 
 91                 fixed3 ambient =  UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
 92 
 93                 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(tangentNormal,tangentLightDir));
 94 
 95                 //计算得到矢量h
 96                 fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
 97 
 98                 //高光遮罩
 99                 fixed3 specularMask = tex2D(_SpecularMask,i.uv).r * _SpecularScale;
100 
101                 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss) * specularMask;
102 
103 
104                 return fixed4(ambient + diffuse + specular,1.0);    
105             }
106             ENDCG
107 
108         }
109 
110     }
111     
112     FallBack "Specular"
113 }

 

 

 这里需要注意的是:

① 这里三张纹理共用了 _MainTex_ST ,而不是一张纹理对应一个 _ST 变量。因为随着纹理越来越多,我们会迅速占满顶点着色器中可以使用的插值寄存器。而很多时候,我们并不需要对纹理进行平铺和位移,或者很多纹理使用同一种平铺,那么我们就可以对这些纹理使用同一个纹理坐标。

② 这张遮罩图我们只使用了 r 分量,那么有很多空间都是浪费了,因为一般遮罩纹理的 rgba 存储的是不同的表面属性,善用遮罩纹理,可以创作出高自由度的材质,就可以实现更强的画面效果。

 

最后给出三种纹理的对比图

技术分享图片?

总结

纹理是十分重要的一环,它可以决定你看到的事物有多细腻逼真。

另外再强调一次,本文实现的 shader 仅供学习,因为光照计算并不完整,所以不能直接运用于项目之中

如果对光照不太了解的朋友,可以去翻看我的前一篇文章【Unity Shader】(三) ------ 漫反射和高光反射的实现

 

最后,希望本文能对你有所帮助!!!路漫漫其修远兮 !!!

 

本文实现的 Shader 和纹理

 



以上是关于Unity Shader 获取深度纹理和法线纹理的主要内容,如果未能解决你的问题,请参考以下文章

Unity Shader ------ 纹理之法线纹理单张纹理及遮罩纹理的实现

Unity shader - 如何根据纹理贴图生成法线贴图

着色器编程_unity中的基础纹理,使用Unity Shader实现基础纹理的渲染效果

着色器编程_unity中的基础纹理,使用Unity Shader实现基础纹理的渲染效果

着色器编程_unity中的基础纹理,使用Unity Shader实现基础纹理的渲染效果

Unity Shader ------ 高级纹理(上)