写在前面
这篇博客是对《Unity Shader 入门精要》第3章-Unity Shader基础的学习记录和总结。从这篇开始我就再也不是“纸上谈兵”得学习理论知识啦!
激动的心颤抖的手!
如何在Unity里创建Shader?创建材质?怎么把一个shader赋给材质?shader怎么赋给场景中的游戏对象?...如果你这些问题都有疑问——还不快去学!这篇博客不会讲这些基础的东西了,只会记录我想要写下来保存的内容(见谅~
之前在学习Unity的时候需要写C#脚本,当时用的是Visual Studio 2022写的,external tool设定的就是VS2022,结果打开Shader发现,即使添加了ShaderlabVS插件,在VS里写Shader竟然无法自动缩进!每次都要一个tab一个tab的手打(心累),于是转战之前Houdini用的sublime。
于是,我参考下面这篇教程,想要完成同时达到:用VS打开C#,用sublime打开Shader,这一效果的配置
用SublimeText当Unity Shader的编辑器 - meteoric_cry - 博客园 (cnblogs.com)
结果我只完成了在sublime里添加Unity Shader包的操作,怎么都实现不了Unity双击.shader文件自动跳到sublime打开的效果,索性直接用了最粗暴的办法:
直接修改系统打开目标文件的默认程序,.shader文件选择“始终以sublime打开”,.cs文件选择“始终以VS 2022打开”。
这种粗暴的办法用起来也没啥不妥,但是其实用sublime写Shader也有不爽的地方——不能自动美化格式,但他可以自动缩进呀!相比VS已经够香的了~
Unity我用的是Unity2021.3.8f1,但《入门精要》书是2016出版用的是Unity5.2.1,上Unity官网看的话,Unity版本3.x --> 4.x --> 5.x --> 2017.x --> ... --> 2022.x,时间线是依次推进的。
系统的话我是用的Win10,Windows是基于DirectX的。
既然提到了Unity版本,这里就先打个预防针,要留意一些Unity不同版本之间内置函数使用的差异性,比如我在学习后面的内容时遇到了:
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
//把MVP矩阵替换成了后面版本出现的内置函数
o.pos = UnityObjectToClipPos(v.vertex);
shader中的UnityObjectToClipPos和UNITY_MATRIX_MVP
上文中清楚的提到了Unity5.6版本之后,Shader中的:
不过,我发现Unity还是很智能的,它会帮你转换写的低版本函数。
Unity提供了Unity Shader让我们能更加轻松地管理着色器代码以及渲染设置,由于渲染设置包含了很多相对复杂的内容,例如逐片元操作(也叫合并)中——做的很多测试工作(深度测试、模板测试)、开启/关闭混合功能等等,我们不需要复杂和冗长的代码去管理,使得上手就轻松了很多。
总之,Unity Shader意义虽然挺抽象,但它本质上就是一个文本文件,有了它,我们学习和编写着色器的过程变得更加简单。
行,那我们怎么才能去使用这个Unity Shader来实现控制呢?——Unity提供了ShaderLab这样一种说明语言,所有的Unity Shader都由它来编写。既然是说明语言,那一定有某些表说明的“标签”,在ShaderLab中这样的“标签”就是一些嵌套在花括号内部的语义(Properties、SubShader等等),这些语义定义了Unity Shader的结构。啊,还有一点,如果你学习过DirectX,一定知道它用FX效果文件(.fx)来管理渲染方式,它也包含了渲染状态的设置和HLSL代码,可以把ShaderLab和FX效果文件类比着记忆。
ShaderLab的语义定义了整个Unity Shader的结构,语义的话有Properties、SubShader、Fallback语义块,它们三个是最最常用的语义,接下来我将参考《入门精要》中3.3章对语义部分的介绍进行叙述。
为什么会说Properties语义块是材质和Unity Shader的桥梁?因为它定义的内容就是一些材质属性(property),这些属性会在Unity的Inspector面板中展现出来,把材质属性在Inspector面板暴露出来,我们调整起来更加方便!
须知:即使我们不在Properties语义块中定义某些属性,只要在后续的代码段中定义变量,也能使用这些属性变量,因此Properties语义块的作用仅仅是为了让属性可视化在Inspector面板中。
一个Properties语义块格式通常如下:
Properties {
Name ("display name", propertyType) = DefaultValue
Name ("display name", propertyType) = DefaultValue
...
}
其中,
Name——名字,这是我们在Shader中访问它用到的属性名字,通常以一个下划线“_”开头
display name——显示名字,这是显示在Inspector面板中的名字
propertyType——属性类型,这是我们需要指定的类型,下面会举出常见的属性
DefaultValue——默认值,我们需要给每个属性指定默认值
Properties语义块支持的属性类型
属性类型 | 默认值的定义语法 |
Int | number |
Float | number |
Range(min, max) | number |
Color |
(num, num, num, num) |
Vector | (num, num, num, num) |
2D | "defaulttexture" {} |
Cube | "defaulttexture" {} |
3D | "defaulttexture" {} |
其中, 2D、Cube、3D的""内要么是空的,要么是Unity内置的纹理名称(black、white、bump等),至于它们是什么?在之前的HLSL常用函数介绍中,纹理映射函数就已经说过了纹理有1D(几乎不用了)、2D、Cube和3D。如果要指定纹理属性,需要在顶点着色器中编写计算相应纹理坐标的代码。
Shader "Unlit/3.1"
{
Properties
{
//数字和slider
_Int("Int",Int) = 2
_Float("Float",float)=1.5
_Range("Range",Range(0.0,5.0))=3.0
//颜色值和向量
_color("Color",Color)=(1,1,1,1)
_vector("Vector",Vector)=(2,3,6,1)
//纹理信息
_2D ("2D", 2D) = "" {}
_Cube ("Cube", Cube) = "white" {}
_3D ("3D", 3D) = "black" {}
}
FallBack "Diffuse"
}
每个Unity Shader都会至少有一个SubShader语义块。由于不同显卡有不同的渲染能力(很好理解,高级的显卡有更多更复杂的计算能力,而低端一点仅支持部分指令),Unity加载当前Unity Shader时会扫描所有的SubShader语义块,选择一个适合当前平台运行的SubShader语义块。如果都不支持,那么就会使用Fallback语义(后续会提到)指定的Unity Shader。
一个SubShader语义块的格式通常如下:
SubShader {
//可选的
[Tag]
//可选的
[RenderSetup]
Pass {
...
}
//other passes
}
还记得第3节介绍Unity Shader的时候,介绍了它可以让我们更方便地处理渲染设置吗?这就是在SubShader中实现的。ShaderLab提供了一系列渲染状态的设置指令,可以设置显卡的各种状态,是否开启混合模式、是否开启深度测等等,把冗长的代码凝炼成了RenderSetup的指令!
题外话:这里也正好验证了介绍GPU管线概述提到的——逐片元操作我们可进行配置但不是可编程的,这一特性。
还须注意,
ShaderLab常见的渲染状态设置选项
状态名称 | 设置指令 | 解释 |
Cull | Cull Back | Front | Off | 剔除模式设置:剔除背面/正面/关闭 |
ZTest | ZTest Less Great | LEqual | GEqual | Equal | NotEqual | Always | 设置深度测试使用的函数 |
ZWrite | ZWrite On | Off | 开启/关闭深度写入 |
Blend | Blend SrcFactor DstFactor | 开启并设置混合模式 |
Tags是一个键值对(Key/Value Pair),键和值都是字符串类型。目的是告诉Unity引擎,当前的SubShader希望如何以及何时渲染当前对象。
还须注意,SubShader的标签类型和接下来讲的Pass语义块的标签类型不一样,不能互通用~
SubShader的标签格式通常如下:
Tags {"TagNamel" = "Value1" "TagName2" = "Value2"}
SubShader的标签类型
标签类型 | 说明 | |
Queue | 控制渲染顺序,指定渲染队列,保证所有透明物体可以在所有不透明物体后面被渲染,还可以自定义渲染队列 | Tags {"Queue" = "Transparent"} |
RenderType | 对着色器进行分类,例如定义当前着色器是透明or不透明 | Tags {"RenderType" = "Opaque"} |
DisableBatching | 直接指明当前是否对该SubShader进行批处理 | Tags {"DisableBatching" = "True"} |
ForceNoShadowCasting |
控制当前SubShader是否投射阴影 | Tags {"ForceNoShadowCasting" = "True"} |
IgnoreProjector | 为True,则表示使用当前SubShader的问题不受Projection影响,通常用于半透明物体 | Tags {"IgnoreProjector" = "Ture"} |
CanUseSpriteAtlas | 当前SubShader用于精灵sprites时,标签为False | Tags {"CanUseSpriteAtlas" = "False"} |
PreviewType | 指明如何预览该材质,默认情况是球形,可以改成plane、skybox等 | Tags {"PreviewType" = "Plane"} |
定义的这些标签可以在Unity Shader的导入设置面板查看到:
Pass语义块可以说是最重要的部分,几乎整个渲染管线可编程的着色器代码都写在Pass语义块内了,它的格式通常如下:
Pass {
[Name]
[Tags]
[RenderSetup]
//other code
}
可以是:
Name "MyPassName"
还可以使用ShaderLab的UsePass命令——通过访问其名字,直接使用其他Unity Shader中的Pass,这样可以提高代码的复用性,但须注意用UsePass命令时必须使用大写名字,例如:
UsePass "MyShader/MYPASSNAME"
前面说了,Pass的标签不同于SubShader的标签,但目的是一样的,都是告诉Unity如何渲染该物体,Pass的标签:
Pass的标签类型
标签类型 | 说明 | |
LightMode | 定义该Pass在Unity的渲染流水线中的角色 | Tags {"LightMode" = "ForwardBase"} |
RequireOptions | 指定当满足某些条件时才渲染该Pass | Tags {"RequireOptions" = "SoftVegetation"} |
前面SubShader的状态设置同样适用于Pass。
这个其实就是一个保证:万一当前的显卡实在太low了,现有的所有SubShader都无法运行,FallBack语义块就相当于一个最次备选,给他准备一个Fallback内置的最最low的Shader。
格式通常如下:
Fallback "name"
//or
Fallback Off
Fallback不仅有这一个功能,还能影响阴影的投射。渲染阴影纹理时,Unity需要在Unity Shader中寻找一个阴影投射的Pass,而Fallback刚好有内置的这样一个通用的Pass,有了它我们就不需要再单独写一个Pass了,说明正确设置Fallback是多么的重要!
学习渲染管线的时候似乎还没听说过这个着色器。
《入门精要》是这么描述的:它是Unity自创的一种着色器代码类型,本质上和顶点/片元着色器是一样的。总结下来有这两个特点,
他们是老朋友了,在Unity中是用Cg/HLSL语言编写的,灵活性相比表面着色器高很多。这里就不介绍啥代码结构啦,后面当然会再次遇到,到时候再剖析也不迟。
关于“固定”,其实我在PC和手机的主流图形API介绍中就提到了:“OpenGL ES 2.0——引入可编程着色器,弃用了1.x中需对固定功能”,其中的固定功能就是指的这个固定函数着色器,是OpenGL ES 1.1及之前的版本会用的,DX的话是7.0之前,现在只有一些旧设备需要使用啦,所以才会说“被抛弃”。
对于它我们不深入了解,只需要知道有过就行。