unity custom shader 101

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_example
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 能在编辑器中看到如下结果:

shader_properties_example

一般需要美术调节的参数,都可以列举出来,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_path_view

如上图所示当选择了 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


  1. 创建一个 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.y
    8.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 编辑器上设置一下,得到结果

simple_shader_hello_world_result

参考文献

Shader Reference
ShaderLab syntax: Blending
Unity’s Rendering Pipeline
ShaderLab syntax: Pass Tags

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容