acecase 发表于 2022-9-5 09:06

Unity Shader基础

本文主要参考《Unity Shader入门精要》第3章内容。
1、Unity中的Shader

1.1 材质(Material)和Unity Shader



1.2 Unity中的Shader类型

在Unity 5.2及以上版本中,一共提供4种Unity Shader模板,Standard Surface Shader, Unlit Shader, Image EffectShader 以及 Compute Shader。
Standard Surface Shader 会产生1个包含了标准光照模型(使用了 Unity 5中新添加的基于物理的渲染方法,详见第18章)的表面着色器模板。
Unlit Shader则会产生一个不包含光照(但包含雾效)的基本的顶点/片元着色器。后面两个着色器暂不叙述。
新建了一个Shader后,在Compile and showcode下拉列表可以让开发者检查该Unity Shader针对不同图像编程接口(例如OpenGL、D3D9D3Dll等)最终编译成的Shader代码,直接单击该按钮可以查看生成的底层的汇编指令。




2. Unity Shader基础

2.1 ShaderLab

“计算机科学中的任何问题都可以通过增加一层抽象来解决。”一大卫 惠勒
Unity Shader 是 Unity 为开发者提供的高层级的渲染抽象层。


Unity 中,所有的 Unity Shader 都是使用 ShaderLab 来编写的,本质上是一种文本文件。
一个Unity Shader 基础结构如下所示:
Shader "ShaderName " {
   Properties { //属性
   }
   SubShader { //显卡A使用的子着色器
   }
   SubShader { //显卡B使用的子着色器
   }
   Fallback "VertexLit"
}
Unity 在背后会根据使用的平台来把这些结构编译成真正的代码和 Shader 文件,而开发者只需要和 Unity Shader 打交道即可。
2.2 Unity Shader的结构

2.2.1 给Shader起个名字
Shader "Custom/MyShader " {   }那么这个 Unity Shader 在材质面板中的位置就是 Shader-> Custom-> MyShader


2.2.2 属性(Properties)
Properties {
   Name ("display name" , PropertyType) = DefaultValue
   Name ("display name", PropertyType) = DefaultValue //更多属性
}如果我们需要在 Shader 中访问材质的属性,就需要使用每个属性的名字 (Name) 。在 Unity 中,这些属性的名字通常由1个下划线开始。
显示的名称 (display name) 则是出现在材质面板上的名字。
我们需要为每个属性指定它的类型 (PropertyType) 常见的属性类型如下图所示。
我们还需要为每个属性指定一个默认值,在我们第一次把该 Unity Shader 赋给某个材质时,材质面板上显示的就 是这些默认值。


对于 2D、Cube、3D 这几种类型,默认值的定义稍微复杂,它们的默认值是通过一个字符串后跟一个花括号来指定的,其中,字符串要么是空的,要么是内置的纹理名称,如 "white" "black" "gray" 或者 "bump" 。
下面的代码给出了一个展示所有属性类型的例子以及在材质上显示的结果:




2.2.3 重量级成员SubShader
每一个UnityShader文件可以包含多个SubShader语义块,但最少要有一个。
当Unity需要加载这个Unity Shader时,Unity会扫描所有的SubShader语义块, 然后选择第一个能够在目标平台 上运行的SubShader。
如果都不支持的话, Unity就会使用Fallback语义指定的Unity Shader。
Unity提供这种语义的原因在于,不同的显卡具有不同的能力。 例如, 一些旧的显卡仅能支持一定数目的操作指令, 而一些更高级的显卡可以支持更多的指令数, 那么我们希望在旧的显卡上使用计算复杂度较低的着色器, 而在高级的显卡上使用计算复杂度较高的着色器, 以便提供更出色的画面。
SubShader语义块中包含的定义通常如下:
SubShader {
//可选的标签
   

//可选的状态
   [RenderSetup)

   Pass {
   }
   // Other Passes
}

