原文:
Structure
Shader Structure
着色器编程难度较大,在学习初期阶段,我们首先需要学习它的基本结构,以便于后面灵活的修改、应用它们。
现代着色器采用的是可变渲染管线,其中顶点着色器、和片段着色器是其基本组成。除此之外,还有可选部分,几何着色器、曲面细分着色器,但是它们的应用场景比较少。顶点着色器的作用是将模型网格,通过矩阵变换,投影到屏幕(实际是投影到裁剪空间)。同时,顶点着色器的一个非常有用的操作是,执行顶点动画。当顶点坐标变换到屏幕空间后,其所构成的三角面片将被栅格化。为了保证三角面片能够正确显示,从顶点着色器到片段着色器的过程中,需要对顶点进行插值,从而得到三角面片中各个片段的位置、颜色值等。
上面简单介绍了着色器的基本流程。接下来我将阐述如何编写着色器、这些“空间坐标系”的具体含义、不同着色器之间的数据传递。但是,我相信了解其基本流程,有助于我们对着色器不同阶段之间的关系有一个直观的理解。因为,在大多数着色器语言中,基本采用了这种基本流程。即便是那些更为高级的、可以通过节点拼接的着色器语言,最终也是将其翻译为这种基本流程。
ShaderLab
在Unity中,着色器实际上就是一个以.shader
结尾的文本文件。我们可以在资源面板下,依次选择Create > Shader >
中的着色器模板,当然模板中的内容可能并不是我们想要的,不过没关系,下面我将介绍如何自定义着色器。为了易于上手,这里我以模板着色器Create > Shader > Unlit
为参考,编写我们自己的着色器。当然这里我写的和Unlit
之间最大的区别在于,我们这个是不会处理雾效,同时又增加了一个颜色属性,这样可以从整体对模型颜色进行调整。接下来我也会一步一步的讲解其实现逻辑。本教程目标是着色器小白,所以你如果有哪里不理解的地方,可以告诉我,促使我对其进行调整,以便于后续学习者能够更加顺畅。
1 | Shader "Tutorial/001-004_Basic_Unlit"{ |
Whats ShaderLab?
Shaderlab
是Unity内置的着色器语言,其中定义了绝大多数渲染模型所需的数据。但是着色器执行渲染逻辑的部分,实际上是采用hlsl
、glsl
、CG
这三种着色语言中的一种。这里hlsl
是微软开发的底层着色器语言,glsl
是英伟达开发的底层着色器语言,CG
是更为高级的作色器语言。而这些执行部分在Shaderlab
中占有一个独立的区域。具体一点,Shaderlab
是在执行hlsl
、glsl
、CG
的基础上,扩展了一些属于自身的语法,其中包括Properties
属性块,用来关联外部输入参数。
从上图可以看到,实际ShaderLab
扩展的仅占着色器很小区域。其中一部分原因是:Shaderlab
不是可执行语言,而是一种抽象的描述性语言,定义了着色器有哪些输入参数、需要执行哪些渲染操作。而Unity便会识别这些描述性语言,然后将其翻译为GPU可执行的作色器语言,同时关联渲染所需的数据。对于一些简单的渲染需求,参考上图的例子,然后对CG
部分进行简单的调整就行了。
Shader/SubShader/Pass
在上图中,你可能也发现了,其中有很多个{}
花括号,将程序分为很多个块。下面我们来看看,这些块到底是干嘛的。
首先,最外层的Shader
块,代表了整个作色器。在我们创建材质球后,需要在材质球面板上选择所需的着色器,从而得到我们所需的材质球。那些在材质面板上的着色器名称,实际上就是紧跟在Shader
块后面的字符串。在这个字符串中,我们可以使用/
反斜杠来对着色器进行有效的组织分类,这很像我们文件目录的组织形式。在本教程中,我将所有的着色器划分到Tutorial
这个大类中,其中还会细分出一些小类。当然,这些分类可以根据需要随意改动,只要达到有效组织的目的就行。
可以看到,所谓的着色器,实际上也就是一连串描述的文本文件。需要注意的是,一个文本文件,只能定义一个着色器。但是,一个着色器却可以复用另一个着色器的功能。例如,在Shader
块中,也就是最外层花括号中,我们可以定义fallback shader
,当Unity将其翻译为更底层的着色器代码时,会将fallback shader
中的SubShader
块复制过来。
在Shader
块中,可以定义多个SubShader
块。在模型渲染时,只会从中选择一个SubShader
来执行,而具体选择哪个,依赖于实际运行的平台。可惜的是,关于如何定义SubShader
的说明文档极其匮乏,根据我多年的经验,在很多情况下,一个Shader
块中只定义一个SubShader
能满足基本需求,减少很多不必要的麻烦。凡事皆有特例,当我们想实现阴影效果时,需要在当前SubShader
块中实现相应的ShadowPass
,每次都实现一遍很麻烦。因为阴影着色流程基本固定,所以Unity提供的现成的便可以使用,这时候,我们可以使用包含阴影着色逻辑的fallback shader
,当渲染时,从该SubShader
中未找到可以使用的ShadowPadd
时,便会从fallback shader
中去查找。大多数情况下,我们使用VertexLit
着色器,来作为我们的fallback shader
,因为VertexLit
中的逻辑简单、性能消耗低、基本上能够兼容所有的显卡,也可能是大家相互Copy,从而形成VertexLit
流行的假象:-)。另外,在SubShader
块中,我们可以定义Subshader tags;还可以定义多个Pass
,例如前面说的Shadow pass
;以及属性,在SubShader
块中定义的属性是由所有Pass
共享的。
一个Pass
包含一套完整的渲染流程,从底层着色语言的角度来看,一个Pass
才是实际上的着色器,它将模型数据转换为五彩斑斓的画面。在内置渲染管线中,如果我们在SubShader
块中定义了多个Pass
,当该SubShader
被平台选定时,其中的Pass
将会被一个接着一个的执行(而最新的URP渲染管线,目前仅支持单个光照Pass
)。对于具有多个Pass
的SubShader
,我们可以将其公共属性等数据定义在SubShader
中,而Pass
中定义一些独有的数据或逻辑。
Properties and Tags
你们可能注意到,在上图中还有两个块Properties
、Tags
未被提及,那我们继续吧。
在很多编程语言中有字典的概念,顾名思义,就是类似汉语字典一样,可以通过拼音、笔画等关键信息进行快速检索。这里的Tags
就可以类比到字典,在Tags
中可以定义多个关键字,以及关键字所对应的值,它们公共构成了着色器的配置参数。其中关键字表示了参数的类型,值表示了参数的实际设置。在SubShader
中,可以通过Tags
定义着色器的材质表现、渲染顺序、以及其他操作;而Pass
中的Tags
主要定义了光照模式。详细的说明可以参考SubShader Tags和Pass Tags。
Properties
属性块,主要用来定义材质面板中的属性显示。通过材质面板来调整材质参数具有一定的局限性,因为整个调整是以材质球为单位,也就是说,如果多个模型使用同一个材质球,那么就无法做到材质差异化。这时候需要创建多个材质球,分别对应于不同的模型。在接下来得教程中我也会详细讨论Properties
的使用。
下面我们对Shaderlab
的基本结构进行一个总结:
1 | Shader "Category/Name"{ |
Source
所有的教程都有配套源码,可以在教程结尾找到相关链接。因为目前我只是做了一些简单的分析介绍,所有源码在上面已经出现过了,这里直接简单的整理一下。
1 | Shader "Tutorial/001-004_Basic_Unlit"{ |
希望你能喜欢这个教程哦!如果你想支持我,可以关注我的推特,或者通过ko-fi、或patreon给两小钱。总之,各位大爷,走过路过不要错过,有钱的捧个钱场,没钱的捧个人场:-)!!!