Shader即着色器,是一款运行在GPU上的程序,用来对三维物体进行着色处理、光与影的计算、纹理颜色的呈现等,从而将游戏引擎中的一个个作为抽象的几何数据存在的模型、场景和特效,以和真实世界类似的光与影的形式呈现在玩家的眼中。

开始

Shader是什么

Shader即着色器,是一款运行在GPU上的程序,用来对三维物体进行着色处理、光与影的计算、纹理颜色的呈现等,从而将游戏引擎中的一个个作为抽象的几何数据存在的模型、场景和特效,以和真实世界类似的光与影的形式呈现在玩家的眼中。
着色器用于图形处理器(GPU)的可编程流水线。渲染流水线可查看这篇读书笔记。可编程流水线还能处理所有像素、顶点、纹理的位置、色调、饱和度、明度、对比度并实时地绘制图像。着色器还能产生如模糊、高光、有体积光源、失焦、卡通渲染、色调分离、畸变、凹凸贴图、边缘检测、运动检测等效果。

一些概念

材质(Material)
Shader负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出。绘图单元可以依据这个输出来将图像绘制到屏幕上。输入的贴图或者颜色等,加上对应的Shader,以及对Shader的特定的参数设置,将这些内容(Shader及输入参数)打包存储在一起,得到的就是一个Material(材质)。

纹理(Texture)
又称纹理贴图,在计算机图形学中是把存储在内存里的位图包裹到3D渲染物体的表面。纹理贴图给物体提供了丰富的细节,用简单的方式模拟出了复杂的外观。一个图像(纹理)被贴(映射)到场景中的一个简单形体上,就像印花贴到一个平面上一样。这大大减少了在场景中制作形体和纹理的计算量。
undefined

1: 未加纹理贴图的球体, 2: 纹理贴图加凹凸贴图, 3: 仅凹凸贴图, 4: 透明图加纹理贴图 图片来源于wiki

纹理坐标
模型上面的贴图是通过纹理映射技术将其附加在模型上面,美术人员利用Max等工具建模时,会在建模软件中利用纹理展开技术把纹理映射坐标存储在每个顶点上,纹理映射坐标定义了该顶点在纹理中对应的2D坐标。这些坐标使用一个二维变量(u,v)来表示,纹理映射坐标也称为UV坐标。顶点UV坐标的范围通常都被归一化到[0,1]范围内。

纹理采样

顶点和片元
顶点即模型中原始的顶点,一个顶点可以包含位置、颜色、法线、切线等数据。图元是由几何顶点组合而成,例如点、线段、多边形。片元是图元经过裁剪、转换窗口坐标、光栅化等一系列步骤后得来,需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了屏幕坐标、深度信息、以及从几何阶段输出的法线、纹理坐标等。
显示在屏幕上的顶点经过的变换:一个顶点是怎么显示在屏幕上的

渲染路径
Rendering Path其实指的就是渲染场景中光照的方式。由于场景中的光源可能很多,甚至是动态的光源。所以怎么在速度和效果上达到一个最好的结果确实很困难。以当今的显卡发展为契机,人们才衍生出了这么多的Rendering Path来处理各种光照。
详细了解Rendering Path

进阶

Unity中的Shader

在Unity中,在Project视图中右击->Create->Shader能够创建一下集几种Shader

undefined

unity版本:5.5.0f3

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

Surface Shader(表面着色器)

着色器基础结构:

Shader "Custom/MyShader" {
	Properties {
		//属性
	}
	
	SubShader {
		//显卡A使用的Shader
	}
	
	SubShader {
		//显卡B使用的Shader
	}
	....
	//回滚
	FallBack "Diffuse"
}

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

Properties {
		Name("显示在面板的名字",属性类型) = DefaultValue
		Name("显示在面板的名字",属性类型) = DefaultValue
		//...
	}

属性类型(PropertyType)包括:Int,Float,Range(min,max),Color,Vector,2D,Cube,3D

Unity Shader可以包含多个SubShader语义块,至少一个。提供这种语义的原因是因为不同的显卡有不同的能力,在计算着色时,平台先选择最优先可以使用的着色器,然后依次运行其中的Pass(Unity Shader中的顶点/片元着色器)。

下面是Unity中创建的默认Surface Shader:

