Basic
Shaderlab
unity 使用一个叫做 shaderlab 的语言用来包装和组织整个shader结构,我们要为 unity 定制 shader ,也需要书写在 shaderlab 结构之中。shaderlab 结构形如:
shader "name"
{
properties { ... }
subshader { .... }
subshader { .... }
subshader { .... }
fallback "alternative shader name"
}
其中 shader "name" { .... } ,name 不仅作为标识区分别的 shader,同时还指明了 shader 在 unity 编辑器的菜单位置。假如名称中书写了如 shader "Sprites/Default" { .... } 的shader,就会在菜单中按照层级结构出现,如下图所示:
shader "name" { [properties] subshaders [fallback] }
为了构成完整的 shaderlab ,需要书写 1~n个 subshader, 0~1个 properties 段。假如书写了 properties 段,则它必须要在所有 subshader 段之前。
fallback 定义了备选 shader。假如硬件平台不支持 shader 中任何一个 subshader , unity 会使用 fallback 中提供的名字进行查找 shader 作为备选方案渲染,如不需要提供 fallback ,可以把名字替换成off关闭备选方案:fallback off ,当然也是可以不写的。
unity 会根据平台不同将 shaderlab 编译输出成特定 shader ,d3d render->hlsl,opengl->glsl。
实现途径
在 shaderlab 中,unity允许使用三种不同的途径书写 shader ,分别是:
- surface shader:一种书写光照计算函数的简易的方法。只需要关注与光交互的参数并书写一个颜色值的函数。
- vertex & fragment shader:在shaderlab中使用 cg / hlsl / glsl 书写 shader 的方式。 unity 不建议使用此种方式书写与光相关的函数。但实际上移动游戏不得不使用这种方式去书写更有效率的计算方式。也有一些 trick 来获得光照信息。
- fixed function shader:一种类似自然语言的很诡异的语法书写的。忽略掉。
使用 surface shader 可以定制自己的光照模型,它编译后的输出结果是一个被 pixel shader 调用的函数, unity 会在一个通用的 pixel shader 中调用,返回值作为颜色值输出。使用 surface shader 的好处在于不用纠结 unity 会使用哪些 uniform 变量,semantic 字段,这也是 unity 推荐的定制与光交互的 shader 写法,缺点是可控性不高。
我没有使用过glsl,手册上说仅支持debug使用,也没有验证过,因此后文会根据 vertex & fragment shader (CG) 作为主要的书写方式。一般实现 shader 会在 subshader 段中的 pass 段内书写 cg shader ,并用 CGPROGAM/ENDCG 括起来,形如:
subshader
{
....
pass
{
CGPROGRAM
struct vsout { .... }
vsoutut vertex_shader ( in input ) { .... }
float4 pixel_shader ( in vsoutut ) { .... }
ENDCG
}
....
}
当然代码也可以写在 pass 之外。经过自己测试,代码既可以写在 shaderlab 之内,也可以写在结构体外部。因为使用多个 subshader 或者使用多 pass 的时候,会有可能复用到一些代码,所以我更愿意把 cg 写在 pass 之外。 unity built-in shader 往往把代码安排在 properties 标签和第一个 subshader 标签之间。一般我把它放在 shader "name" { .... } 结构后头。代码写在 pass 外部需要用 CGINCLUDE/ENDCG 来标明:
shader "my_shader/texture"
{
properties
{
_MainTex("基贴图", 2D) = "black" { }
}
subshader
{
tags { "RenderType" = "Opaque" }
lighting off
pass
{
name "texture pass"
tags { "LightMode" = "Vertex" }
CGPROGRAM
#pragma vertex vertex_shader
#pragma fragment pixel_shader
ENDCG
}
}
fallback off
}
CGINCLUDE
#include "UnityCG.cginc"
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
struct v2p
{
float4 position : SV_POSITION;
half2 texcoord : TEXCOORD0;
};
v2p vertex_shader (
in float4 position : POSITION,
in float4 texcoord : TEXCOORD0)
{
v2p o;
o.position = mul(UNITY_MATRIX_MVP, position);
o.texcoord = TRANSFORM_TEX(texcoord, _MainTex).xy;
return o;
}
fixed4 pixel_shader (in v2p p) : COLOR
{
return tex2D(_MainTex, p.texcoord);
}
ENDCG
根据以上shader,在下文中逐步说明每个段落细节。
properties
properties 段定义了 shader 中需要用到的参数。他是连接 cg shader 参数和 unity 参数编辑器的桥梁,如果cg shader不需要外部的输入参数,可以不写。常用的词条列举如下:
properties
{
_MainTex ("基贴图", 2D) = "black" { option }
_Cubemap ("环境图", CUBE) = "bump" { option }
_Color ("颜色", Color) = (1,1,1,1)
_Vector ("向量", Vector) = (0,0,0,0)
_Float ("数值", Float) = 0.5
_FloatRange ("数值范围", Range(0.1,1.0)) = 0.5
}
properties 中书写的词条,能在 unity material editor UI 生成编辑界面对象,用户可以直接在界面上调节参数,保存在 material中。以上的 properties 能在编辑器中看到如下结果:
一般需要美术调节的参数,都可以列举出来,unity 会把这些值保存在每个 material 里。此外, unity 会根据 property 词条名称匹配 cg shader 中的 uniform 变量,并使用用户设置好的 material 的参数对变量进行赋值。因此可以一般的,uniform变量会同properties词条一同出现:
properties
{
_MainTex ("基贴图", 2D) = "black" { option }
}
....
CGINCLUDE
uniform sampler2D _MainTex;
....
如果某些 uniform参数需要使用脚本动态设置 (如屏幕特效使用的 shader),可以不用在 properties 中书写词条。
subshader
每个单独的 subshader 将构成一个完整的渲染效果, 当有多个 subshader 依次出现时,unity 只会选择其中一个执行, unity 根据既定顺序从最靠前的 subshader 尝试匹配平台,直到找出第一个成功匹配平台的 subshader 使用。如果完全没有匹配的 subshader,渲染失败。
subshader { [tags] [common state] pass [pass....] }
一个 subshader 中一般包含 0~1个 tags 定义,0n个通用参数设置,1n个 pass 。如下所示:
subshader {
tags { .... }
//common state settings etc.
light on
zwrite on
pass { .... }
pass { .... }
pass { .... }
}
tags
tags 中根据词条定义了整个 subshader 的渲染先后、队列位置等。一般 subshader tags 都会设置"RenderType"
和 "Queue"
。形式如下:
tags
{
"RenderType" = "Opaque"
"Queue" = "Geometry"
"OtherTagName" = "OtherTagValue"
}
下面说说常用的 tag 和 tags的取值。
-
"RenderType"
:此 tag 会把不同的 shader 组织到几个预定义组里。组主要用来指导 camera 选取对应的 replacement shader ,作为渲染深度图的重要参考。通过书写设置 replacement shader 可以让我们拥有定义自定义"RenderType"
的能力,但是一般使用 built-in 的就够用了。built-in的常用取值如下(略有忽略):
值 | 说明 |
---|---|
"Opaque" |
各种带法线的、自发光的、反光的不反光的、地形等等 |
"Transparent" |
大部分的透明物体、粒子、字体、地形的叠加效果等 |
"TransparentCutout" |
半透明需要clip的对象,第二层要反着画vegetation的那种 |
"Background" |
天空盒子背景什么的 |
"Overlay" |
GUI图片, 光环, 镜头光晕 |
-
"Queue"
:队列用来定义物体渲染的先后顺序,主要保证渲染半透明物体在不透明物体之后才渲染,当然也有可能因为UI重叠关系,通过代码设置Queue取值。预定值如下:
值 | 常量 | 说明 |
---|---|---|
"Background" |
1000 | 必须最先画,天空盒子什么的 |
"Geometry" |
2000 | (默认的)几何图形 |
"AlphaTest" |
2450 | 顾名思义,有这个Queue是因为保证半透明要实体在之后了 |
"Transparent" |
3000 | 半透明效果从后到前的,玻璃、粒子什么丢这儿 |
"Overlay" |
4000 | 最后的画的(镜头lens flare效果)放这里,应该是image effect之前的东东。 |
不透明的物体一般都应该使用"Geometry"
。这个队列是经过最佳优化的(应该是ealry-z)。而其他队列的物体会通过深度排序,从最远的渲染到最近。当有特殊用途的时候可以自己规定一些特殊值:
Tags { "Queue" = "Geometry+1" }
这样就能保证使用了这个 subshader 渲染的物体,在使用 Geometry 渲染的物体之后,但是又不会超过半透明的物体。就是能覆盖 Geometry 的物体,但是会被半透明盖住。值得注意的是 "Queue"
这个值只能在 subshader 的 tags 设置里。
"IgnoreProjector"
:"True"
/"False"
主要是设置给半透明物体不会受到 Projector 影响,似乎 Projector 是生成圆片片影子用的。"ForceNoShadowCasting"
:"True"
/"False"
本段 subshader 不会产生阴影,惭愧没试验过。
pass
pass 是定义了单次渲染的 shader,定义和 subshader 类似。
pass { [name and tags] [render setup] CGPROGRAM ... ENDCG }
一般包括01个名称,01个tags段,0~n个渲染状态设置,1个 vertex shader 和1个 fragment(pixel) shader ,形如:
pass
{
name "name"
tags { .... }
state settings
CGPROGRAM
#pragma vertex vertex_shader
#pragma fragment pixel_shader
ENDCG
}
"name"
标签主要是要和别的shader进行 use/grab pass 进行交互,如果不需要的use/grab功能,填上就当注释用了。
vertex/fragment shader 需要使用 CGPROGRAM/ENDCG 包括起来,其中 vertex_shader / pixel_shader 是自定义的 shader function 名称。
常用state setting的列表见表格
值 | 说明 |
---|---|
lighting on/off |
设置光照 |
cull back/front/off |
剔除参数 |
ztest cmp_key |
深度测试 |
zwrite on/off |
深度写入 |
alphatest cmp_key cutoff_value |
半透明测试 |
blend src dst |
混合模式 |
colormask rgb/a/0/...) |
颜色遮罩 |
- cmp_key = (less/greater/lequal/gequal/equal/notequal/always)其中之一。
- cutoff_value 是一个浮点数
- colormask 设为0关闭所有写入通道
- blend 请参考ShaderLab syntax: Blending
注意! 并不是在一个 subshader 中 有多少个 pass 就会渲染几次。而是 unity 会根据 rendering path 选取合适的 pass 渲染,选择的因素在于 "lightmode"
取值。 pass tags 只有2个参数能设置,而最重要的就是 "lightmode"
。具体细节请阅读第二章 Rendering Path了解。
Rending Path
“书写的 shader 并不一定会被 unity使用”,这就是为什么在101书写第二章的原因。在为 unity 定制 shader 之前,需要了解 unity rendering path 的内容,rendering path 有三分别如下:
- vertex-lit 顶点光照,往后都用 vertex 代替。
- forward rendering 前向渲染,后使用 foward 代替。
- deferred lighting 延迟光照,后使用 deferred 代替
对deferred不熟悉,据说移动平台上切换 render target 速度较慢,根据实际测试,unity在切换的时候仍然不是太理想,所以针对手机定制效果的话不推荐推荐deferred。也不是非说不能用延迟,朋友说他在手机上已经写好deferred框架了,速度哇哇的。不过接下来仍以 vertex 和 forward 渲染作为基础说明。
rendering path可以在菜单 edit->project->player setting 下被设置,但实际上我们会通过在 shader 中提供有限的 subshader ,仅仅支持特定的 path。我们可以在 scene 中看到效果以验证物体是在哪个 path 中渲染的。编辑器 scene 窗口的左上角的工具栏进行设置,使用不同rendering path 的对象会以不同颜色标明。
如上图所示当选择了 rendering paths 之后,场景上的物体会根据以下表格显示出不同的颜色。
vertex |
forward |
deferred |
---|---|---|
红色 | 黄色 | 绿色 |
一般的,设置会使用 forward 路径,如果没有合适的 pass,shader会自动降成 vertex-lit 的路径。更多细节可以参照 Unity’s Rendering Pipeline
Light Mode
本质上和 rendering path 有关系就是 pass tag lightmode 参数。定制 shader 只可能是提供某些适应 rendering path 的 pass,选用哪个 pass 则由 unity 根据 player setting 决定。
Vertex-Lit
vertex 可以访问到多盏光源的信息,加上只进行单次的渲染特性,完全当做渲染速度最快的方式进行,让多个光照在一个pass渲染内完成计算,以保证效率。
当材质预期使用单个pass渲染物体的时候,可以将 "lightmode"
设置为以下三个值其一。请不要在使用 Vertex 之后,又使用 forward 相关的标签(具体见下文),不然 rendering path 会优先使用更高级的 path。导致 vertex pass 没有被unity使用。
取值 | 意义 |
---|---|
"Vertex" |
没有接受到光照图影响的物体,这是我现在用的设置 |
"VertexLMRGBM" |
受到Lightmap影响的物体,经过RGBM压缩 |
"VertexLM" |
受到Lightmap影响的物体,经过LDR压缩 |
Forward
forward 会对单个物体进行多次渲染,渲染次数在1-n(2)不等,n可以在菜单 edit-project-quality setting 里的 pixel light 标签找到,渲染次数主要取决于物体受到的光的数量。因此要实现 forward ,需要同时实现 "ForwardBase"
和 "ForwardAdd"
。
使用 forward 的原因在于 target2.0 64条指令限制了 vertex 不可能超过4。其次 forward 路径下,辅光源会通过 alpha blending 叠加,效果相对柔和。
取值 | 意义 |
---|---|
"ForwardBase" |
计算主光源(和SH光照),一般为平行光 |
"ForwardAdd" |
通过叠加的方式将其他的的光绘制,经过测试点光源和探照灯只可能使用此pass |
我现在最常用的为 "Vertex"
"ForwardBase"
"ForwardAdd"
3个标签。除此之外 "Alway"
则会在每次渲染的时候都会被使用到,因为能力有限,没找到访问的到该流程下的光照信息的方案,暂时不知道有啥用,具体的 "lightmode"
设置可以参照unity的指南:ShaderLab syntax: Pass Tags。
书写一个 shader
- 创建一个 mesh。
创建一个 material。
创建一个 shader 并打开,你会看到一个shaderlab 框架,不需要它。删掉并书写为如下格式:
Shader "x/hello_world"
{
SubShader
{
Pass
{
CGPROGRAM
ENDCG
}
}
}选择mesh,在
mesh renderer
组件上,把 material[0] 设置为新创建的 material。选择 meterial,在
shader
标签中,设置为我们刚创建的 x/hello_world。这时你已经把 mesh-material-shader 联系在一起了。接下来要做的就是书写 cg 代码。
-
确定 shader 使用的 rendering path,我们选择 vertex。把框架搭好:
Shader "x/hello_world" { SubShader { Pass { tags { "lightmode" = "vertex" } CGPROGRAM #pragma vertex vertex_shader #pragma fragment pixel_shader ENDCG } } } CGINCLUDE struct vs_out { }; vs_out vertex_shader( ) { } fixed4 pixel_shader(in vs_out i) { } ENDCG
书写 vertex shader。
CGINCLUDE
struct vs_in
{
float4 position : POSITION;
float4 texcoord : TEXCOORD0;
};
struct vs_out
{
float4 position : POSITION;
float2 texcoord : TEXCOORD0;
};
vs_out vertex_shader(vs_in i)
{
vs_out o;
o.position = mul(UNITY_MATRIX_MVP, i.position);
o.texcoord = i.texcoord.xy;
return o;
}
ENDCG书写 fragment shader
properties
{
_MainTex("基贴图", 2D) = "white" { }
}
....
CGINCLUDE
#include "unityCG.cginc"
uniform sampler2D _MainTex;
....
fixed4 pixel_shader(in vs_out i) : COLOR
{
bool p = fmod(i.texcoord.x8.0,2.0) < 1.0;
bool q = fmod(i.texcoord.y8.0,2.0) > 1.0;
fixed grid_color = ((p && q) || !(p || q)) ? 1.0 : 0.5;
return fixed4(grid_color,grid_color,grid_color,1.0) * tex2D(_MainTex, i.texcoord);
}
....
ENDCG找张贴图,在 material 编辑器上设置一下,得到结果
参考文献
Shader Reference
ShaderLab syntax: Blending
Unity’s Rendering Pipeline
ShaderLab syntax: Pass Tags