第三章、Unity Shader基础

一、Unity Shader概述

1、材质和Unity Shader

  • Unity中需要配合使用材质(Material) 和Unity Shader才能达到需要的效果。
  • 常见的流程:
    • 创建一个材质
    • 创建一个Unity Shader,并把它赋给上一步中创建的材质
    • 把材质赋给要渲染的对象
    • 在材质面板中调整Unity Shader的属性


      Unity Shader和材质。首先创建需要的Unity Shader和材质,然后把Unity Shader赋给材质,并在材质面板上调整属性(如使用的纹理、漫反射系数等)。最后,将材质赋给相应的模型来查看最终的渲染效果
  • Unity Shader定义了渲染所需的各种代码(如顶点着色器和片元着色器)、属性(如使用那些纹理等)和指令(渲染和标签设置等),而材质则允许我们调节这些属性,并将其最终赋给相应的模型。
  • Unity中的材质决定了我们的游戏对象看起来是什么样子(配合Unity Shader)

2、Unity中的材质

Unity中的材质需要结合一个GameObject 的 Mesh或者Particle Systems组件来工作。它决定了我们的游戏对象看起来是什么样子的(这当然也需要Unity Shader 的配合)。

为了创建一个新的材质,我们可以在Unity的菜单栏中选择Assets -> Create -> Material来创建,也可以直接在Project视图中右击->Create -> Material 来创建。当创建了一个材质后,就可以把它赋给一个对象。这可以通过把材质直接拖曳到Scene视图中的对象上来实现,或者在该对象的 Mesh Renderer组件中直接赋值,如图3.2所示。

在Unity 5.x版本中,默认情况下,一个新建的材质将使用Unity内置的Standard Shader,这是一种基于物理渲染的着色器,我们将在第18章中讲到。

对于美术人员来说,材质是他们十分熟悉的一种事物。Unity 的材质和许多建模软件(如 Cinema4D、Maya等)中提供的材质功能类似,它们都提供了一个面板来调整材质的各个参数。这种可视化的方法使得开发者不再需要自行在代码中设置和改变渲染所需的各种参数,如图3.3所示。


3、Unity中的Shader

  • Unity Shader

    Shader,Unity Shader能做的大于传统Shader
  • Unity Shader模板:
    • Standard Surface Shader:标准光照模型
    • Unlit Shader:不包含光照(但包含雾效)的基本顶点/片元着色器
    • Image Effect Shader:屏幕后处理效果
    • Compute Shader:利用GPU的并行性来进行一些与常规渲染流水线无关的计算

二、Unity Shader的基础:ShaderLab

1、ShaderLab语言

Unity提供了一种专门为Unity Shader服务的语言——ShaderLab。在Unity中,所有的Unity SHader都是使用ShaderLab来编写的。ShaderLab是Unity提供编写Unity Shader的一种说明性语言。

它使用了一些嵌套在花括号内部的语义(syntax) 来描述一个Unity Shader文件的结构。

这些结构包含了许多渲染所需的数据,例如Properties语句块中定义了着色器所需的各种属性,这些属性将出现在材质面板中。

ShaderLab类似于CgFX和Direct3D Effects(.FX)语言,他们都定义来了要显示一个材质所需的所有东西,而不仅仅是着色器代码
 


Unity Shader为控制渲染过程提供了一层抽象。如果没有使用Unity Shader(左图),开发者需要和很多文件和设置打交道,才能让画面呈现出想要的效果;而在Unity Shader的帮助下(右图),开发者只需要使用ShaderLab来编写Unity Shader文件就可以完成所有的工作


2、Unity Shader的基础结构

一个Unity Shader的基础结构如下所示:

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

Unity在背后会根据使用的平台来把这些结构编译成真正的代码和 Shader 文件,而开发者只需要和Unity Shader打交道即可。


三、Unity Shader的结构

1、给Shader起一个名字

每个Unity Shader 文件的第一行都需要通过Shader语义来指定该Unity Shader的名字。这个名字由一个字符串来定义,例如“MyShader”。当为材质选择使用的Unity Shader时,这些名称就会出现在材质面板的下拉列表里。通过在字符串中添加斜杠(“/”),可以控制UnityShader在材质面板中出现的位置。例如:

