Summary
在Unity中,我们可以创建一个空的着色器脚本,然后手动一行一行的去实现整个脚本。当然这种方式比较费时,毕竟着色器有一套固定的流程,因此可以将一部分代码复用,例如光照模型。如果能够直接配置一些参数就能自动生成相关代码,你一定会喜欢吧。Unity就是这么会投其所好,它实现了一种名叫表面着色器的东东,刚好能够满足咱懒人的需求。懒-是推动科技发展的第一生产力,至理名言啊!
那什么是表面作色器呢?在进入正题之前,我建议你先了解最简单的无光照的着色器,如果你不清楚,可以参考我上一个教程。
Conversion to simple Surface Shader
相比于前面介绍的着色器实现方法,表面着色器的实现就显得更加简洁,原先需要处理的很多内容都可以剔除,因为Unity会自动帮我们生成相关代码。以上一个教程的着色器脚本为例,如果我们要用表面着色器来实现,那么前面提到的什么顶点着色器都可以不要了。与之相对应的宏命令也可以删除,两者之间的插值数据也可以不要了。甚至是UnityCG.cginc
文件也可以不要,还有MainTex_ST
等等。这些代码最终都会由Unity自动生成。一顿大刀阔斧,来看看我们的成果吧:
1 | Shader "Tutorial/005_surface" { |
简洁的令人发指!等等,先别急着感叹,还有事没做完。上面的着色器脚本并不能执行,因为表面着色器有自己的一些要求。
首先,我们需要添加一个新的数据类型作为片段着色器的输入。这个数据类型将会包含所有与片段着色相关的必要数据。当然,我们这个简单的不能再简单的着色器只需要传递UV坐标。UV坐标还是二维向量。但是,这里的UV变量命名有特殊的规则。因为UV坐标是用来纹理采样的,上一章讲过UV变换,每一个纹理都有自己的缩放、偏移参数,所以必须将变换后的UV和对应的纹理相关联。这里采用命名规则来实现,首先UV变量必须以uv
开头,然后后面跟随纹理变量的名称。这样,在后面自动生成代码的时候,程序就知道谁和谁配对了。
1 | struct Input { |
接下来我们要对之前的片段着色其进行修改,使其编程表面着色器。为了区分两者,先把函数名改为surf
。然后表面着色器函数是没有返回值的,所以函数返回类型改为void
。
然后再拓展出两个参数。第一个参数正是我们刚刚定义好的Input
结构,通过这个参数,表面着色器可以获取所有相关的必要参数;第二个参数是一个叫做SurfaceOutputStandard
的结构体,从字面意思可以看出,这就是表面着色器的最终输出数据。当然,在函数结束之前,必须计算好、并赋值所有需要外传的参数。除了SurfaceOutputStandard
结构外,Unity还定义了其他类似的结构,它们之间的区别在于适用于不同的光照模型。而这些结构中的成员变量就包含了后续光照处理所必须的数据,这些数据的具体含义将在后面介绍。
然后是删除SV_Target
语义标识符,因为我们的surf
函数的返回类型是void
。
最后是把return
语句删除。然后将计算所得的数据通过SurfaceOutputStandard
传递出去。
1 | void surf (Input i, inout SurfaceOutputStandard o) { |
最后一步,我们需要引入光照模型,同时将写好的表面着色器函数与表面着色器相关联,这个和顶点着色器类似,都是通过#pragma
来实现。因为我们这里的surf
的输出结构是SurfaceOutputStandard
,意味着我们使用标准的光照模型。所以在其后我们还得加上Standard
来说明所用的光照模型,最后的fullforwordshadows
是告诉Unity使用前向阴影。
1 | Shader "Tutorial/005_surface" { |
Standard Lighting Properties
在SurfaceOutputStandard
结构中包含很多材质相关的属性参数,具体如下:
- Albedo : 表示材质表面的颜色。而材质最终呈现的颜色受光照的影响,因为材质本身只能决定对吸收哪些波段的光。
- Normal : 表示材质表面法向向量。法向向量一般和顶点坐标一起,存储在模型数据中,这时候法向向量所在的坐标系是切向空间。什么是切向空间呢?可以简单的理解为沿着模型表面的坐标系。而法向是指垂直与模型表面的方向。法向向量一般要变换到世界坐标系再参与计算。
- Emission :表示材质的自发光特性。一般的的模型渲染依赖于外部光源,如果关闭外部光源,那么模型表现为黑色。而具备自发光材质的模型即便没有外部光源也能显示出原本的颜色。一般场景中的灯具、或者熔岩会使用自发光属性。
- Metallic :表示材质的金属特性。现实中,金属材料和非金属材料的光学特性不一样,例如即便黑色的金属也能在阳光下反射光芒,但是黑色的非金属就表现的黯淡无光。
- Smoothness : 表示材质的光滑度。光滑程度主要决定漫反射、和镜面反射之间的权重分布。玻璃的光滑度非常高,所以可以用来做镜子,而一般木材非常粗糙。
- Occlusion :表示环境遮罩特性。举个例子,平坦的桌面上,光线能够达到每个角落。而崎岖不平的背包上,那些深深的褶皱显得格外阴暗。这些因为表面相互遮挡而产生的阴影就是我们这里的环境遮罩效果。
- Alpha : 表示材质的透明度。从字面可以很好理解,有些材质是透明的,如玻璃,有些不是,如木头。
上面提到的这些参数前三个是向量,后四个是标量。这些参数有些可以直接暴露在材质面板,方便美术编辑材质效果。
Implement a few Lighting Properties
接下来我们把emission
、metallic
、smoothness
三个参数为例,将其作为材质的可调参数。当然你也可以根据实际需求做调整。
首先,我们定义两个公共变量:光滑度和金属度。它们的类型我选择half
。一般来说除了坐标采用float
,其他的都选half
类型。
1 | half _Smoothness; |
然后将这些公共变量添加到Properties
块中,将其暴露在材质面板上。但是材质面板并不知道所显示的变量的类型,所以还需要在其名称后增加类型说明,如下所示:
1 | Properties { |
在surf
函数中,我们可以直接将这些公共变量传递给SurfaceOutputStandard
结构体,这样在后续的光照计算中,就可以使用这些参数了,同时我们修改材质面板上的值,便能立即改变材质的表现效果。
1 | void surf (Input i, inout SurfaceOutputStandard o) { |
目前为止,表面着色器已经基本完成了。但是还有些需要完善的地方,因为我们在材质面板上修改参数时,很容易设置到非法值,最终导致材质显示异常。我们可以在Properties
做些小修改,将float
修改为Range(0,1)
就可以将这些参数限定在一个有效范围内。
1 | Properties { |
接下来我们添加自发光颜色。同样的我们也需要在公共变量、和Properties
中加入_Emission
的定义。
1 | // ... |
为了避免自发光材质过度曝光,我们只能将自发光颜色限定在[0-1]
之间。当然,如果我们将自发光颜色定义为HDR类型的话,就可以不用担心过曝的问题了。如果我们使用纹理来将材质各个部位的自发光特性差别化,那么可以产生很炫的效果。例如怪兽的眼睛放光的效果。
1 | [HDR] _Emission ("Emission", Color) = (0,0,0,1) |
Minor Improvements
最后,让我们做一些小改动,让我们的材质看起来更自然。首先,我们在着色器脚本最后面加上fallback shader
,这样我们可以复用其他着色器中的代码。这里我将标准着色器作为我们的fallback shader
,然后复用其中的阴影渲染部分shadow pass
的代码,这样就可以让我们的材质也表现出阴影效果。我们使用fullfowardshadows
参数进而获得很好的阴影效果。另外我们也可以指定当前着色器的目标平台,例如设置target
为3.0。其中target
的值越高,表示可以使用的特性越多,但是支持的硬件平台会越少。这里target
为3.0,已经可以使用大多数的高级特性了,所以能够实现更好的光照效果。
1 | Shader "Tutorial/005_surface" { |
希望本章的介绍能让你有所收获!
你可以在以下链接找到源码:https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/005_Surface_Basics/simple_surface.shader
希望你能喜欢这个教程哦!如果你想支持我,可以关注我的推特,或者通过ko-fi、或patreon给两小钱。总之,各位大爷,走过路过不要错过,有钱的捧个钱场,没钱的捧个人场:-)!!!