《Unity Shader入门精要》总结 #第五章 开始Unity Shader学习

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

1.1 顶点/片元的基本结构

包含Shader、Properties、SubShader、Fallback等语义块缓存

Shader "MyShaderName"{
    Properties{
        //属性
    }
    SubShader{
        //针对显卡A的SubShader
        Pass{
            //设置渲染状态和标签

            //开始CG代码片断
            CGPROGRAM
            //该代码片断的编译指令
            #pragma vertex vert
            #pragma fragment frag

            //CG代码写在这里
            ENDCG
            //其余设置
        }
        //其余须要的Pass
    }
    SubShader{
        //针对显卡B的SubShader
    }

    //上述SubShader都失败后用于回调的Unity Shader
    Fallback "VertexLit"
}

最重要的是Pass语义块app

Shader "Unity Shaders Book/Chapter5/SimpleShader" {
	SubShader{
		Pass{
			CGPROGRAM
            //如下两条告诉Unity哪一个函数包含顶点着色器代码,哪一个函数包含片元着色器代码
            //#pragma vertex name
            //#pragma fragment name
			#pragma vertex vert
			#pragma fragment frag

            //v包含顶点位置,经过POSITION语义定义,返回值float4,POSITION和SV_POSITION均为                    
            //CG/HLSL语义。如POSTION告诉Unity将模型顶点坐标填充到参数v中,SV_POSITION告诉         
            //Unity顶点着色器的输出是裁剪空间的顶点坐标
			float4 vert(float4 v:POSITION) : SV_POSITION{
				return UnityObjectToClipPos (v);
			}

            //SV_Target是HLSL的语义,告诉渲染器用户的输出颜色存储到一个渲染目标中,这里输出到 
            //默认的帧缓存中,返回一个表示白色的fixed4类型。
			fixed4 frag() : SV_TARGET{
				return fixed4(1.0,1.0,1.0,1.0);
			}

			ENDCG
		}
	}
}

1.2 模型数据来源

上面例子中POSITION获得了模型顶点位置,当须要模型上每一个顶点的纹理坐标和法线方向时。纹理坐标访问纹理,法线用于计算光照。此次新的参数将为一个结构体。函数

Shader "Unity Shaders Book/Chapter5/SimpleShader" {
	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{
                //使用v.vertex访问模型空间顶点坐标
				return UnityObjectToClipPos (v.vertex);
			}
			fixed4 frag() : SV_TARGET{
				return fixed4(1.0,1.0,1.0,1.0);
			}

			ENDCG
		}
	}
}

对于顶点着色器的输出,语义有:POSITION、TANGENT、NORMAL、TEXCOORD0、TEXCOORD一、TEXCOORD二、TEXCOORD三、COLOR,在Unity中由使用该材质的Mesh Render组建提供,在每帧Draw Call调用时发送。性能

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

Shader "Unity Shaders Book/Chapter5/SimpleShader" {
	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) : SV_POSITION{
                //声明输出结构
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, 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{
                //插值后的i.color显示到屏幕上
				return fixed4(i.color,1.0);
			}

			ENDCG
		}
	}
}

顶点着色器的输出结构中必须包含一个变量,其语义是SV_POSITION,不然渲染器将没法获得裁剪空间中的顶点坐标,也就没法把顶点渲染到屏幕上。COLOR0语义中的数据可由用户自行定义,但通常为存储颜色,如逐顶点漫反射颜色或逐顶点的高光反射颜色。相似语义还有COLOR1等。优化

vertex shader逐顶点调用,fragment shader逐片调用。FS的输入是VS的输出进行插值后的结果spa

1.4 如何使用属性

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unity Shaders Book/Chapter5/SimpleShader" {
	Properties{
		_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属性控制输出颜色,即color tint
				c*=_Color.rgb;
				return fixed4(c,1.0);
			}

			ENDCG
		}
	}
}

结果:调试

Color,初始值白色。code

ShaderLab中属性类型和CG中变量类型间匹配关系:orm

ShaderLab属性类型 CG变量类型
Color, Vector float4, half4, fixed4
Range, Float float, half, fixed
2D sampler2D
Cube samplerCUbe
3D sampler3D
uniform fixed4 _Color;