[*]每个 Pass定义了一次完整的渲染流程, 但如果Pass的数目过多, 往往会造成渲染性能的下降。 因此, 我们应尽量使用最小数目的Pass。
[*]状态和标签同样可以在Pass声明。不同的是,SubShader中的一些标签设置是特定的。 也就是说,这些标签设置和Pass中使用的标签是不一样的。
[*]对于状态设置来说,其使用的语法是相同的。 但是, 如果我们在SubShader进行了这些设置, 那么将会用于所有的Pass。
标签的结构如下:
Tags { " TagNamel " = "Valuel " "TagName2" = "Value2 " }Pass的结构如下:
Pass{
   
   
   
    //Other code
}我们可以在 Pass 中定义该 Pass 名称,例如:
Name "MyPassName "通过这个名称,我们可以使用 ShaderLab的UsePass 指令来直接使用其他 Unity Shader 中的Pass 。例如:
UsePass "MyShader/MYPASSNAME "这样可以提高代码的复用性。需要注意的是,由于 Unity 内部会把所有 Pass 的名称转换成大写字母的表示,因此 在使用 UsePass 命令时必须使用大写形式的名字。
2.2.4 留一条后路:Fallback
Fallback "name"
// 或者
Fallback off它用于告诉 Unity, "如果上面所有的 SubShader 在这块显卡上都不能运行,那么就使用这个最低级的 Shader 吧!”

3. Unity Shader的形式

Shader "MyShader" {
   Properties {
   //所需的各种属性
   }
   SubShader {
   //真正意义上的 Shader 代码会出现在这里
   //表面着色器 (Surface Shader) 或者
   //顶点/片元着色器 (Vertex/Fragment Shader) 或者
   //固定函数着色器 (Fixed Function Shader)
   SubShader {
   //和上—个 SubShader 类似
   }
}3.1 Unity的宠儿:表面着色器

表面着色器 (Surface Shader) 是Unity 自己创造的一种着色器代码类型。它需要的代码量很少,Unity 在背后做了很多工作,但渲染的代价比较大。
当给 Unity 提供一个表面着色器的时候,它在背后仍旧把它转换成对应的顶点/片元着色器。也就是说,表面着色器是 Unity 对顶点/片元着色器的更高一层的抽象。它存在的价值在于,Unity 为我们处理了很多光照细节,使得我们不需要再操心这些“烦人的事情”。
一个非常简单的表面着色器示例代码如下:
Shader "Custom/Simple Surface Shader" {
   SubShader {
      Tags { "RenderType " = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input{
         float4 color : COLOR;
      };
      void surf (Input IN, inout SurfaceOutput o) {
         o.Albedo = l;
      }
      ENDCG
   }
   Fallback "Diffuse"
}
表面着色器被定义在 SubShader 语义块(而非 Pass 语义块)中的CGPROGRAM和ENDCG 之间 。原因是,表面着色器不需要开发者关心使用多少个 Pass 、每个Pass 如何渲染等问题, Unity会在背后为我们做好这些事情。
3.2 最聪明的孩子:顶点/片元着色器

一个非常简单的顶点/片元着色器示例代码如下:
Shader "Custom/Simple VertexFragment Shader" {
   SubShader {
      Pass {
         CGPROGRAM
         #pragma vertex vert
         #pragma fragment frag
         float4 vert(float4 v : POSITION) : SV_POSITION {
            return mul (UNITY_MATRIX_MVP, v);
         }
         fixed4 frag () : SV_Target {
            return fixed4 (1.0, 0.0, 0.0, 1.0);
         }
         ENDCG
      }
   }
}

[*]和表面着色器类似,顶点/片元着色器的代码也需要定义在 CGPROGRAM和 ENDCG 之间。
[*]不同的是顶点/片元着色器是写在 Pass 语义块内,而非 SubShader 内的。原因是我们需要自己定义每个 Pass 需要使用的 Shader 代码。
[*]虽然我们可能需要编写更多的代码,但带来的好处是灵活性很高。更重要的是, 我们可以控制渲染的实现细节。
3.3 选择哪种Unity Shader形式

这里给出了 一些建议:

[*]除非你有非常明确的需求必须要使用固定函数着色器,例如需要在非常旧的设备上运行你的游戏(这些设备非常少见),否则请使用可编程管线的着色器,即表面着色器或顶点/ 片元着色器。
[*]如果你想和各种光源打交道,你可能更喜欢使用表面着色器,但需要小心它在移动平台的性能表现。
[*]如果你需要使用的光照数目非常少,例如只有1个平行光,那么使用顶点/片元着色器是一个更好的选择。
[*]最重要的是,如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。
Reference
《Unity Shader 入门精要》— 冯乐乐
页: [1]
查看完整版本: Unity Shader基础