Shader "path/display name"

那么这个Unity Shader在材质面板中的位置就是:Shader -> Custom -> MyShader,如图3.7所示。
 


Custom/MyShader:在Unity Shader的名称定义中利用斜杠来组织在材质面板中的位置


2、Properties:材质和Shader的桥梁

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

Properties语句块的定义通常如下:

Properties{
	Name("display name", PropertyType) = DefaultValue
	Name("display name", PropertyType) = DefaultValue
	// 更多属性
}

  • Name:属性的名字,通常由一个下划线开始。Shader中访问的标识符。
  • display name:出现在材质面板上的名字
  • PropertyType:属性的类型
  • DefaultValue:默认值,第一次赋予材质时,面板上出现的值就是默认值

Properties 语义块支持的属性类型

int、Float、Range数字类型属性:其默认值就是一个单独的数字;

Color、Vector属性:默认值是用圆括号包围的一个四维向量;

2D、Cube、3D这3种纹理类型:它们的默认值是通过一个字符串后跟一个花括号来指定的,其中,字符串要么是空的,要么是内置的纹理名称,如“white”"black""“gray”或者“bump”。花括号的用处原本是用于指定一些纹理属性的,例如在 Unity 5.0以前的版本中,我们可以通过TexGenCubeReflect、TexGen CubeNormal等选项来控制固定管线的纹理坐标的生成。但在Unity 5.0以后的版本中,这些选项被移除了,如果我们需要类似的功能,就需要自己在顶点着色器中编写计算相应纹理坐标的代码。

下面的代码给出一个展示所有属性类型的例子:

Shader "Custom/ShaderLabProperties" {
    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" {}
    }

    FallBack "Diffuse"
}

上述代码在材质面板显示结果如下:

