【UnityShader入门精要学习笔记】第五章(1)年轻人的第一个Shader

在这里插入图片描述
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:

  • 书本中句子照抄 + 个人批注
  • 项目源码
  • 一堆新手会犯的错误
  • 潜在的太监断更,有始无终

总之适用于同样开始学习Shader的同学们进行有取舍的参考。



一个最简单的顶点/片元着色器

现在我们将学习如何编写一个顶点/片元着色器

一个Unity Shader的基本结构,包括了Shader,Properties,SubShader,Fallback等语句块。
在这里插入图片描述

借上图回忆一下。

其中,最重要的部分就是Pass语义块,每一个渲染流程都是写在Pass语义块中的(之前定义表面着色器的时候是在SubShader块中定义的,而本质上SubShader块最后还是编译成了Pass语义块)

现在我们试着创建一个最简单的Shader:

(1) 新建一个Unity Shader 并命名
(2) 使用刚刚创建的Shader新建一个材质
(3) 新建一个球体并将刚才的材质赋给它
(4) 修改新建的Unity Shader代码:

Shader "Unity Shader Book/Chapter 5/Simple Shader"
{
	SubShader{
		pass{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			float4 vert(float4 v : POSITION) : SV_POSITION{
				return UnityObjectToClipPos(v);
			}

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

(注:在Unity5之后mul(UNITY_MATRIX_MVP,*)可以被UnityObjectToClipPos(*)所代替)
上述代码很容易看懂,我们通过对材质挂载物体顶点的MVP变换获得顶点转换到齐次裁剪空间的坐标,并return作为顶点着色器的输出。而片元着色器的输出我们默认为fixed4(1.0,1.0,1.0,1.0),对应RGBA的颜色值(1,1,1,1)

float4 vert(float4 v : POSITION) : SV_POSITION
返回值类型 函数名(输入定义) : 输出定义
vert函数的输入v包含了这个顶点的位置,这是通过POSITION语义指定的。它的返回值是一个float4类型的变量,它是该顶点在裁剪空间的位置。POSITIONSV_POSITION都是CG/HLSL中的语义(semantics),它们是不可省略的。POSITION语义告诉Unity,把模型的顶点坐标填充到输入参数v中,SV_POSITION则告诉Unity,顶点着色器的输出是裁剪空间的顶点坐标。(SV是system value的简写,用于输出裁剪空间的输出结果)如果没有这些语义来限定输入和输出参数的话,渲染器完全不知道用户的输入输出是什么,因此就会得到错误的结果。

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

片元着色器的frag函数没有任何输入。它的输出结果是fixed4类型的变量,并且使用了SV_Target语义进行限定。SV_Target语义告诉渲染器,将用户的输出颜色存储到一个**渲染目标(render target)**中,这里将输出到默认的帧缓存中。


获取模型数据

在上述例子中,我们为输入定义了POSITION语义来获得模型的顶点位置

现在如果我们想要获取更多的模型数据,例如想要模型上每个顶点的纹理坐标和法线方向,这个需求是很常见的。我们就需要使用纹理坐标(UV)来访问纹理,而法线可用于计算光照。因此,我们不能使用POSITION语义了,我们需要为顶点着色器定义一个新的输入参数,这个参数不再是一个简单的数据类型,而是一个结构体:

Shader "Unity Shader Book/Chapter 5/Simple Shader"
{
	SubShader{
		pass{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			// 使用一个结构体来定义顶点着色器的输入
			struct a2v {
				// POSITION 语义告诉Unity,用模型空间的顶点坐标填充vertex变量
				float4 vertex : POSITION;
				// NORMAL 语义告诉Unity,用模型空间的法线方向填充normal变量
				float3 normal : NORMAL;
				// TEXCOORD0 语义告诉Unity ,用模型的第一套纹理坐标填充texcoord变量
				float4 texcoord : TEXCOORD0;
			};

			float4 vert(a2v v) : SV_POSITION{
				return UnityObjectToClipPos(v.vertex);
			}

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

在上例中,我们将所需的输入定义为了结构体a2v,并使用语义定义填充结构体内部的变量。这样一个结构体可以包含多个输入,而上述处理顶点着色器的代码本质上和修改前的代码是一致的,只不过我们的入参从POSITION定义的vertex变为了使用结构体a2v内的变量vertex

对于顶点着色器的输入,Unity支持的语义有POSITON(顶点),TANGENT(切线),NORMAL(法线)TEXCOORD0(第一套纹理),TEXCOORD1(第2套纹理),TEXCOORD2(第3套纹理),TEXCOORD3(第4套纹理),COLOR(颜色)

为了创建一个自定义的结构体,我们必须用如下的格式来定义它:

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

语义是不可省略的。

a2v的含义就是application to vetex shader 也就是从应用阶段到顶点着色器,对应着GPU渲染流水线中的阶段。

那么填充语义的数据是从哪里来的呢?在Unity中,它们是由材质的MeshRender组件提供的,在每帧调用DrawCall的时候,MeshRender组件会把它负责渲染的模型数据发送给UnityShader。在每帧调用DrawCall的时候,MeshRender组件会把它负责渲染的模型数据发送给Unity Shader。顶点中就包含了法线,切线,纹理坐标,颜色等数据,就可以在Shader中访问了。


顶点着色器和片元着色器之间如何通信

我们往往希望从顶点着色器输出一些数据,例如将模型的法线,纹理坐标等数据传递给片元着色器。就需要顶点着色器与片元着色器之间进行通信。

Shader "Unity Shader Book/Chapter 5/Simple Shader"
{
	SubShader{
		pass{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};

			struct v2f {
				// SV_POSITION 语义告诉Unity,Pos里包含了顶点在裁剪空间中的位置信息
				float4 pos : SV_POSITION;
				// COLOR0语义可以用于存储颜色信息
				fixed3 color : COLOR0;
			};

			v2f vert(a2v v) {
				// 声明输出结构
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				// v.normal 包含了顶点的法线方向,其分量范围在[-1.0,1.0]
				// 下面的代码把分量范围映射到了[0.0,1.0]
				// 存储到o.color中传递给片元着色器
				o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target {
				return fixed4(i.color,1.0);
			}
			ENDCG
		}
	}
}

在这里插入图片描述
上述代码在顶点着色器函数vert中获取顶点信息,并将法线信息映射为[0,1]的分量,并转换为颜色值,返回给结构体v2f中的color变量。最后将color变量显示到屏幕上

上述代码中,我们定义了一个结构体v2f,并作为顶点着色器函数的输出类型来保存顶点着色器的输出。最后在片元着色器中结构体v2f作为输入类型。而SV_POSITION被我们定义为了结构体中pos的输入语义。

总而言之,不同着色器之间要进行数据通信,需要用到自定义的结构体,结构体中还得根据着色器默认语义(即必须包含的定义语义,例如顶点着色器的POSITION,片元着色器的SV_POSITION)来定义一些变量。最后在着色器函数定义时要将该结构体作为上一个着色器的输出和下一个着色器的输入。


如何使用属性

如果想要使用材质来改变模型的渲染,那么就需要使用到Properites,在之前的学习中我们知道属性是可以在材质面板上进行实时调整的。想要使用属性,就需要正确的定义,那么我们再次修改上述的代码:

Shader "Unity Shader Book/Chapter 5/Simple Shader"
{
	Properties	{
		// 声明一个Color类型的属性
		_Color ("Color Tint",Color) = (1.0,1.0,1.0,1.0)
	}

	SubShader{
		pass{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			// 在CG代码中,我们需要定义个与属性名称和类型都匹配的变量
			fixed4 _Color;

			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};

			struct v2f {
				float4 pos : SV_POSITION;
				fixed3 color : COLOR0;
			};

			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target {
				fixed3 c = i.color;
				// 使用_Color属性来控制输出颜色
				c *= _Color.rgb;
				return fixed4(c,1.0);
			}
			ENDCG
		}
	}
}

我们定义了一个颜色属性,并将该颜色rgb值与法线计算结果i.color的颜色rgb值进行相乘。
在这里插入图片描述
这样我们就能在材质面板上随意调整属性来改变着色器渲染结果拉~

我们需要在Properties语义块中定义属性_Color,并且需要在着SubShader块中定义一个新变量fixed4 _Color来访问这个属性。

细心的朋友们可能发现了,在Properties语句块和SubShader块中定义的变量类型是不同的。在ShaderLab中定义的变量类型和CG变量类型存在下列的匹配关系:

ShaderLab属性类型 CG变量类型
Color,Vector float4,half4,fixed4
Range,Float float,half,fixed
2D sampler2D
Cube sampleCube
3D sample3D

有时,在CG变量前还有一个uniform关键字,例如:

uniform fixed4 _Color;

uniform关键字是CG中修饰变量和参数的一种修饰词,它仅仅用于提供一些关于该变量的初始值是如何指定和存储的相关信息(与其他图像编程接口的uniform关键字不太一样),在Unity Shader中,uniform关键字是可以省略的。


Unity提供的内置文件和变量

在定义顶点/片元着色器的时候,我们处理信息是需要自己编写代码处理的。为了方便开发者,Unity也提供了一些内置文件,包含了许多提前定义的函数,变量,宏等方便进行Shader编写。如果Shader代码中发现了一些没有声明和定义的变量或者函数,那么可能就是使用了Unity内置的函数和变量。

内置的包含文件

包含文件(include file),类似于C++中的头文件。在Unity中,他们的文件后缀名为.cginc。在编写Shader时,我们可以使用#include指令来包含这些文件,这样我们就可以使用Unity为我们提供一些非常有用的变量和帮助函数。

CGPROGRAM
// ...
#include "UnityCG.cginc"
// ...
ENDCG

内置着色器可以在这个仓库(或者去这个全版本仓库)下载。

在这里插入图片描述
其中CGIncludes文件夹中包含了所有的内置包含文件。DefaultResources文件夹中则包含了一些内置组件或功能所需要的Unity Shader 。例如一些GUI元素使用的Shader;DefaultResourcesExtra则包含了所有Unity中内置的Unity Shader;Editor文件夹用于编辑器面板设置,可以自定义Standard Shader所用的材质面板。当我们想要学习内置着色器的实现或是寻找内置函数的实现时,都可以在这里找到内部实现。

在安装路径下也能够找到CGIncludes文件夹,在PC上的路径是Unity安装路径/Data/CGIncludes/

Unity中一些常用的包含文件

文件名 描述
UnityCG.cginc 包含了最常使用的帮助函数,宏和结构体等
UnityShaderVariables.cginc 在编译Unity Shader时,会被自动包含进来。包含了许多内置的全局变量,如UNITY_MATRIX_MVP等
Lighting.cginc 包含了各种内置的光照模型,如果编写的是Surface Shader的话,会被自动包含进来
HLSLSupport.cginc 在编译UnityShader时,会被自动包含进来。声明了很多用于跨平台编译的宏和定义

其中UnityCG.cginc是我们最常接触的包含文件。例如我们可以直接使用其中预定义的结构体作为顶点着色器的输入和输出,下表给出了一些结构体的名称和包含的变量。

UnityCG.cginc中一些常用的结构体

名称 描述 包含的变量
appdata_base 可用于顶点着色器的输入 顶点位置、顶点法线、第一组纹理坐标
appdata_tan 可用于顶点着色器的输入 顶点位置、顶点切线、顶点法线、第一组纹理坐标
appdata_full 可用于顶点着色器的输入 顶点位置、顶点切线、顶点法线、四组或更多纹理坐标
appdata_img 可用于顶点着色器的输入 顶点位置、第一组纹理坐标
v2f_img 可用于顶点着色器的输出 裁剪空间中的位置、纹理坐标

UnityCG.cginc中一些常用的帮助函数

名称 描述
float3 WorldSpaceViewDir(float4 v) 输入一个模型空间中顶点位置,返回世界空间中从该点到摄像机的观察方向
float3 ObjSpaceViewDir(float4 v) 输入一个模型空间中顶点位置,返回模型空间中从该点到摄像机的观察方向
float3 WorldSpaceLightDir(float4 v) 仅可用于前向渲染中。输入一个模型空间中顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化
float3 ObjSpaceLightDir(float4 v) 仅可用于前向渲染中。输入一个模型空间中顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化
float3 UnityObjectToWorldNormal(float3 normal) 将法线方向从模型空间转换到世界空间
float3 UnityObjectToWorldDir(float3 dir) 将方向矢量从模型空间转换到世界空间
float3 UnityWorldToObjectDir(float3 dir) 将方向矢量从世界空间转换到模型空间

如果可以,建议大家阅读cginc中的源代码理解这些函数的功能是如何实现的。

此外,在Unity中还提供了一些内置的变量可以直接访问,例如时间、光照、雾效和环境光等目的的变量。这些变量大多位于UnityShaderVariables.cginc中,与光照有关的内置变量还会位于Lighting.cginc、AutoLight.cginc等文件中。


Unity提供的CG/HLSL语义

什么是语义

在前文代码中,我们看到了例如SV_POSITION、POSITION、COLOR0等大写的名称。这些是CG/HLSL提供的语义(semantics)。语义实际上就是一个赋给Shader输入和输出的字符串,这个字符串表达了这个参数的含义,让Shader知道从哪读取数据,并把数据输出到哪里。

这些语义在CG/HLSL的流水线中是不可或缺的,他们描述了变量的用途。至于变量本身存储了什么,Shader并不关心。

Unity为了方便对模型数据的传输,对一些语义进行了特别的含义规定,例如,在顶点着色器的输入结构体a2v中用TEXCOORD0来描述texcoord,Unity会识别TEXCOORD0语义,以把模型的第一组纹理坐标填充到texcoord中。需要注意的是,即便语义的名称一样,如果出现的位置不同,含义也不同。例如,TEXCOORD0既可以用于描述顶点着色器的输入结构体a2v,也可以用于描述输出结构体v2f。但在输入结构体a2v中,TEXCOORD0有固定的含义,即把模型的第一组纹理坐标存储在改变量,而在输出结构体v2f中,TEXCOORD0修饰的变量含义则可以由我们来决定。

在DirectX10之后,有了一种新的语义类型,就是系统数值语义(system-value semantics) 。这类语义是以SV开头的,SV代表的含义是系统数值(system -value) 。这些语义在渲染流水线中有特殊的含义,例如在之前的代码中,我们使用SV_POSITION语义去修饰顶点着色器的输出变量pos,那么就表示pos包含了可用于光栅化的变换后的顶点坐标(即齐次裁剪空间中的坐标)。

这些SV开头的语义的变量是不可以随意赋值的,因为流水线需要用他们去完成特定的目的。有时一个变量在不同的Shader里有不同的语义修饰,例如一些shader会使用POSITION而非SV_POSITION。在大多数平台上,POSITION和SV_POSITION,COLOR和SV_Target等等是等价的,但是为了更好的跨平台性,还是建议使用SV语义来准确修饰对应变量。

Unity支持的语义

虽然一些常用语义并没有使用SV开头,但是Unity内部赋予了他们特殊的含义,意味着这些语义在固定阶段有着特殊的作用。

从应用阶段传递模型数据给顶点着色器时Unity支持的常用语义

语义 描述
POSITION 模型空间中的顶点位置,通常是float4类型
NORMAL 顶点法线,通常是float3类型
TARGENT 顶点切线,通常是float4类型
TEXCOORDn 该顶点的纹理坐标,TEXCOORD0表示第一组纹理坐标,以此类推,通常是float2或float4类型,通常一个模型不会使用超过2组纹理坐标
COLOR 顶点颜色,通常是fixed4或float4类型

从顶点着色器传递数据给片元着色器(v2f)时untiy使用的常用语义

语义 描述
SV_POSITION 顶点着色器的输出结果,裁剪空间中的顶点坐标,结构体中必须包含一个该语义修饰的变量
COLOR0 通常用于输出第一组顶点颜色,但不是必须的
COLOR1 通常用于输出第二组顶点颜色,但不是必须的
TEXCOORD0~TEXCOORD7 通常用于输出纹理坐标,但不是必须的

上述中除了SV_POSITION由特殊定义之外,其他语义对变量的含义没有明确需求,

片元着色器输出时Unity支持的常用语义

语义 描述
SV_Target 输出值将会存储在渲染目标(render target)中。等同于COLOR语义,但最好使用SV_Target

如何定义复杂的变量类型

在这里插入图片描述
需要注意:一个语义使用的寄存器只能处理4个浮点数(float),因此,如果我们想要定义矩阵类型,例如float4x4就需要更多空间,一种方法是——将这些变量拆分为多个变量,例如对于float4x4的矩阵类型。我们可以拆分成4个float4类型的变量。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>