URL
status
type
date
slug
summary
tag
category
password
icon
做Shader笔记的前后缘由一、ShaderLab介绍二、Shader的语法1.Shader命名2.属性集Properties借助Cg语言声明Properties3.重量级成员:SubShader● 状态设置RenderSetup●SubShader的标签Tags● Pass语义块4.留一条后路:Fallback三、Unity Shader的形式1.Unity的宠儿:表面着色器2.最聪明的孩子:顶点/片元着色器3.被抛弃的角落:固定函数着色器三种着色器的对比Unity Shader的编译我可以使用GLSL来写吗扩展阅读
做Shader笔记的前后缘由
今年春节期间,窝在老家的楼顶啃了啃冯乐乐的《Unity Shader入门精要》,对着电脑啃了两章,了解了下渲染流水线的底层原理,然后就半途而废了。我一直觉得图形学是一个挺难的计算机科学分支,发展时间较晚但发展速度很快,就算读研的方向是图形学,也不见得能深入到哪里去,觉得只要会把材质球丢到游戏对象上去就行了,会用不一定要理解,但是总觉得最近学习没什么动力,游戏开发所需的知识很庞杂,这个也学,那个也学,没有什么明显的成果,再加上突然的感冒发烧,所以就打算继续啃这本书,调整一下精神状态。这页笔记记录的是第三章,UnityShader基础。
一、ShaderLab介绍
“Unity Shader是Unity为开发者提供的高层级的渲染抽象层。Unity希望通过这种方式来让开发者更加轻松地控制渲染。
Unity Shader为控制渲染过程提供了一层抽象。如果没有使用Unity Shader,开发者需要和很多文件和设置打交道,才能让画面呈现出想要的效果;而在Unity Shader的帮助下,开发者只需要使用ShaderLab来编写Unity Shader文件就可以完成所有的工作”
“在Unity中,所有的Unity Shader都是使用ShaderLab来编写的。ShaderLab是Unity提供的编写Unity Shader的一种说明性语言。它使用了一些嵌套在花括号内部的语义
(syntax)
来描述一个Unity Shader文件的结构。这些结构包含了许多渲染所需的数据,例如Properties
语句块中定义了着色器所需的各种属性,这些属性将会出现在材质面板中。从设计上来说,ShaderLab类似于CgFX和Direct3D Effects(.FX)语言,它们都定义了要显示一个材质所需的所有东西,而不仅仅是着色器代码。
二、Shader的语法
一个Unity Shader的基础结构如下所示:
1.Shader命名
“通过在字符串中添加斜杠(“/”),可以控制Unity Shader在材质面板中出现的位置。例如:”
“那么这个Unity Shader在材质面板中的层级就是:Shader->Custom-> MyShader“
2.属性集Properties
“Properties语义块中包含了一系列属性(property),这些属性将会出现在材质面板中。”
Properties语义块的定义通常如下:
开发者们声明这些属性是为了在材质面板中能够方便地调整各种材质属性。如果我们需要在Shader中访问它们,就需要使用每个属性的名字(Name)。在Unity中,这些属性的名字通常由一个下划线开始。显示的名称(display name)则是出现在材质面板上的名字。我们需要为每个属性指定它的类型(PropertyType),常见的属性类型如下表所示。除此之外,我们还需要为每个属性指定一个默认值DefaultValue,在我们第一次把该Unity Shader赋给某个材质时,材质面板上显示的就是这些默认值。
Properties语义块支持的属性类型
属 性 类 型
| 默认值的定义语法
| 例 子 |
Int | number | _Int ("Int", Int) = 2 |
Float | number | _Float(”Float”,Float)=1.5 |
Range(min,max) | number | _Range(”Range”,Range(0.0,5,0))=3.0 |
Color | (number,number,number,number) | _Color(”Color”,Color)=(1,1,1,1) |
Vector | (number,number,number,number) | _Vector(”Vector”,Vector)=(2,3,6,1) |
2D | “defaulttexture”{} | _2D(”2D”,2D)=””{} |
Cube | “defaulttexture”{} | _Cube(”Cube”,Cube)=”white”{} |
3D | “defaulttexture”{} | _3D(”3D”,3D)=”black”{} |
对于Int、Float、Range这些数字类型的属性,其默认值就是一个单独的数字;
对于Color和Vector这类属性,默认值是用圆括号包围的一个四维向量;
对于2D、Cube、3D这3种纹理类型,默认值的定义稍微复杂,它们的默认值是通过一个字符串后跟一个花括号来指定的,其中,字符串要么是空的,要么是内置的纹理名称,如“white”“black”“gray”或者“bump”。花括号的用处原本是用于指定一些纹理属性的,例如在Unity 5.0以前的版本中,我们可以通过TexGenCubeReflect、TexGen CubeNormal等选项来控制固定管线的纹理坐标的生成。但在Unity 5.0以后的版本中,这些选项被移除了,如果我们需要类似的功能,就需要自己在顶点着色器中编写计算相应纹理坐标的代码。
“下面的代码给出了一个展示所有属性类型的例子:
上述代码在材质面板中的显示结果。
借助Cg语言声明Properties
有时,我们想要在材质面板上显示更多类型的变量,例如使用布尔变量来控制Shader中使用哪种计算。Unity允许我们重载默认的材质编辑面板,以提供更多自定义的数据类型。我们在本书资源的材质Assets -> Materials -> Chapter3 -> RedifyMat
中提供了这样一个简单的例子,这个例子参考了官方手册的Custom Shader GUI一文(http://docs.unity3d.com/Manual/SL-CustomShaderGUI.html)中的代码。
通常,Cg的代码片段是位于Pass语义块内部的,如下所示:
为了在Shader中可以访问到这些属性,我们需要在Cg代码片中定义和这些属性类型相匹配的变量。需要说明的是,即使我们不在Properties语义块中声明这些属性,也可以直接在Cg代码片中定义变量。此时,我们可以通过脚本向Shader中传递这些属性。因此,Properties语义块的作用仅仅是为了让这些属性可以出现在材质面板中。
3.重量级成员:SubShader
每一个Unity Shader文件可以包含多个SubShader
语义块,但最少要有一个。
当Unity需要加载这个Unity Shader时,Unity会扫描所有的SubShader语义块,然后选择第一个能够在目标平台上运行的SubShader。
如果都不支持的话,Unity就会使用Fallback语义指定的Unity Shader。
Unity提供这种语义的原因在于,不同的显卡具有不同的能力。例如,一些旧的显卡仅能支持一定数目的操作指令,而一些更高级的显卡可以支持更多的指令数,那么我们希望在旧的显卡上使用计算复杂度较低的着色器,而在高级的显卡上使用计算复杂度较高的着色器,以便提供更出色的画面。
SubShader语义块中包含的定义通常如下:
SubShader中定义了一系列Pass以及可选的状态([RenderSetup])和标签
([Tags])设置。
每个Pass定义了一次完整的渲染流程,但如果Pass的数目过多,往往会造成渲染性能的下降。因此,我们应尽量使用最小数目的Pass。
状态和标签同样可以在Pass声明。不同的是,SubShader中的一些标签设置是特定的。
也就是说,这些标签设置和Pass中使用的标签是不一样的。而对于状态设置来说,其使用的语法是相同的。但是,如果我们在SubShader进行了这些状态设置,那么将会覆盖所有的Pass。
● 状态设置RenderSetup
ShaderLab提供了一系列渲染状态的设置指令,这些指令可以设置显卡的各种状态,例如是否开启混合/深度测试等。下表给出了ShaderLab中常见的渲染状态设置选项。
状态名称 | 设置指令 | 解释 |
Cull | Cull Back|Front|Off | 设置剔除模式:剔除背面/正面/关闭剔除 |
ZTest | Ztest Less Greater|LEqual|GEqual|Equal|NotEqual|Always | 设置深度测试时使用的函数 |
ZWrite | ZWrite On|Off | 开启/关闭深度写入 |
Blend | Blend SrcFactor DstFactor | 开启并设置混合模式 |
当在SubShader块中设置了上述渲染状态时,将会应用到所有的Pass。如果我们不想这样(例如在双面渲染中,我们希望在第一个Pass中剔除正面来对背面进行渲染,在第二个Pass中剔除背面来对正面进行渲染),也可以在Pass语义块中单独进行上面的设置。
●SubShader的标签Tags
SubShader的标签(Tags)是一个键值对(Key/Value Pair),它的键和值都是字符串类型。这些键值对是SubShader和渲染引擎之间的沟通桥梁。它们用来告诉Unity的渲染引擎:我希望怎样以及何时渲染这个对象。
标签的结构如下
SubShader的标签块支持的标签类型如下表所示。
标签类型 | 说明 | 例子 |
Queue | 控制渲染顺序,指定该物体属于哪一个渲染队列,通过这种方式可以保证所有的透明物体可以在所有不透明物体后面被渲染(详见第8章),我们也可以自定义使用的渲染队列来控制物体的渲染顺序 | Tags { “Queue” = “Transparent” } |
RenderType | 对着色器进行分类,例如这是一个不透明的着色器,或一个透明的着色器等。这可以被用于着色器替换功能(Shader Replacement) | Tags { “RenderType” = “Opaque” } |
DisableBatching | 一些SubShader在Unity的批处理功能时会出现问题,例如使用了模型空间下的坐标进行顶点动画(详见11.3节)。这是可以通过该标签来直接指明是否对该SubShader使用批处理 | Tags { “DisableBatching” = “True” } |
ForceNoShadowCasting | 控制使用该SubShader的物体是否会投射阴影(详见8.4节) | Tags { “ForceNoShadowCasting” = “True” } |
IgnoreProjector | 如果该标签值为“True”,那么使用该SubShader的物体将不会受Projector的影响。通常用于半透明物体 | Tags { “IgnoreProjector” = “True” } |
CanUseSpriteAtlas | 当该SubShader是用于精灵(sprites)时,将该标签设为”False” | Tags { “CanUseSpriteAtlas” = “False” } |
PreviewType | 指明材质面板将如何预览该材质。默认情况下,材质将显示为一个球形,我们可以通过把该标签设为“Plane””SkyBox”来改变预览类型 | Tags { “PreviewType” = “Plane” } |
具体的标签设置我们会在本书后面的章节中讲到。
需要注意的是,上述标签仅可以在SubShader中声明,而不可以在Pass块中声明。Pass块虽然也可以定义标签,但这些标签是不同于SubShader的标签类型。这是我们下面将要讲到的。
● Pass语义块
Pass语义块包含的语义如下:
首先,我们可以在Pass中定义该Pass的名称,例如:
“通过这个名称,我们可以使用ShaderLab的UsePass命令来直接使用其他Unity Shader中的Pass。例如:”
这样可以提高代码的复用性。需要注意的是,由于Unity内部会把所有Pass
的名称转换成大写字母的表示,因此,在使用UsePass命令时必须使用大写形式的名字。
其次,我们可以对Pass设置渲染状态。SubShader的状态设置同样适用于Pass。除了上面提到的状态设置外,在Pass中我们还可以使用固定管线的着色器(详见3.4.3节)命令。
Pass同样可以设置标签,但它的标签不同于SubShader的标签。这些标签也是用于告诉渲染引擎我们希望怎样来渲染该物体。表给出了Pass中使用的标签类型。
标签类型 | 说明 | 例子 |
LightMode | 定义该Pass在Unity的渲染流水线中的角色 | Tags { “LightMode” = “ForwardBase” } |
RequireOptions | 用于指定当满足某些条件时才渲染该Pass,它的值是一个由空格分隔的字符串。目前,Unity支持的选项有SoftVegetation.在后面的版本中,可能会增加更多的选项 | Tags { “RequireOptions” = ”SoftVegetation” } |
除了上面普通的Pass定义外,Unity Shader还支持一些特殊的Pass
,以便进行代码复用或实现更复杂的效果。
- UsePass:如我们之前提到的一样,可以使用该命令来复用其他Unity Shader中的Pass;
- GrabPass:该Pass负责抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass处理(详见10.2.2节)。
4.留一条后路:Fallback
紧跟在各个SubShader语义块后面的,可以是一个Fallback指令。Fallback相当于switch语句中的default,它用于告诉Unity,“如果上面所有的SubShader在这块显卡上都不能运行,那么就使用这个最低级的Shader吧!”
“它的语义如下:”
如上所述,我们可以通过一个字符串来告诉Unity这个“最低级的Unity Shader”是谁。我们也可以任性地关闭Fallback功能,但一旦你这么做,你的意思大概就是:“如果一块显卡跑不了上面所有的SubShader,那就不要管它了!”
下面给出了一个使用Fallback语句的例子:
事实上,Fallback还会影响阴影的投射。在渲染阴影纹理时,Unity会在每个Unity Shader中寻找一个阴影投射的Pass。通常情况下,我们不需要自己专门实现一个Pass,这是因为Fallback使用的内置Shader中包含了这样一个通用的Pass。因此,为每个Unity Shader正确设置Fallback是非常重要的。更多关于Unity中阴影的实现,可以参见9.4节。
ShaderLab还有其他的语义吗?
除了上述的语义,还有一些不常用到的语义。例如,如果我们不满足于Unity内置的属性类型,想要自定义材质面板的编辑界面,就可以使用CustomEditor语义来扩展编辑界面。我们还可以使用Category语义来对Unity Shader中的命令进行分组。由于这些命令很少用到,本书将不再进行深入的讲解。
三、Unity Shader的形式
尽管Unity Shader可以做的事情非常多(例如设置渲染状态等),但其最重要的任务还是指定各种着色器所需的代码。这些着色器代码可以写在SubShader语义块中(表面着色器的做法),也可以写在Pass语义块中(顶点/片元着色器和固定函数着色器的做法)。
在Unity中,我们可以使用下面3种形式来编写Unity Shader。而不管使用哪种形式,真正意义上的Shader代码都需要包含在ShaderLab语义块中,如下所示:
1.Unity的宠儿:表面着色器
表面着色器(Surface Shader)是Unity自己创造的一种着色器代码类型。它需要的代码量很少,Unity在背后做了很多工作,但渲染的代价比较大。它在本质上和下面要讲到的顶点/片元着色器是一样的。也就是说,当给Unity提供一个表面着色器的时候,它在背后仍旧把它转换成对应的顶点/片元着色器。我们可以理解成,表面着色器是Unity对顶点/片元着色器的更高一层的抽象。它存在的价值在于,Unity为我们处理了很多光照细节,使得我们不需要再操心这些“烦人的事情”。
一个非常简单的表面着色器示例代码如下:
从上述程序中可以看出,表面着色器被定义在SubShader
语义块(而非Pass语义块)中的CGPROGRAM和ENDCG之间。原因是,表面着色器不需要开发者关心使用多少个Pass、每个Pass如何渲染等问题,Unity会在背后为我们做好这些事情。我们要做的只是告诉它:“嘿,使用这些纹理去填充颜色,使用这个法线纹理去填充法线,使用Lambert光照模型,其他的不要来烦我!”。
CGPROGRAM和ENDCG之间的代码是使用Cg/HLSL编写的,也就是说,我们需要把Cg/HLSL语言嵌套在ShaderLab语言中。值得注意的是,这里的Cg/HLSL是Unity经封装后提供的,它的语法和标准的Cg/HLSL语法几乎一样,但还是有细微的不同,例如有些原生的函数和用法Unity并没有提供支持。
2.最聪明的孩子:顶点/片元着色器
在Unity中我们可以使用Cg/HLSL语言来编写顶点/片元着色器(Vertex/Fragment Shader)。它们更加复杂,但灵活性也更高。
一个非常简单的顶点/片元着色器示例代码如下:
和表面着色器类似,顶点/片元着色器的代码也需要定义在CGPROGRAM
和ENDCG之间,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内的。原因是,我们需要自己定义每个Pass需要使用的Shader代码。虽然我们可能需要编写更多的代码,但带来的好处是灵活性很高。更重要的是,我们可以控制渲染的实现细节。同样,这里的CGPROGRAM和ENDCG之间的代码也是使用Cg/HLSL编写的。
3.被抛弃的角落:固定函数着色器
上面两种Unity Shader形式都使用了可编程管线。而对于一些较旧的设备(其GPU仅支持DirectX 7.0、OpenGL 1.5或OpenGL ES 1.1),例如iPhone 3,它们不支持可编程管线着色器,因此,这时候我们就需要使用固定函数着色器(Fixed Function Shader)
来完成渲染。这些着色器往往只可以完成一些非常简单的效果。
一个非常简单的固定函数着色器示例代码如下:
可以看出,固定函数着色器的代码被定义在Pass语义块中,这些代码相当于Pass中的一些渲染设置,正如我们之前在3.3.3节中提到的一样。
对于固定函数着色器来说,我们需要完全使用ShaderLab的语法(即使用ShaderLab的渲染设置命令)来编写,而非使用Cg/HLSL。
由于现在绝大多数GPU都支持可编程的渲染管线,这种固定管线的编程方式已经逐渐被抛弃。实际上,在Unity 5.2中,所有固定函数着色器都会在背后被Unity编译成对应的顶点/片元着色器,因此真正意义上的固定函数着色器已经不存在了,而表面着色器也会被编译成顶点/片元着色器,因此从本质上来说Unity中只存在顶点/片元着色器。
三种着色器的对比
ㅤ | 表面着色器(Surface Shader) | 顶点/片元着色器(Vertex/Fragment Shader) | 固定函数着色器(Fixed Function Shader) |
位置 | CGPROGRAM和ENDCG之间,SubShader内 | CGPROGRAM和ENDCG之间,Pass内 | Pass内 |
语言 | Cg/HLSL/GLSL | Cg/HLSL/GLSL | ShaderLab的渲染设置命令 |
特点 | 代码量少,渲染成本高 | 灵活很高,渲染实现细节可控 | 直接通过代码调用 |
适用场景 | 如果你想和各种光源打交道,你可能更喜欢使用表面着色器,但需要小心它在移动平台的性能表现。 | 光照数目非常少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。 | 在不支持可编程管线着色器的旧设备上运行游戏。 |
Unity Shader的编译
在提供给编程人员这些便利的背后,Unity编辑器会把这些Cg片段编译成低级语言,如汇编语言等。通常,Unity会自动把这些Cg片段编译到所有相关平台(这里的平台是指不同的渲染平台,例如Direct3D 9、OpenGL、Direct3D 11、OpenGL ES等)上。这些编译过程比较复杂,Unity会使用不同的编译器来把Cg转换成对应平台的代码。这样就不会在切换平台时再重新编译,而且如果代码在某些平台上发生错误就可以立刻得到错误信息。
“正如在3.1.3节中看到的一样,我们可以在Unity Shader的导入设置面板上查看这些编译后的代码,查看这些代码有助于进行Debug或优化等,如下图所示。
在Unity Shader的导入设置面板中可以通过Compile and show code按钮来查看Unity对CG片段编译后的代码。通过单击Compile and show code按钮右端的倒三角可以打开下拉菜单,在这个下拉菜单中可以选择编译的平台种类,如只为当前的显卡设备编译特定的汇编代码,或为所有的平台编译汇编代码,我们也可以自定义选择编译到哪些平台上。
但当发布游戏的时候,游戏数据文件中只包含目标平台需要的编译代码,而那些在目标平台上不需要的代码部分就会被移除。例如,当发布到Mac OS X平台上时,DirectX对应的代码部分就会被移除。”
我可以使用GLSL来写吗
当然可以。如果你坚持说:“我就是不想用Cg/HLSL来写!就是要使用GLSL来写!”,但是这意味着你可以发布的目标平台就只有Mac OS X、OpenGL ES 2.0或者Linux,而对于PC、Xbox 360这样的仅支持DirectX的平台来说,你就放弃它们了。
建立在你坚持要用GLSL来写Unity Shader的意愿下,你可以怎么写呢?和Cg/HLSL需要嵌套在CGPROGRAM和ENDCG之间类似,GLSL的代码需要嵌套在GLSLPROGRAM和ENDGLSL之间。
更多关于如何在Unity Shader中写GLSL代码的内容可以在Unity官方手册的GLSL Shader Programs一文(http://docs.unity3d.com/Manual/SLGLSLShaderPrograms.html)中找到。
扩展阅读
Unity官网上关于Unity Shader方面的文档正在不断补充中,由于Unity封装了很多功能和细节,因此,如果读者在使用Unity Shader的过程中遇到了问题可以去到官方文档(http://docs.unity3d.com/Manual/SL-Reference.html
)中查看。除此之外,Unity也提供了一些简单的着色器编写教程(http://docs.unity3d.com/Manual/ShaderTut1.html
,http://docs.unity3d.com/Manual/ShaderTut2.html
)。由于在Unity Shader中,绝大多数可编程管线的着色器代码是使用Cg语言编写的,读者可以在NVIDIA提供的Cg文档(http://http.developer.nvidia.com/Cg/
)中找到更多的内容。NVIDIA同样提供了一个系列教程(http://http.developer.nvidia.com/CgTutorial/cg_tutorial_chapter01.html
)来帮助初学者掌握Cg的基本语法。
摘录来自
Unity Shader入门精要
冯乐乐
- 作者:Cloud
- 链接:https://cloud09.xyz/article/e29e012f-25de-4363-88a4-423de53a267f
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。