Shader "Custom/MyShader" {
	Properties {
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200
		
		CGPROGRAM
		// Physically based Standard lighting model, and enable shadows on all light types
		#pragma surface surf Standard fullforwardshadows

		// Use shader model 3.0 target, to get nicer looking lighting
		#pragma target 3.0

		sampler2D _MainTex;

		struct Input {
			float2 uv_MainTex;
		};

		half _Glossiness;
		half _Metallic;
		fixed4 _Color;

		void surf (Input IN, inout SurfaceOutputStandard o) {
			// Albedo comes from a texture tinted by color
			fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;
			// Metallic and smoothness come from slider variables
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
	}
	FallBack "Diffuse"
}

MyShader详解:

Properties
Properties即定义的属性。在材质面板中如下图所示:
undefined
SubShader
然后是SubShaderSubShader语义块中包含的定义通常如下:

SubShader{
	//可选的标签
	[Tags]

    //可选的状态
    [RenderSetup]
    
    //顶点/片元着色器
    Pass{
    }
    //Other Passses
   }

Tags
SubShader的标签是一个键值对,并且都是字符串类型。这些键值对是SubShader和渲染引擎之间沟通的桥梁。用来告诉Unity怎样以及何时渲染这个对象物体。
标签的结构:

Tags{"TagName1" = "Value1" "TagName2" = "Value2" }
**SubShader的标签类型**
标签类型 说明
Queue 控制渲染顺序,指定该物体的渲染队列,可以使用自定义的渲染队列控制渲染顺序。
RenderType 对着色器进行分类,例如这是一个不透明或者透明的着色器
DisableBatching 指明是否对该SubShader使用批处理
ForceNoShadowCasting 控制使用该SubShader的物体是否会投射阴影。
IgnoreProjector 控制使用该SubShader的物体是否受Projector的影响。通常用于半透明物体
CanUseSpriteAtlas 当该SubShader用于精灵(Sprites)时,将该标签设置为False。
PreviewType 指明材质面板将如何预览该材质。默认为球形,可以设置为"Plane","SkyBox"。

状态

ShaderLab提供了一系列渲染状态的设置指令。

**常见的渲染状态设置选项**
状态名称 设置指令 说明
Cull Cull Back/Front/Off 设置剔除模式:剔除背面/正面/关闭剔除
ZTest ZTest Less Greater/LEqual/GEqual/Equal/NotEqual/Always 设置深度测试时使用的函数
ZWrite ZWrite On/Off 开启/关闭深度写入
Blend Blend SrcFactor DstFactor 开启并设置混合模式

当在SubShader块中设置了渲染状态时,将会应用到所有Pass。也可以在Pass块中单独设置渲染状态。

LOD
LOD即Level of Detail的缩写。是一种控制细节级别的技术。

在脚本中,我们可以用 Shader.globalMaximumLOD = 100 来全局设置 LOD值,激活使用相对应的 SubShader。
也可以在Edit->Project Setting->Quality 中设置Maximum LOD Level设置最大LOD等级。

Shader的LOD值小于所设定的LOD值,才会被编译使用。 Maximum LOD Level的等级可以设置7个级别,例如设置为1,则表示把最大LOD值设置为100,等级2,则最大LOD值为200,以此类推,若设置为0,则表示不进行LOD判断,任何LOD值的Shader都会被使用。

根据这个特性,我们就可以在一个 Shader 里写一出组 SubShader ,分别设置不同的 LOD ,LOD 越大对应更好的效果并且要求更好的显卡性能。然后我们就可以用设置 LOD 的方法来控制游戏画面的渲染质量。

主体

//Shader的主体部分
CGPROGRAM
、、、
ENDCG

使用CGPROGRAM表示使用Cg/HLSL编写Shader代码。
如果使用GLSL编写则需要嵌套在GLSLPROGRAMENDGLSL之间

// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows

这是一据编译指令,它的格式如下:

#pragma surface surfaceFunction lightModel [optionalparams]
  • surface 表示声明的是一个表面着色器
  • surfaceFunction 表示着色器代码的方法的名字
  • lightModel 使用的光照模型
  • optionalparams 其他可选的编译指令

所以上面的代码表示:着色器代码的方法的名字是surf( ){ }使用的光照模型是基于物理的标准光照模型,fullforwardshadows表示在所有灯光类型启用阴影在Forward渲染路径中。

更多的编译指令可以看这里

// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0

这个编译指令表示定义Shader模型为Shader Model 3.0,

sampler2D _MainTex;
half _Glossiness;
half _Metallic;
fixed4 _Color;

这是CG程序中的属性声明。可以发现这些变量与上面的属性名称是一样的。
原因是这个Shader其实是由两个相对独立的块组成的,外层的属性声明,回滚等等是Unity可以直接使用和编译的ShaderLab;而现在我们是在CGPROGRAM...ENDCG这样一个代码块中,这是一段CG程序。对于这段CG程序,要想访问在Properties中所定义的变量的话,必须使用和之前变量相同的名字进行声明。于是其实sampler2D _MainTex;做的事情就是再次声明并链接了_MainTex,使得接下来的CG程序能够使用这个变量。

//结构体
struct Input {
			float2 uv_MainTex;
		};

这里声明一个结构体,在后面作为着色器函数的输入。
在CG程序中,在一个贴图变量(例子中是_MainTex)之前加上uv两个字母,就代表提取它的uv值(其实就是两个代表贴图上点的二维坐标 )。我们之后就可以在surf程序中直接通过访问uv_MainTex来取得这张贴图当前需要计算的点的坐标值了。

void surf (Input IN, inout SurfaceOutputStandard o) {
		// Albedo comes from a texture tinted by color
		fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
		o.Albedo = c.rgb;
		// Metallic and smoothness come from slider variables
		o.Metallic = _Metallic;
		o.Smoothness = _Glossiness;
		o.Alpha = c.a;
	}

这里即着色器的方法部分了。
SurfaceOutputStandard是预定义的输出结构,surf函数的目标就是根据输入把这个输出结构填上。
SurfaceOutputStandard结构体的定义如下:

struct SurfaceOutputStandard
{
    fixed3 Albedo;      // base (diffuse or specular) color
    fixed3 Normal;      // tangent space normal, if written
    half3 Emission;
    half Metallic;      // 0=non-metal, 1=metal
    half Smoothness;    // 0=rough, 1=smooth
    half Occlusion;     // occlusion (default 1)
    fixed Alpha;        // alpha for transparencies
};

回滚

FallBack "Diffuse"

这个指令用于告诉Unity,如果上面所有的SubShader在这块显卡上都不能运行,就用这个最低级的Shader。
也可以FallBack Off直接关闭回滚功能。

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

在Unity中通过在项目视图右键 Create->Shader->Unity Shader可以创建一个默认的顶点/片元着色器

Shader "Unlit/VertFragShader"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				// apply fog
				UNITY_APPLY_FOG(i.fogCoord, col);
				return col;
			}
			ENDCG
		}
	}
}
#pragma vertex vert
#pragma fragment frag

