Section02 Unity Shader

发布于 2022-11-09  260 次阅读


概述

材质和Unity Shader

在Unity 中我 需要配合使用 材质 (Material) 和 Unity Shader 能达到需要的效果。一个最常见的流程是

  1. 创建一个材质;
  2. 创建一 Unity Shader, 并把它赋给上一步中创建的材质
  3. 把材质赋给要渲染的对象;
  4. 在材质面板中调整 Unity Shader 属性 以得到满意的效果。

Unity Shader 定义了渲染所需的各种代码(如顶点着色器 片元着色器)属性(使用哪些纹理等)和指令(渲染和标签设置等),而材质允许我们调节这些属性,并将其最终赋给相应的模型

Unity中的Shader

Unity一共提供了4种Unity Shader模板供我们选择

  • Standard Surface Shader 会产生一个包含了标准光照模型的表面着色器模板
  • Unlit Shader 会产生一个不包含光照的基本顶点/片元着色器模板
  • Image Effect Shader 为我们实现各种屏幕后处理效果提供了一个基本模板
  • Compute Shader 会产生一种特殊shader文件,这类shader旨在利用GPU的并行性来进行一些与常规渲染流水线无关的计算

Standard Surface Shader为我们提供了典型的表面着色器的实现方法,但本书的重点在于如何在Unity中编写顶点/片元着色器,因此在后续的学习中,我们通常会使用Unlit Shader来生成一个基本的顶点/片元着色器模板。

ShaderLab

Unity为了解决自定义渲染需要很多文件和复杂的设置,提供了一层抽象———ShaderLab

ShaderLab是Unity提供的一种专门为Unity Shader服务的语言,是为Unity开发者提供的高层级的渲染抽象层

一个Unity Shader的基础结构:

Shader "ShaderName" {
    Properties {
          //属性
    }
    SubShader {
          //显卡A使用的子着色器
    }
    SubShaderB {
          //显卡B使用的子着色器
    }
    FallBack "VertexLit"
}

Unity Shader的结构

Name

每个Unity Shader文件的第一行都需要通过Shader语义来指定名字,可以通过在字符串中添加 / ,控制Shader在材质面板出现的位置

Shader "Custom/Shader" { }

Properities

Properties 语义块中包含了 一系列属性 (property),这些属性将会出现在材质面板中。

显示的名称 (display name) 则是出现在材质面板上的名字。我们需要为每个属性指定它的类型 (PropertyType)

Properties {
   Name ("display name",PropertyType) = DefaultValue
   Name ("display name",PropertyType) = DefaultValue
   //更多属性
}
属性类型
  • 对于 Int Float Range 这些数字类型的屈性,其默认值就是一个单独的数字;
  • 对于 Color 和 Vector 这类属性,默认值是用圆括号包围的一个四维向量;
  • 对于 2D Cube 3D 种纹理类型,默认值的定义稍微复杂,它们的默认值是通过一个字符串后跟一个花括号来指定的
Shader "Unity Shaders Book/Chapter 3/MyShader" {
	Properties {
		// Numbers and Sliders
		_Int ("Int", Int) = 2
		_Float ("Float", Float) = 1.5
		_Range("Range", Range(0.0, 5.0)) = 3.0
		// Colors and Vectors
		_Color ("Color", Color) = (1,1,1,1)
		_Vector ("Vector", Vector) = (2, 3, 6, 1)
		// Textures
		_2D ("2D", 2D) = "" {}
		_Cube ("Cube", Cube) = "white" {}
		_3D ("3D", 3D) = "black" {}
         }
}

SubShader

每一个Unity Shader文件可以包含多个SubShader语义块,但最少要有一个。

当Unity需要加载这个Unity Shader时,Unity会扫描所有的SubShader语义块, 然后选择第一个能够在目标平台上运行的SubShader。 如果都不支持的话, Unity就会使用Fallback语义指定的Unity Shader。

Unity提供这种语义的原因在于, 不同的显卡具有不同的能力。

SubShader{
   //可选的
   [Tags]

   //可选的
   [RenderSetup]

   Pass {
   }
   //Other Passes
}

SubShader中定义了 一系列Pass以及可选的 状态([RenderSetup]) 标签([Tags]) 设置。 每个Pass定义了一次完整的渲染流程

状态和标签同样可以在Pass声明。不同的是,SubShader中的一些标签设置是特定的。 也就是说, 这些标签设置和Pass中使用的标签是不一样的。但是, 如果我们在SubShader进行了这些设置, 那么将会用于所有的Pass。

状态设置

Shader Lab提供了一 系列渲染状态的设置指令, 这些指令可以设置显卡的各种状态, 例如是
否开启混合/深度测试等。

当在SubShader块中设置了下述渲染状态时,将会应用到所有的Pass。

SubShader的标签

SubShader的标签(Tags)是一个键值对(Key/Value Pair), 它的键和值都是字符串类型。

这些键值对是SubShader和渲染引擎之间的沟通桥梁。它们用来告诉Unity的渲染引擎:我希望怎样以及何时渲染这个对象

Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
SubShader标签类型

需要注意的是,上述标签仅可以在 SubShader 声明 ,而不可以在 Pass 块中声明。 Pass块虽然也可以定义标签,但这些标签是不同于 SubShader 的标签类型。

Pass语义块

Pass {
    [Name]
    [Tags]
    [RenderSetup]
    //Other code
}
Pass 名称
Name "MyPassName"