用于提供关于该变量的初始值是如何指定和存储的相关信息,可省略对象

三、Unity提供的内置文件和变量

CGPROGRAM
#include "UnityCG.cginc"
ENDCG

CGIncludes中主要的包含文件及其主要用处

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

还有UnityStandardBRDF.cginc、UnityStandardCore.cginc等,用于实现基于物理的渲染

能够直接使用UnityCG.cginc中预约义的结构体做为顶点着色器的输入和输出

经常使用结构体

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

帮助函数

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

访问时间、光照、雾效和环境光等变量大位于UnityShaerVariables.cginc,与光照有关的还会位于LIghting.cginc、AutoLight.cginc等文件中

四、Unity提供的CG/HLSL语义

4.1 什么是语义

赋给Shader输入和输出的字符串,让Shader知道在哪里读取数据并输出到哪里。一般状况下输入输出变量不须要特别的意义,可自行决定变量的用途。上面的例子利用COLOR0语义描述color但变量自己存储什么Shader并不关心

语义出现位置不一样含义也不一样。如,TEXCOORD0既可用于描述顶点着色器输入结构a2f也可用于描述输出结构v2f。a2f中把模型第一组纹理坐标存储在该变量里,在v2f中TEXCOORD0修饰的变量含义由咱们来定义

系统数值语义以SV开头。pos包含了可用于光栅化的变换后的顶点坐标,用这些语义描述的变量不可随意赋值。SV_POSITION修饰的变量通过光栅化后显示在屏幕上。在某些特定平台上必须使用SV_POSITION、COLOR和SV_Target,为了更好跨平台性,有特殊含义的变量最好以SV开头的语义进行修饰。

4.2 Unity支持的语义

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

语义 描述
POSITION 模型空间顶点位置,一般float4
NORMAL 顶点法线,一般float3
TANGENT 顶点切线,一般float4
TEXCOORDn 顶点纹理坐标,TEXCOORD0表示第一组,一般float2/float4
COLOR 顶点颜色,一般float4/fixed4

TEXCOORDn中n数目是Shader Model有关,如Shader Model 2/3中n=8(默认编译到2版本),在Shader Model 4/5中n=16。一般状况下一个模型纹理坐标组数不超过2,每每使用TEXCOORD0/1。在appdata_full中最多使用六个。

顶点着色器传递数据到片元着色器阶段经常使用语义

语义 描述
SV_POSITION 裁剪空间中的顶点坐标,结构体中必须包含用该语义修饰的变量,等同于DirectX 9中的POSITION,最好用SV_POSITION
COLOR0 输出第一组顶点颜色,非必需
COLOR1 输出第二组顶点颜色,非必需
TEXCOORD0~TEXCOORD7 输出纹理坐标,非必需

自定义数据从顶点着色器到片元着色器通常用TEXCOORD0等。只有SV_POSITION有特殊含义。

片元着色器输出经常使用语义

语义 描述
SV_Target 输出值将会存储到渲染目标中,等同于DirectX 9中的COLOR语义,但最好使用SV_Target

4.3 定义复杂的变量类型

利用结构体定义。

一个语义可使用的寄存器只能处理4个浮点值,若想要定义矩阵类型,就讲这些变量拆分红多个变量,好比float4*4矩阵类型就能够拆分红4个float4类型变量,每一个变量存储矩阵中的一行数据

4.4 Debug建议

-使用假色彩图像

将调试的变量映射到[0,1]之间。颜色中大于一的数值会被设置为1,任何小于0的数值会被设置为0。

利用假色彩图像方式可视化法线、切线、纹理、坐标、顶点颜色。

