一、Unity Shader概述
1.材质与unity shader
unityshader定义了渲染所需要的各种代码(如顶点着色器和片元着色器)、属性(如使用了那些纹理等)和指令(渲染和标签设置等)
而材质则允许我们调节这些属性,并将其最终赋给相应的模型
2.Unity中的材质
Unity中的材质需要结合GameObject的Mesh或者粒子系统(Particle Systems)组件来工作
默认情况下,一个新建的材质使用unity内置的Shandard Shader,这是一种基于物理渲染的着色器
3.Unity中的Shader
1.Unity Shader模板
Standard Surface Shader | 产生一个包含了标准光照模型的表面着色器 |
Unlit Shader | 产生一种不包含光照但包含雾效的几百本的顶带你片元着色器 |
Image Effect Shader | 实现各种屏幕后处理效果的基本模板 |
Compute shader | 利用GPU的并行来进行一些与常规渲染流水线无关的计算 |
2.UnityShader的导入设置
Default Maps:指定该Unity Shader使用的默认纹理
当任何材质第一次使用该Unity Shader时,这些纹理就会自动被赋予到相应的属性上。
该Unity Shader的相关信息,如是否是一个表面着色器(Surface Shader)、是否是一个固定函数着色器(Fixed function)、是否会有投影、使用的渲染队列、LOD值、使用的渲染队列(Render queue)、是否关闭通道批处理(Disable batching)、属性列表(Properties)等信息
Compile and show code下拉列表可以检查该shader针对不同图像编程接口最终编译成的Shader代码
二、Unity Shader的基础:ShaderLab
1.什么是ShaderLab?
Unity Shader是Unity为开发者提供的高层级渲染抽象层
所有的Unity Shader都是使用ShaderLab来编写的,ShaderLab是Unity提供的编写Unity Shader的一种说明性语言。使用一些嵌套在花括号内部的语义(syntax)来描述一个Unity Shader文件的结构。
ShaderLab类似于CgFX和Direct3D Effects (.FX)语言,都定义了要显示一个材质所需要的所有东西,不仅仅是着色器代码
2.一个Unity Shader的基础结构
Shader "ShaderName" {
Properties {
//属性
}
SubShader {
//显卡A使用的子着色器
}
SubShader {
//显卡B使用的子着色器
}
Fallback "vertexLit"
}
Unity在背后会根据使用的平台来把这些结构编译成真正的代码和Shader文件
三、Unity Shader的结构
1.给Shader命名
Shader "Shader/MyShader"
2.Properties
Properties语义块中包含了一系列属性(property),这些属性将会出现在材质面板中。
Properties的语义块支持的属性类型:
Int、Float、Range默认值就是一个单独的数字
Color、Vector默认值为用圆括号包围的四维向量
2D、Cube、3D字符串加花括号(字符串可空)
3.SubShader
每一个Unity Shader文件可以包含多个SubShader语义块,但至少要有一个
当Unity需要加载这个Unity Shader时,Unity会扫描所有SubShader语义块,然后选择第一个能在目标平台上运行的SubShader
如果都不支持的话,Unity就会使用Fallback语义定义的Unity Shader
使用SubShader的原因:不同的显卡具有不同的能力。例如,一些旧的显卡仅能支持一定数目的操作指令,而一些更高级的显卡就可以支持更多的指令数,那么我们就希望在旧的显卡上使用计算复杂度较低的着色器,而在高级的显卡上使用计算复杂度较高的着色器,以便提供更出色的画面
SubShader语义块在包含的定义如下:
SubShader
{
//可选的
[Tags]
//可选的
[RenderSetup]
Pass
{
}
//Other Passes
}
SubShader中定义了一系列Pass以及可选的状态[RenderSetup]和标签[Tags]设置
每个Pass定义了一次完整的渲染流程
Pass数目过多会造成性能下降,所以要使用尽量少的Pass
状态和标签同样可以在Pass中声明
但SubShader中的一些标签是特定的
SubShader中进行的设置会应用于所有Pass
状态设置
常见渲染状态设置选项
在SubShader中设置渲染状态会应用到所有Pass
如果不想这样,希望只应用于某一个pass,那么就在该pass中单独设置
SubShader标签
SubShader的标签(Tags)是一个键值对,键和值都是字符串类型
这些键值对是SubShader和渲染引擎之间的沟通桥梁,用来告诉Unity的渲染引擎:SubShader希望怎样以及何时渲染这个对象
标签结构:
Tags { "TagName1"="Value1" "TagName2"="Value2" }
SubShader的标签块支持的标签类型如下:
注意:
上述标签只可在SubShader中声明,而不可以在Pass中声明。
Pass块也可以定义标签,但这些标签是不同于SubShader的标签类型的
Pass语义块
Pass
{
[Name]
[Tags]
[RenderSetUp]
//Other code
}
1.我们可以在Pass中定义该Pass的名称,例如:
Name "MyPassName"
通过这个名称,我们可以使用SubShader的UsePass命令来直接使用其他UnityShader中的Pass例如:
UsePass "MyShader/MYPASSNAME"
好处是提高代码复用性
注意是Unity内部会把所有Pass的名称转换成大写字母的表示,在使用UsePass时必须使用形式的名字
2.可以对Pass设置显然状态,SubShader的状态同样适用于Pass。
处理上面提到的状态设置外,Pass还可以使用固定管线着色器命令
Pass的标签不同于SubShader的标签,这些标签也是用于告诉渲染引擎我们希望怎样来渲染该物体
Pass中使用的标签类型
一些特殊Pass:
- UsePass:使用其他Unity中的Pass
- GrabPass:复杂抓取屏幕并将结果存储在一张纹理上
4.Fallback
如果上述所有SubShader在这块显卡上都不能运行,就使用这个最低级的shader吧
Fallback "name"
//或者
Fallback off
你也可以关闭Fallback功能
Fallback off
即如果所有SubShader都不能在这块显卡上运行,就不要管他了
Fallback "VertexLit"
Fallback会影响阴影投射。在渲染阴影纹理时,Unity会在每一个UnityShader中寻找一个阴影投射的Pass
通常情况下,我们不需要自己专门实现一个Pass,因为Fallback使用的内置Shader中包含了这样一个通用Pass
为每个Unity shader正确设置Fallback很重要
CustomEditor语义可以扩展编辑器界面
ShaderLab:指定自定义编辑器 - Unity 手册 (unity3d.com)
Category语义可以对命令进行分组
ShaderLab:使用 Category 代码块对命令进行分组 - Unity 手册 (unity3d.com)
四、Unity Shader的形式
Shader "MyShader"
{
Properties
{
//所需的各种属性
}
SubShader
{
//真正意义上的Shader代码会出现在这里
//表面着色器或者顶点/片元着色器或者固定函数着色器
}
SubShader
{
//和上一个SubShader类似
}
}
1.表面着色器
表面着色器(Surface Shader):
- Unity自创的一种着色器代码类型
- 需要代码量很少
- 渲染代价很大
- unity在背后做到工作很多
表面着色器示例:
2.顶点/片元着色器
- 需要自己定义每个Pass
- 需要写更多代码
- 灵活性高
- 可以控制渲染的实现细节
- 用CG/HLSL编写
3.固定函数着色器
- 旧设备使用
- 只可以完成一些简单的效果
- 代码被定义在Pass中,相当于Pass的一些渲染设置
- 只能完全使用ShadeLab,不能用CG/HLSL
- 已被抛弃的
4.如何一个选择Unity Shader形式
- 除非有非常明确需求,否则不适应固定函数着色器
- 和各种光源打交道用表面着色器,但小心移动平台
- 光照数目少用顶点或片元着色器
- 有很多自定义渲染效果,用顶点或片元着色器
五、Unity Shader !=真正的Shader
Unity Shader实际上指定时一个ShaderLab文件,硬盘上以.shader作为文件后缀的一种文件
优点:
- 可以在同一个文件里同时包含所需要的顶点着色器和片元着色器代码
- 可以进行一些如开启混合、深度测试等渲染设置
- 只需要在特定语义块在声明一些属性,就可以依靠材质来方便的改变这些属性
缺点:
高度封装,可以被编写的Shader类型和语法被限制
六、Unity Shader和CG/HLSL之间的关系
Unity Shader使用ShaderLab编写的,但也可以在ShaderLab中嵌套CG/HLSL代码
CG/HLSL时嵌套在CGPROGRAM和ENDCG之间的
CG代码位于Pass语义内部:
表面着色器本质上也是顶点/片元着色器
七、GLSL
可以发布的平台只有Mac OS X、OpenGL ES 2.0或者Linux,而对于PC、Xbox360这样仅支持DirectX的平台就得放弃
GLSL的代码也要写在CGPROGRAM和ENDCG之间
八、顶点/片元着色器
1.顶点/片元着色器的基本结构
#pragma vertex vert
#pragma fragment frag
告诉unity,哪个函数包含了顶点着色器代码,哪个函数包含了片元着色器代码,通用指令如下:
#pragma vertex name
#pragma fragment name
name指函数名,这两个函数名不一定是vert和frag,可以是我们自定义的任意合法函数名
vert函数的定义:
float vert (float4 v : POSITION) : SV_POSITION
{
return mul (UNITY_MATRIX_MVP, v);
}
vert函数输入的v包含了这个顶点的位置,这是通过POSITION语义指定的。它的返回值是一个float4类型的变量,它是顶点在裁剪空间的位置,POSITION和SV_POSITION都是CG/HLSL中的语义,是不可省略的。
这些语义告诉系统用户需要输入哪些值以及用户的输出是什么。
例如上述:
- POSITION告诉unity把模型的顶点坐标填充到输入参数v中
- SV_POSITION告诉unity顶点着色器的输出是裁剪空间中的顶点坐标
如果没有上述语义限制输入参数,渲染器完全不知道用户输入输出是什么,就会出现错误效果
UNITY_MATRIX_MVP矩阵是unity内置的模型、观察、投影矩阵,将物体的局部空间左边转换到屏幕裁剪空间坐标
frag函数的定义:
fixed4 frag () : SV_Target
{
return fixed4 (1.0, 1.0, 1.0, 1.0);
}
frag没有任何输入,它的输出是一个fixed4类型的变量,并且使用SV_Target语义进行限定
SV_Target是HLSL中的系统语义,告诉渲染器,用户输出颜色储存的到一个渲染目标(render target )中,这里将输出到默认的帧缓存中。
片元着色器输出的颜色的每个分量范围在[0,1],其中(0,0,0)表示黑色;(1,1,1)表示白色
2.模型数据从哪里来
在顶点着色器中,使用POSITION获得模型的顶点位置,我们想要得到更多模型数据怎么办?
要得到模型上每个顶点的纹理坐标和法线方向,就要使用纹理坐标来访问纹理,而法线可用于计算光照。需要修改顶点着色器定义一个新的输入参数,该参数不再是一个简单的数据类型,而是一个结构体
声明了一个新的结构体a2v,包含顶点着色器需要的模型数据, 获得了unity支持的语义:POSITION、NORMAL、TANGENT、TEXCOOR0、TEXCOOR1、TEXCOOR2、TEXCOOR3、COLOR等。它们作为顶点着色器的输入时都有特定的含义,unity根据这些语义来填充这个结构体
自定义结构体的格式:
struct StruncName
{
Type Name : Semantci;
Type Name : Semantci;
......
};
该语义是不可被省略的
我们要修改vert函数的输入参数类型,,把它设置为我们新定义的结构体a2f,通过这种自定义结构体的方式,我们可以在顶点着色器中访问模型数据
a2v名字的含义:
a表示应用,v表示顶点着色器,a2v即把数据从应用阶段传递到顶点着色器中
POSITION、NORMAL、TANGENT这些语义中的数据究竟从哪来?
在Unity中,它们是由该材质的Mesh Render组件中提供的。每帧调用Draw Call的时候,Mesh Render组件会把它负责的模型数据发给Unity Shader
一个模型通常包含一组三角形面片,每个三角形由3个顶点构成,每个顶点又包含一些数据,例如:顶点位置、法线、切线、纹理坐标、顶点颜色等
通过上述方法,可以在顶点着色器中访问顶点的这些模型数据
3.顶点着色器与片元着色器之间如何通信
从顶点着色器输入一些数据,如:模型的法线、纹理坐标等传递给片元着色器
再定义一个新的结构体
v2f用于在顶点着色器和片元着色器之间传递信息,v2f也需要指定每个变量的语义
用SV_POSITION和COLOR0语义
顶点着色器的输出结构中,必须包含一个变量,语义是SV_POSITION,否则渲染器就无法得到裁剪空间中的顶点坐标,就无法把顶点渲染到屏幕上。
COLOR0中的数据可以由用户自行定义,一般都存储颜色,例如:逐顶点的漫反射颜色或逐顶点的高光反射颜色
4.如何使用属性
要在材质面板上显示一个拾色器,可以直接控制模型在屏幕上显示颜色
ShaderLab中属性的类型和CG中变量的类型之间的匹配关系
uniform fixed _Color;
uniform关键词是CG中修饰变量和参数的一种修饰词,仅仅用于提供一些关于该变量的初始值是如何指定和存储的相关信息,可省略
九、Unity提供的内置文件和变量
1.内置的包含文件
包含文件(include file),类似于c++中头文件的一种文件,在Unity中,它们的文件后缀是.cginc
在编写Shader时,我们可以使用#include指令把这些文件包含进来,这样我们就可以使用unity提供的一些变量和帮助函数
内置着色器 include 文件 - Unity 手册 (unity3d.com)
2.内置的变量
与光照有关的内置变量:Lighting.cginc、AutoLight.cginc
unity还提供了一些访问时间、光照、雾效、环境光等目的的变量
十、unity提供的CG/HLSL语义
1.什么是语义
Direct3D 12 编程指南 - Win32 apps | Microsoft Learn
语义实际上是一个赋给Shader输入和输出的字符串,这个字符串表达了这个参数的含义
这些语义可以让shader知道从哪里读取数据,并把数据输出到哪里,它们在CG/HLSL的Shader流水线中是不可缺少的
系统数值语义:以SV开头,SV代表系统数值
在大部分平台上POSITION和SV_POSITION语义等价,但在某些平台(如索尼的PS4)上必须使用SV_POSITION,否则无法让Shader好好工作
为了跨平台性,我们都要SV开头
2.unity支持的语义
TEXCOORDn,n等于8或16
我们要把自定义的数据从顶点着色器传递给片元着色器,一般选用TEXCOORD0等
3.如何定义复杂的变量类型
注意:
一个语义可以使用的寄存器只能处理4个浮点值(float),若要定义矩阵类型,如float3x4、float4x4等变量,就需要使用更多的空间
一种方法是,把这些变量拆分成很多变量,如float4x4的矩阵类型,可以拆分成4个float4类型的变量,每个变量存储了矩阵中的一行数据
十一、Debug
假色彩图像
假色彩图像是指用加色彩技术生成的一种图像。与加色彩图像对应的是照片这种真色彩图像
一张加色彩图像可以用于可视化一些数据
主要思想:
把需要调试的变量映射到[0,1]之间,把它们作为颜色输出到屏幕上,然后通过屏幕上显示的颜色来判断这个值是否正确
缺点:得到的调试信息很模糊,信息很有限
Visual Studio
帧调试器
十二、渲染平台差异
1.坐标差异
2.语法差异
3.shader语义差异
十三、Shader简洁之道