通过这个名称,我们可以使用 ShaderLab UsePass 令来直接使用其他 Unity Shader 中的Pass 。

因为Unity内部会把所有Pass的名称转换成大写字母的表示,因此,在使用UsePass命令时必须使用大写形式的名字

UsePass "MyShader/MYPASSNAME"
Pass渲染状态

SubShader 的状态设置同样适用于 Pass 。除了上面提到的状态设置外,在 Pass 中我们还可以使用固定管线的着色器。Pass 同样可以设置标签 但它的标签不同于 SubShader 的标签。

除了上面普通的 Pass 定义外, Unity Shader 支持一些特殊的 Pass, 以便进行代码复用或实现更复杂的效果。

  • UsePass: 如我们之前提到的一样,可以使用该命令来复用其他 Unity Shader 中的 Pass
  • GrabPass: 负责抓取屏称并将结果存储在 张纹理中,以用于后续的 Pass 处理

Fallback

紧跟在各个 SubShader 语义块后面的,可以是一个 Fallback 指令。Fallback用于在所有Subshader都不适用的情况下使用

Fallback "name"
//或者
Fallback off

Fallback 还会影响阴影的投射。

Unity Shader的形式

尽管 Unity Shader 可以做的事情非常多(例如设置渲染状态等),但其最重要的任务还是指定各种着色器所需的代码。这些工作可以在SubShader中也可以在Pass中实现。

在Unity Shader中使用三种形式来编写

Shader "MyShader" {
    Properties {
       //所需的各种属性
    }
    SubShader {
       //表面着色器 Surface Shader
       //或者
       //顶点/片元着色器 Vertex/Fragment Shader
       //或者
       //固定函数着色器 Fixed Function Shader
    }
    SubShader {
       //和上一个类似
    }
{

表面着色器

表面着色器 (Surface Shader) 是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 SurfaceOut o) {
           o.Albedo = 1;
       }
       ENDCG
    }
    Fallback "Diffuse"
}

表面着色器被定义在 SubShader 语义块(而非 Pass 语义块)中的CGPROGRAM ENDCG 之间

原因是,表而着色器不需要开发者关心使用多少个 Pass 、每个Pass 如何渲染等问题, 会在背后为我们做好这些事情

CGPROGRAM ENDCG 之间的代码是使用 CG/HLSL 编写的,也就是说,我们需要把CG/HLSL 语言嵌套在 ShaderLab 语言中

顶点/片元着色器

在Unity中我们可以使用 CG HLSL 语言来编写顶点/片元着色器 (Vertex/Fragment Shader),它们更加复杂,但灵活性也更高。

Shader "Custom/Soimple VertexFragment Shader" {
    SubShader {
       Pass {
          CGPROGRAM

          #pragma vertex vert
          #pragma fragment frag

          float4 vert(float4 v : POSITION) : SV_POSITION {
             return mul (UNITY_MATRIX_MVP,v);
          }

          fixed frag() : SV_Target {
             return fixed4(1.0,0.0,0.0,1.0);
          }

          EDNCG
       }
    }
}

和表面着色器类似 顶点/片元着色器的代码也需要定义在 CGPROGRAM ENDCG 之间,但不同的是 顶点/片元着色器是写在 Pass 语义块内,而非 SubShader 内的,原因是,我们需要自已定义每个 Pass 需要使用的 Shader 代码 。

固定函数着色器

上面两种 nity Shader 形式都使用了可编程管线。而对于一些较旧 的设备 (其 GPU 仅支持DirectX 7.0 OpenGL 1.5 OpenGL ES 1. 1), 例如 iPhone 3, 它们不支持可编程管线着色器,因此,这时候我们就需要使用 固定函数着色器 (Fixed Function Shader) 来完成渲染。这些着色器往往只可以完 一些非常简单效果。

选择Unity Shader形式

  • 除非你有非常明确的需求必须要使用固定函数着色器,例如需要在非常旧的设备上运行你的游戏(这些设备非常少见),否则请使用可编程管线的着色器,即表面着色器或顶点/片元着色器。
  • 如果你想和各种光源打交道,你可能更喜欢使用表面着色器 ,但需要小心它在移动平台的性能表现。
  • 如果你需要使用的光照数目非常少,例如只有 个平行光,那么使用顶点/片元着色器是个更好的选择。
  • 最重要的是,如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。

Unity Shader != 真正Shader

  • Unity 里, Unity Shader 实际上指的就是 ShaderLab 文件——硬盘上以 .shader 作为文件后缀的一种文件。Unity Shader里,我们可以做的事情远多于一个传统意义上的Shader
  • 在传统的 Shader 中,我们仅可以编写特定类型的 Shader, 例如顶点着色器、片元着色器等。而在 Unity Shader 中,我们可以在同 个文件里同时包含需要的顶点着色器和片元着色器代码。
  • 在传统的 Shader 中,我们无法设置一些渲染设置,例如是否开启混合、深度测试等,这些是开发者在另外的代码中自行设置的。而在 Unity Shader 中,我们通过 行特定的指令就可以完成这些设置。
  • 在传统的 Shader 中,我们需要编写冗长的代码来设置着色器的输入和输出,要小心地处理这些输入输出的位翌对应关系等。而在 Unity Shader 中,我们只需要在特定语句块中声明一些属性,就可以依靠材质来便地改变这些属性。而且对于模型自带的数据(如顶点位置、纹理坐标、法线等),Unity Shader 也提供了直接访问的方法,不需要开发者自行编码来传给着色器