有时,我们想要在材质面板上显示更多类型的变量,例如使用布尔变量来控制Shader中使用哪种计算。Unity 允许我们重载默认的材质编辑面板,以提供更多自定义的数据类型。我们在本书资源的材质Assets -> Materials -> Chapter3 - RedifyMat中提供了这样一个简单的例子,这个例子参考了官方手册的 Custom Shader GUI一文(Unity - Manual: ShaderLab: assigning a custom editor中的代码。

为了在 Shader中可以访问到这些属性,我们需要在CG代码片中定义和这些属性类型相匹配的变量。需要说明的是,即使我们不在 Properties语义块中声明这些属性,也可以直接在CG代码片中定义变量。此时,我们可以通过脚本向Shader中传递这些属性。因此,Properties语句块的作用仅仅是为了让这些属性可以出现在材质面板中。


3、重量级成员:SubShader

3.1、SubShader的结构

每一个UnityShader文件可以包含多个SubShader语义块,但最少要有一个。Unity会扫描所有SubShader语义块,选择第一个能够在目标平台运行的。如果都不支持的话,会使用Fallback语义指定的Unity Shader。

Unity提供这种语义的原因在于,不同的显卡具有不同的能力。例如,一些旧的显卡仅能支持一定数目的操作指令,而一些更高级的显卡可以支持更多的指令数,那么我们希望在旧的显卡上使用计算复杂度较低的着色器,而在高级的显卡上使用计算复杂度较高的着色器,以便提供更出色的画面。

SubShader{
	// 可选的
	[Tags]
	
	// 可选的
	[RenderSetup]
	
	Pass{
	
	}
	// Other Passes
}

  • SubShader中定义了一系列Pass以及可选的状态([RenderSetup])和标签([Tags])设置。
  • 每个Pass定义一次完整的渲染流程, 但如果Pass的数目过多, 往往会造成渲染性能的下降。
  • 状态和标签的位置:
    • SubShader:全局作用域
    • Pass:Pass局部作用域(只针对这一次渲染)

3.2、SubShader状态设置

ShaderLab提供了一系列渲染状态的设置指令,这些指令可以设置显卡的各种状态,例如是否开启混合/深度测试等。表3.2给出了ShaderLab中常见的渲染状态设置选项。

当我们在SubShader中设置了上述渲染状态后,将会应用到所有的Pass中,如果我们希望不同的Pass中有不同的渲染效果我们可以单独在SubShader和Pass中声明。


3.3、SubShader的标签

SubShader的标签是一个键值对(Key/Value Pair),它的键和值都是字符串类型。这
些键值对是SubShader和渲染引擎之间的沟通桥梁。 它们用来告诉Unity的渲染引擎:我希望怎样以及何时渲染这个对象。

标签的结构如下:

Tags{"TagName1"="Value1" "TagName2"="Value2"}

SubShader 的标签块支持的标签类型
 


需要注意的是,上述标签只能在SubShader中声明,不能在Pass中声明。Pass中也能定义标签,但是这些标签不同于SubShader的标签类型。

3.4、Pass语义块

Pass语义块包含的语义如下:

Pass{
	[Name]
	[Tags]
	[RenderSetup]
	// Other code
}

  • 首先定义Pass的名称:
Name "MyPassName"

    • 我们可以使用ShaderLab中的UsePass命令来直接使用其他Shader中的Pass。
UsePass "MyShader/MYPASSNAME"

注意Unity内部会把所有Pass的名称转换成大写。

我们还可以对Pass设置渲染状态,还可以使用固定管线的着色器命令。

Pass同样可以设置标签

  • Pass的标签告诉渲染引擎我们希望怎样来渲染该物体。
    Pass的标签类型

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

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

3.5、留一条后路:Fallback

Fallback指令会告诉Unity,如果上面所有的SubShader在这块显卡上都不能运行,那么就使用这个最低级的Shader。

Fallback语义如下:

Fallback "name"
// 或者
Fallback Off

如上所述,我们可以通过一个字符串来告诉Unity这个“最低级的Unity Shader”是谁。我们也可以任性地关闭 Fallback`功能,但一旦你这么做,你的意思大概就是:“如果一块显卡跑不了上面所有的SubShader,那就不要管它了!”

下面给出了一个使用Fallback语句的例子:

Fallback "VertexLit"

事实上, Fallback 还会影响阴影的投射。

在渲染阴影纹理时,Unity 会在每个Unity Shader中寻找一个阴影投射的Pass。通常情况下,我们不需要自己专门实现一个 Pass,这是因为Fallback使用的内置Shader 中包含了这样一个通用的Pass。因此,为每个Unity Shader 正确设置Fallback是非常重要的。


四、Unity Shader的形式

1、Unity Shader中三种着色器代码形式

  • 表面着色器:着色器代码写在SubShader语义块中
  • 顶点/片元着色器和固定着色器:着色器代码写在Pass语义块中

在 Unity中,我们可以使用下面3种形式来编写Unity Shader。而不管使用哪种形式,真正意义上的Shader代码都需要包含在 ShaderLab语义块中,如下所示:

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

2、表面着色器(Surface Shader)

表面着色器是顶点/片元着色器更高一层的抽象。它的渲染代价大,因为实质上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 = 1;
		}
		ENDCG
	}
	Fallback "Diffuse"
}

表面着色器只能定义在SubShader语义块中的CGPROGFRAMENDCG之间。因为表面着色器不需要开发者关心使用多少个Pass和每个Pass如何渲染等问题,Unity会在背后做好这些事情。我们要做的只是告诉它:“嘿,使用这些纹理去填充颜色,使用这个法线纹理去填充法线,使用 Lambert光照模型,其他的不要来烦我!”。

CGPROGRAM和 ENDCG之间的代码是使用CG/HLSL 编写的,也就是说,我们需要把CG/HLSL 语言嵌套在ShaderLab语言中。值得注意的是,这里的CG/HLSL是Unity经封装后提供的,它的语法和标准的CG/HLSL 语法几乎一样,但还是有细微的不同,例如有些原生的函数和用法 Unity并没有提供支持。


3、顶点/片元着色器(Vertex/Fragment Shader)

在Unity中我们可以使用CG/HlSL语言来编写顶点/片元着色器,顶点/片元着色器更加复杂,但也更加灵活。

一个非常简单的顶点/片元着色器示例代码如下:

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_MPV, v);
			}

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

顶点/片元着色器只能定义在Pass语义块中的CGPROGRAMENDCG之间。

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


4、固定函数着色器(Fixed Function Shader)

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

一个非常简单的固定函数着色器示例代码如下:

Shader "Tutorial/Basic" {
    Properties {
        _Color {"Main Color", Color) = (1,0.5,0.5,1)
    }
    SubShader {
        Pass {
            Material {
                Diffuse [_Color]
            }
            Lighting On
        }
    }
}

可以看出,固定函数着色器的代码被定义在Pass语义块中,这些代码相当于Pass中的一些渲染设置。

对于固定函数着色器来说,我们需要完全使用ShaderLab的语法(即使用ShaderLab的渲染设置命令)来编写。而非使用CG/HLSL。

由于现在绝大多数GPU 都支持可编程的渲染管线,这种固定管线的编程方式已经逐渐被抛弃。实际上,在Unity 5.2中,所有固定函数着色器都会在背后被Unity编译成对应的顶点/片元着色器,因此真正意义上的固定函数着色器已经不存在了。


5、选怎哪一种Unity Shader形式

那么,我们究竟选择哪一种来进行Unity Shader 的编写呢?这里给出了一些建议。

除非你有非常明确的需求必须要使用固定函数着色器,例如需要在非常旧的设备上运行你

的游戏(这些设备非常少见),否则请使用可编程管线的着色器,即表面着色器或顶点/片元着色器。

如果你想和各种光源打交道,你可能更喜欢使用表面着色器,但需要小心它在移动平台的

性能表现。

如果你需要使用的光照数目非常少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。

最重要的是,如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。


五、本书使用的Unity Shader形式

本书的目的不仅在于教给读者如何使用Unity Shader,更重要的是想要让读者掌握渲染背后的原理。仅仅了解高层抽象虽然可能会暂时使工作简化,但从长久来着“知其然而不知其所以然”所带来的影响更加深远。

因此,在本书接下来的内容中,我们将着重使用顶点/片元着色器来进行Unity Shader 的编写。对于表面着色器来说,我们会在本书的第17章中进行剖析,读者可以在那里找到更多的学习内容。


六、答疑解惑

1、Unity Shader ≠真正的Shader


2、Unity Shader和CG、HLSL之间的关系


3、我可以使用GLSL来写吗

当然可以。如果你坚持说:“我就是不想用CG/HLSL来写!就是要使用GLSL来写!",但是这意味着你可以发布的目标平台就只有Mac OS X、OpenGL ES 2.0或者Linux,而对于PC、Xbox360这样的仅支持DirectX的平台来说,你就放弃它们了。

建立在你坚持要用GLSL 来写Unity Shader的意愿下,你可以怎么写呢?和CG/HLSL需要嵌套在CGPROGRAM和 ENDCG之间类似,GLSL的代码需要嵌套在GLSLPROGRAM和 ENDGLSL之间。

更多关于如何在Unity Shader中写GLSL 代码的内容可以在Unity官方手册的GLSL ShaderPrograms一文( http://ldocs.unity3d.com/Manual/SL-GLSLShaderPrograms.html)中找到。


七、扩展阅读

Unity 官网上关于Unity Shader方面的文档正在不断补充中,由于 Unity封装了很多功能和细节,因此,如果读者在使用 Unity Shader的过程中遇到了问题可以去到官方文档(http:/ldocs.unity3d.com/Manual/SL-Reference.html)中查看。除此之外,Unity也提供了一些简单的着色器编写教程(http:/docs.unity3d.com/Manual/ShaderTut1.html, http://docs.unity3d.com/Manual/ShaderTut2.html)。由于在Unity Shader中,绝大多数可编程管线的着色器代码是使用CG语言编写的,读者可以在 NVIDIA 提供的CG文档( http:/http.developer.nvidia.com/CG/)中找到更多的内容。NVIDIA同样提供了一个系列教程(http:/http.developer.nvidia.com/CGTutorialcG_tutorial_chapter01.html)来帮助初学者掌握CG的基本语法。

猜你喜欢

转载自blog.csdn.net/qq_63388834/article/details/135189366