Shader "Unity Shaders Book/Chapter5/False Color" {
	SubShader{
		Pass{
			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

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

			v2f vert(appdata_full v){
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);

                //可视化法线方向
				o.color = fixed4(v.normal * 0.5 + fixed3(0.5,0.5,0.5), 1.0);
                //可视化切线方向
				o.color = fixed4(v.tangent * 0.5 + fixed3(0.5,0.5,0.5), 1.0);
                //可视化副切线方向
				fixed3 binormal = cross(v.normal, v.tangent.xyz)*v.tangent.w;
				o.color = fixed4(binormal * 0.5 + fixed3(0.5,0.5,0.5), 1.0);
                //可视化第一组纹理坐标
				o.color = fixed4(v.texcoord.xy , 0.0,1.0);
                //可视化第二组纹理坐标
				o.color = fixed4(v.texcoord1.xy , 0.0,1.0);
                //可视化第一组纹理坐标的小数部分
				o.color = frac(v.texcoord);
				if(any(saturate(v.texcoord) - v.texcoord)){
					o.color.b = 0.5;
				}
				o.color.a = 1.0;
                
                //可视化第二组纹理坐标的小数部分
				o.color = frac(v.texcoord1);
				if(any(saturate(v.texcoord1) - v.texcoord1)){
					o.color.b = 0.5;
				}
				o.color.a = 1.0;
                
                //可视化顶点颜色
				//o.color = v.color;

				return o;
			}
            

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

			ENDCG
		}
	}
}

-Visual Studio

-帧调试器

查看渲染该帧的各类渲染事件,包含DrawCall序列。单击Knot的深度图渲染事件,Game视图显示该事件效果,Hierarchy视图中高亮显示Knot对象,帧调试器的右侧窗口显示该事件细节

五、渲染平台的差别

5.1 纹理坐标的差别

开启抗锯齿后Unity渲染获得屏幕图像,硬件抗锯齿,获得渲染纹理。但在抗锯齿下同时须要处理多张渲染图像,如须要同时处理屏幕图像和法线纹理,则在竖直方向朝向可能不一样。此时须要本身在顶点着色器中翻转某些渲染纹理(如深度纹理或其余由脚本传递来的纹理)的纵坐标,使之复合DirectX平台规则,如:

#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0)
    uv.y = 1 - uv.y;
#endif

UNITY_UV_STARTS_AT_TOP判断当前平台是否为DirectX平台,在这平台上开启抗锯齿后主纹理纹素大小在竖直方向上将变成负值以方便咱们对主纹理进行正确采样,能够经过判断_MainTex_TexelSize.y是否小于0检验是否开启抗锯齿。

5.2 Shader的语法差别

float4 v = float4(0.0) -> float4 v = float4(0.0,0.0,0.0,0.0)

须要在顶点着色器中访问纹理则须要使用tex2Dlod函数,不支持tex2D,没法获得UV偏导,如tex2Dlod(tex,float4(uv,0,0))以及#pragma  target 3.0

5.3 Shader的语义差别

-利用SV_POSITION描述顶点着色器输出的顶点位置

-使用SV_Target描述片元着色器输出的颜色

六、Shader整洁之道

6.1 float、half仍是fixed

float-32位 half-16位 fixed-11位

尽量使用精度较低的类型,能够优化Shader性能,使用fixed存储颜色和单位矢量,更大范围数据能够选择half类型,最差状况下选择float。

6.2 规范语法

6.3 避免没必要要的计算

Shader内(尤为片元着色器)进行大量计算,会致使临时寄存器数目或指令数目超过当前可支持数目。不一样Shader Target、不一样着色器阶段可以使用的临时寄存器和指令数目都是不一样的。

指令 描述
#pragma target 2.0 默认Shader Target等级,至关于Direct3D 9的Shader Model 2.0
#pargma target 3.0 至关于Direct3D 9的Shader Model 3.0
#pargma target 4.0 至关于Direct3D 10的Shader Model 4.0,目前只在DirectX 11和XboxOne/PS4支持
#pargma target 5.0 至关于Direct3D 11的Shader Model 5.0,目前只在DirectX 11和XboxOne/PS4支持

全部相似OpenGL的平台支持到Shader Model 3.0。

Shader Model是微软提供的规范,它们决定了Shader中的各个特性和能力,体如今Shader可使用的运算指令数目和寄存器个数等。

6.4 慎用分支循环

计算流水线上端移动,好比片元着色器到顶点着色器,或在CPU进行预运算再传递给Shader。

-判断分支语句的条件变量最好是常数,即在Shader运行过程当中不变化;

-每一个分支的操做指令数尽量少;

-分支的嵌套层数尽量少;

6.5 不要除以0