本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:
- 书本中句子照抄 + 个人批注
- 项目源码
- 一堆新手会犯的错误
- 潜在的太监断更,有始无终
总之适用于同样开始学习Shader的同学们进行有取舍的参考。
文章目录
(该系列笔记中大多数都会复习前文的知识,特别是前文知识非常重要的时候,这是为了巩固记忆,诸位可以直接通过目录跳转)
复习
Shader的定义
上一节我们学习了Unity Shader的定义、使用方法和基本结构。让我们回忆一下。
我们在学习渲染流水线的时候了解到,Shader是用于着色器编程的。我们首先要区分,Unity中的Shader和我们在渲染流水线中的Shader不是同一个东西,Unity中的Shader统称为Unity Shader。如果开发者需要配置渲染流水线,就需要进行诸多配置以及着色器代码编写,其中涉及的配置和文件实在太多。
Unity为这些工作集成了一个方便的抽象层,也就是Unity Shader 。我们仅需使用ShaderLab来编写Unity Shader就可以完成上述一系列工作。因此ShaderLab定义了一个材质所需的所有东西,而不仅仅是着色器代码。
Shader的使用方法
我们在为3D网格渲染的时候,离不开两个东西:材质(material)和Shader,一个网格的渲染设置需要经过下面的步骤:
- 创建材质
- 创建Shader并赋予材质
- 将材质赋予网格体
- 调整材质的面板属性以得到满意的效果
Material本质上是一个数据集,它包含了Shader的渲染设置,以及Shader需要读取的那些数据(例如Texture纹理数据和Map贴图)。
Unity现在提供了五种Shader模板用于创建:
Standard Surface Shader会产生一个包含标准光照模型的表面着色器模板。
Unlit Shader会产生一个不包含光照(但包含雾效)的基本的顶点/片元着色器。
Image Effect Shader为我们实现各种屏幕的后处理效果提供一个基本的模板。
Compute Shader 会产生一种特殊的Shader 文件,这类Shader旨在利用GPU的并行性来进行一些与常规渲染流水线无关的计算。
高版本还提供了Ray Tracing Shader,一种用于利用RTX显卡实现光线追踪的Shader。
通过在Shader的Import面板上可以看到Shader的一些基本信息,例如Import Setting面板上能看到那些会显示在材质上的属性。Import Object面板则能够看到该Shader的Tags以及定义的Properties。
Shader的代码结构
最后我们学习了Shader的代码结构:
Shader "ShaderName"
{
Properties
{
//属性
Name ("displayName",PropertyType) = DefaultValue
}
SubShader
{
//显卡A使用的子着色器
[Tag]
[RenderSetup]
pass{
[Name]
[Tag]
[RenderSetup]
}
}
SubShader
{
//显卡B使用的子着色器
}
Fallback "VertexLit"
}
上述代码是一个最简单的Shader代码结构,主要可以分为三个代码块:Properties
、SubShader
、pass
。以ShaderName为开头,以Fallback收尾。
我们自上而下来解析ShaderLab的代码结构:
ShaderName
首先第一行是Shader "ShaderName"
,这行指定了Shader的名称以及路径。格式是Shader "一级目录/二级目录/ShaderName"
(层级可以更多)。
Properties
接着是Properties
,定义了该Shader的属性,这些属性就是一些可调整可访问的变量。以该格式定义:
Name ("displayName",PropertyType) = DefaultValue
其中Name代表了属性名(如果想用C#代码调用则需要按照这个字符串访问,通常Name的起名是下划线加大驼峰,例如_MainTex
),displayname代表了该属性在面板上显示的名称,PropertyType定义了该属性的类型,DefaultValue则是该Shader首次被赋值给材质后的初始值。
属性类型 | 默认值的定义语法 | 例子 |
---|---|---|
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” {} |
上表展示了一些属性类型以及使用样例
SubShader
SubShader是Shader的主要部分,其内部定义了一系列Pass 以及各类可选的设置([Tag],[RenderSetup]):
RenderSetup
状态名称 | 设置指令 | 解释 |
---|---|---|
Cull | Cull Back | Front | Off | 设置剔除模式:剔除背面/正面/关闭剔除 |
ZTest | ZTest Less Greater |LEqual |GEqual | Equal | NotEqual | Always | 设置深度测试时使用的函数 |
ZWrite | ZWrite On | Off | 开启/关闭深度写入 |
Blend | Blend SrcFactor DstFactor | 开启并设置混合模式 |
Tag
Tag是一个键值对(Key/Value Pair),其键和值都是字符串类型的。这些键值对是SubShader和渲染引擎之间的沟通桥梁,它们告诉Unity的渲染引擎我们希望怎样以及何时渲染这个对象:
标签的结构如下:
// 不同标签用空格隔开
Tags {"TagNamel" = "Valuel " "TagName2" = "Value2" }
标签类型 | 说明 | 例子 |
---|---|---|
Queue | 控制渲染顺序,指定该物体属于哪一个渲染队列,通过这种方式可以保证所有的透明物体可以在所有不透明物体后面被渲染。我们也可以自定义使用的渲染队列来控制物体的渲染顺序 | Tags {“Queue” = “Transparent“} |
RenderType | 对着色器进行分类,例如这是一个不透明的着色器,或是一个透明的着色器。这可以被用于着色器替换(Shader Replacement)功能。 | Tags {“RenderType” = “Opaque“} |
DisableBatching | 一些SubShader在使用Unity的批处理功能会出现问题,例如使用了模型空间下的坐标进行顶点动画。这时可以通过该标签来直接指明是否对该SubShader使用批处理 | Tags {“DisableBatching" = “True”} |
ForceNoShadowCasting | 控制使用该SubShader的物体是否会投射阴影 | Tags {“ForceNoShadowCasting" = “True”} |
IgnoreProjector | 如果该标签值为“True”,那么使用该SubShader的物体将不会收到Projector(projector指Unity中的投影仪组件)的影响。通常用于半透明物体 | Tags {“IgnoreProjector" = “True”} |
CanUseSpriteAtlas | 当该SubShader是用于Sprite时,将该标签设为“False” | Tags {“CanUseSpriteAtlas" = “False”} |
PreViewType | 指明材质面板将如何预览该材质。默认情况下,材质将显示为一个球,我们可以通过把该标签的值设为"Plane""SkyBox"来改变预览类型 | Tags {“PreViewType" = “Plane”} |
在SubShader下的RenderSetup会影响其内部的所有Pass的渲染设置。而Pass中的RenderSetup则只会影响自身。就像全局变量与局部变量的关系。
Pass
每个Pass块都定义了一次完整的渲染流程,一个SubShader块中可以有多个Pass块,意味着该Shader每帧渲染都需要经过多次Pass块的渲染。所以为了渲染效率考虑,我们还是尽量少定义Pass块。除非真的有必要使用多个Pass。
我们可以为每个Pass定义其Name ,Tag和RenderSetup。我们可以用下列语句定义Pass的名称:
Name "MyPassName"
通过定义Pass名称, 我们可以使用UsePass 命令来直接使用其他Unity Shader中的Pass。
UsePass "MyShader/MYPASSNAME"
使用方法是UsePass加上使用的Shader名和其内部定义的Pass名,且Pass名需要转为全拼大写。
标签类型 | 说明 | 例子 |
---|---|---|
LightMode | 定义该Pass在Unity的渲染流水线中的角色 | Tags {“LightMode” = “ForwardBase”} |
RequireOptions | 用于指定当满足某些条件时才渲染该Pass,它的值是一个由空格分隔的字符串,目前Unity支持的选项有:SoftVegetation。在后续版本可能增加更多的选项(喜报,根据Unity官方文档,直至2023.2版本依然只有这一个选项) | Tags {“RequireOptions” = “SoftVegetation”} |
同样的,Pass也可以设置标签(注意,Pass设置的标签和SubShader的标签不同,注意区分二者)
Unity Shader还支持一些特殊的Pass,以进行代码复用或实现更复杂的效果:
- UsePass:通过Pass名来调用其他Shader中的Pass块
- GrabPass: 抓取屏幕画面并在结果存储在一张纹理中,以用于后续的Pass处理
Fallback
在Unity Shader中,我们也可以定义多个SubShader块。在加载Shader时,Unity会扫描所有的SubShader块,然后选择第一个能在平台上运行的SubShader块。
这是由于不同显卡对于操作指令的支持能力不同,高级的显卡能进行更复杂的着色器计算,而低级显卡可能不支持。因此我们想要程序支持多个显卡,就需要由高到低定义多个SubShader块。
如果所有的SubShader块都不支持渲染,那么会执行Fallback语句。该语句会在所有的SubShader块都失败后执行指定的Shader,语法如下:
Fallback "Name"
// 或者
Fallback Off
// 最常用的Fallback
Fallback "VertexLit"
我们可以指定一个Shader的Name,也可以Fallback Off关闭Fallback功能。
Unity Shader的形式
在UnityShader中,我们可以设置渲染状态和编写着色器代码。可编写的着色器主要有三类(这里指常用的三类):表面着色器,顶点/片元着色器,固定函数着色器 。其中,表面着色器是在SubShader块中定义的,顶点/片元着色器和固定函数着色器在Pass中定义。
表面着色器
表面着色器并不属于渲染管线中的着色器,它是Unity自己创建的一类着色器。我们可以通过下列代码来定义一个表面着色器:
SubShader{
CGPROGRAM
#Pragma surface surt Lambert
struct Input{
float4 color : COLOR;
}
void surf(Input IN, inout SurfaceOutput o){
//代码
}
ENDCG
}
上述代码展示了使用CG/HLSL定义表面着色器的示例,通常我们需要用#Pragma
预编译指令为表面着色器surface
指定表面函数和光照函数。表面函数由我们自定义,光照函数这里使用ShaderLab自带的,当然也能自定义。
表面着色器的渲染代价比较大,Unity会在背后将其转化成对应的顶点/片元着色器。使用表面着色器,Unity能帮我们处理一些光照细节,使我们不用操心这些“烦人的事情”。
此外,CGPROGRAM
标签实际上只能使用在Pass语句块中,之所以在定义表面着色器的时候能用在SubShader语句块中,是因为表面着色器最终还是会转换成顶点/片元着色器的,本质上还是在Pass中处理CG/HLSL的。(相当于一个语法糖?)
(如果想使用GLSL,可以使用GLSLPROGRAM ENDGLSL
标签)
顶点/片元着色器
顶点/片元着色器(VF)需要在Pass块中定义,也是使用CG/HLSL编写:
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 v : POSITION): SV_POSITION{
//代码
}
fixed4 frag(): SV_Target{
//代码
}
ENDCG
}
}
我们将顶点/片元着色器代码定义在Pass内,这样的好处是灵活性更高,我们可以控制渲染的实现细节。
固定函数着色器
上述两个着色器都使用了可编程管线。而对于一些旧式的设备,不支持可编程着色器的情况下,只能使用固定函数着色器完成渲染了,这些着色器使用ShaderLab语言实现只能完成一些简单的效果。
Unity Shader的优缺点
Unity Shader是对Shader代码以及各种渲染设置的封装。它不等同与Shader。
优点
- 在传统Shader中,一个Shader文件只能对应编写一个着色器。但是Unity Shader支持我们同时包含多种着色器。
- 传统Shader无法设置渲染设置,需要开发者们在额外的代码中设置。Unity Shader可以完成这些设置
- 传统Shader需要冗长的代码设置着色器的输入和输出,并小心处理输入输出的位置对应关系,Unity Shader只需要在特定语句块声明属性,就可以依靠Material方便的修改这些属性,并且对于模型自带的数据也提供了直接访问的方法。
缺点
Unity SHader的缺点在于,由于高度封装,导致可编写的Shader的语法和类型都被限制了,而对于一些类型的Shader,例如曲面细分着色器,几何着色器,Unity Shader的支持就不那么好了。