这段编译指令分别指定顶点着色器和片元着色器对应的方法名vertfrag

#include "UnityCG.cginc"

为了方便开发者的编码过程,Unity'提供了很多内置文件,这些文件包含了很多提前定义的函数、变量、宏等。我们可以使用#include指令把这些文件包含进来,就可以使用Unity为我们提供的一些非常有用的变量和帮助函数。

struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

这里使用一个结构体来定义顶点着色器的输入,问了创建一个自定义的结构体,我们必须使用如下格式来定义它:

struct StructName{
	Type Name : Semantic;
	Type Name : Semantic;
	......
};

Unity支持的语义(Semantic)有
POSITION,TANGENT,NORMAL,TEXCOORD0,TEXCOORD1,TEXCOORD2,TEXCOORD3,COLOR等。
上面代码的POSITION语义表示用模型空间的顶点坐标填充vertex变量,TEXCOORD0表示用模型的第一套纹理坐标填充texcoord变量。

struct v2f
		{
			float2 uv : TEXCOORD0;
			UNITY_FOG_COORDS(1)
			float4 vertex : SV_POSITION;
		};

这个结构体用于在顶点着色器和片元着色器之前传递信息,所以一般叫做v2f(vert to frag)。
SV_POSITION表示顶点着色器的输出是裁剪空间的顶点坐标。

需要注意的是表面着色器(Surface Shader)本质上也是顶点/片元着色器。在提供给Unity一个表面着色器时,它会在背后转换为比较复杂的顶点/片元着色器。


参考

[1] 《Unity Shader入门精要》.冯乐乐
[2] 《ShaderLab开发实战详解》.郭浩瑜
[3] https://onevcat.com/2013/07/shader-tutorial-1/
[4] http://gad.qq.com/article/detail/27474
[5] http://blog.csdn.net/wpapa/article/details/72721104