一、着色器的分类
Unity中的着色器可以分为三大类
1.固定管线着色器(Fixed Pipeline Shader)->为了兼容老一代GPU而设计的最早的图形学版本和最早的游戏都是基于这个着色器来编写的。
特点是流水线作业,固定指令,不可编程,比较简单,无法自由的编写GPU程序实现不同的渲染。
2.顶点片元着色器(Fragment Shader)->比固定管线要新,功能强大。
特点是可编程着色器,允许开发者自由编写GPU程序来实现不同的渲染功能。缺点是不支持光照。难易程度适中
3.表面着色器->Unity官方极力推荐的着色器。
最新的游戏都是基于这个着色器编写的。
特点是可编程着色器,允许开发者自由编写GPU程序来实现不同的渲染功能,而且支持光照,可以自定义光照模型,自由度最高。
固定管线是在老一代GPU能力比较有限时,对着色器的约束性比较高的一种形态。
前景:为了市场占有率,新的显示支持部分功能特性,未来会逐渐被淘汰。
二、Shader中空间的概念
要游戏中的3D世界到屏幕上绚丽多彩的画面需要进行一系列的空间变换。
物体空间(本地空间)->世界空间->摄像机空间(视图空间)->裁剪空间->标准屏幕空间->窗口空间
1.物体空间:所需要绘制的3D物体所在的原始坐标系所代表的空间,也叫本地空间。
世界坐标->本地坐标方法:
Unity脚本中->transform.worldToLocalMatrix
Unity着色器中->左乘_World2Object矩阵
2.世界空间:物体最终显示的3D场景中的摆放位置对应的坐标所属的坐标系所代表的空间。
本地坐标->世界坐标方法:
Unity脚本中->transform.localToWorldMatrix
Unity着色器中->左乘_Object2World矩阵
3.摄像机空间:物体经过摄像机观察后进入摄像机空间,指的是以摄像机为原点的一个特定的坐标系所代表的空间,在这个坐标系中,摄像机位于原点,视线沿z轴负方向,y轴的方向与摄像机的UP向量方向一致。
本地坐标->摄像机坐标方法:
UNITY_MATRIX_MV矩阵
4.裁剪空间:
什么是视锥体?
对于正交投影来说是一个四边平行于投影方向的四棱柱
对于透视投影来说是一个以近平面为上底,远平面为下底的棱台(当近平面为0时,则是一个以投影中心为顶点的四棱锥)。
什么是裁剪空间?
只有在摄像机空间中,并且位于视锥体内的物体才能被观察到。将摄像机空间内视锥体内的部分独立出来经过处理后形成的空间,就叫做裁剪空间。
物体坐标->摄像机坐标->屏幕空间的方法:
UNITY_MATRIX_MVP
5.标准设备空间:
对裁剪空间执行透视除法后得到的空间。对于OpenGL来讲,标准设备空间三个轴的坐标范围都是-1.0~1.0。
什么是透视除法?
将齐次坐标[x, y, z, w]的4个分量都除以w,结果是[x/w, y/w, z/w, 1],本质上就是对齐次坐标进行了规范化。
6.实际窗口空间:
代表的是设备屏幕上的一块矩形区域,坐标以像素为单位。主要工作是将执行透视除法后的x,y坐标分量转换为实际窗口的xy像素坐标,主要的思路是将标准设备空间的xy平面对应到视口上,将-1.0~1.0内的x,y坐标折算为视口上的像素坐标。
三、顶点片元着色器
顶点片元着色器是可编程着色器,相对于固定管线着色器可以给开发人员更大的发挥空间。但是缺点是不能直接和光照进行交互。
顶点片元着色器的程序使用CG或HLSL来进行编写,嵌入在着色器的渲染通道块中。
CG的代码被编写在CGPROGRAM和ENDCG之间。
1.编译指令:
使用#pragma指令
pragma vertex 将名称为name的函数编译为顶点着色器
pragma fragment 将名称为name的函数编译为片元着色器
pragma fragmentoption
pragma target 要编译成哪个着色器目标
A.顶点变换,决定顶点最终的位置,顶点函数
将(网格,模型)原始顶点数据在顶点函数中经过特别的处理变为已经经过3D渲染管线变
换的可以投影到2D屏幕上的位置确定的顶点。
B.像素着色,决定每一个像素点最终的颜色,片元函数
将顶点函数变换后的顶点进行着色,确定其最终颜色,对所有经过顶点变换后的顶点进
行光栅化(上色)
渲染管线的核心流程:
(1)把3D的顶点变成2D->顶点变换,算出顶点的最终位置->vertex顶点函数功能
(2)给2D屏幕上的顶点着色->顶点光栅化,算出顶点的最终颜色->fragment片元函数功能
片元着色器处理流水线,渲染管线
原始网格顶点数据->顶点函数->最终位置顶点数据->片元函数->顶点的颜色值
特别注意的:
每一个Shader代码文件都必须包含一个顶点程序或一个片元程序或两个都有。
必须使用#pragma vertex或#pragma fragment或两个都使用。
2.顶点数据结构体:
顶点着色器中顶点数据必须以一个结构体的形式提交给CG/HLSL顶点程序,下面介绍一些常用的顶点数据结构体:
(1)appdata_base:由顶点位置,法线以及一个纹理坐标组成
vertex->顶点坐标
normal->法线
texcoord->纹理坐标
(2)appdata_tan:由顶点位置,切线,法线以及一个纹理坐标组成
vertex->顶点坐标
tangent->切线
normal->法线
texcoord->纹理坐标
(3)appdata_full:由顶点位置,切线,法线,两个纹理坐标以及颜色组成
vertex->顶点坐标
tangent->切线
normal->法线
texcoord->纹理坐标1
texcoord1->纹理坐标2
color->颜色
3.内置变化矩阵
(1)顶点着色器的流程:传入顶点数据->顶点着色器->顶点运算,变换矩阵变换后的位置->返回
(2)片元着色器的流程:顶点着色器返回的最终值->片元着色器->计算颜色->返回最终颜色
Unity着色器内置的常用变换矩阵有如下几个:
UNITY_MATRIX_MVP -> 基本变换矩阵x摄像机矩阵x投影矩阵
UNITY_MATRIX_MV -> 基本变换矩阵x摄像机矩阵
UNITY_MATRIX_V -> 摄像机矩阵
UNITY_MATRIX_P -> 投影矩阵
UNITY_MATRIX_VP -> 摄像机矩阵x投影矩阵
_Object2World -> 本地坐标系转换为世界坐标系
_World2Object -> 世界坐标系转换为本地坐标系
4、什么是GrabPass?
GrabPass是一种特殊的pass类型。当物体将要被绘制时,它抓取屏幕内容并绘制到一张texture里。
GrabPass的特点?
运算开销较大,不如AlphaBlend等指令。能用AlphaBlend实现的就不用GrabPass。
GrabPass的使用方式
(1)GrabPass{} 抓取当前屏幕内容。
(2)GrabPass{"TextureName"}将抓取屏幕内容并保存至一张texture里。每帧只为第一次使用这张纹理的物体做一次,这种更高效
UnityObjectToClipPos的作用?
其作用等同于UNITY_MATRIX_MVP,但是如果直接使用UNITY_MATRIX_MVP,会引入一个额外的矩阵乘法运算,所以推荐使用UnityObjectToClipPos / UnityObjectToViewPos函数,它们会把这一次额外的矩阵乘法优化为向量-矩阵乘法。
tex2D函数的作用?
tex2D是纹理采样函数,可以通过纹理图像素信息与纹理坐标计算该位置的顶点的纹理像素点颜色。
tex2D(texture,uv)第1个参数为纹理图,第2个参数为纹理坐标,返回值为指定纹理坐标位置的颜色值
四、****表面着色器:
顶点片元着色器最大的缺点是不能直接和光照交互。为了让开发人员更方便快捷地处理光照,Unity提供了表面着色器。表面着色器代码也是使用CG或HLSL语言编写的。
着色器的三种形态中,表面着色器比固定管线着色器更加灵活,又比顶点片元着色器更加方便地处理光照,所以在游戏开发中最常用的是表面着色器。下面介绍表面着色器的基础知识。
1.编译指令
表面着色器与其他任何着色器一样放置于CGPROGRAM….ENDCG块中。区别是必须将其放置于子着色器块中,而不能放在通道中,表面着色器自身会编译为多个通道。它使用#pragma surface指令来表明它是个表面着色器。
pragma surface指令格式如下:
pragma surface[optionalparams]
这其中:
surfaceFunction为表面着色器函数名称,通过该指令告诉编译器cg代码中surfaceFunction函数为表面着色器函数。
lightModel为光照模型。通过该指令告诉编译器这个表面着色器使用哪个光照模型。Unity内置的光照模型为Lambert(漫反射)和BlinnPhong(高光),也可以自定义光照模型。
Optionalparams为可选参数。可用的可选参数如下表所示:
2.输入输出参数结构体
表面着色器函数可以有两个参数,其中一个参数为Input结构体,用于为表面着色器函数输入所需的纹理坐标和其他数据。另一个参数为SurfaceOutput结构体,需在表面着色器函数中写入相应的值,用于输出数据。
//如果属性中有纹理,则需要写这个结构体
struct Input {
// uv+变量名
float2 uv_MainTex;
};
//需要对Properties中的属性重写定义
//重新设置类型
//2D == sampler2D
//Range == half
//color == fixed4
sampler2D _MainTex;
half _Glossiness;
half _Metallic;
fixed4 _Color;
fixed4 _AlphaColor;
Input结构体中的纹理坐标必须在纹理名称前面加上”uv”或”uv2”,带”uv”的纹理坐标为物体所带的第一个纹理坐标,如果物体带有第二个纹理坐标,则带”uv2”的纹理坐标为物体所带的第二个纹理坐标。其他可用的数据如下表所示:
Input结构体不但可以包含上面所列的数据,也可以包含自定义的数据,用于从顶点函数传递数据给表面着色器。表面着色器的输出结构体SurfaceOutput是内置定义好的,只需在表面着色器函数中为需要的变量赋值就可以了。标准的表面着色器输出结构体如下:
也可以自定义表面着色器的输出结构体,但自定义的结构体必须包括SurfaceOutput结构体的所有变量,然后可以添加自己需要的变量用于从自定义光照模型函数传递数据给表面着色器函数。
下面用一段自定义表面着色器材质的基础Shader代码来解释一下表面着色器的结构:
Shader "Custom/BaseForm2" {
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 //LOD数值
CGPROGRAM
#pragma surface surf Standard fullforwardshadows //表面着色器编译指令
#pragma target 3.0 //着色器编译目标
sampler2D _MainTex; //2D纹理属性
struct Input { //定义输入参数结构体
float2 uv_MainTex; //纹理UV坐标
};
half _Glossiness; //定义高光系数属性
half _Metallic; //定义金属材质系数属性
fixed4 _Color; //定义主颜色属性
void surf (Input IN, inout SurfaceOutputStandard o) { //表面着色器函数
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; //根据UV坐标从纹理提取颜色
o.Albedo = c.rgb; //设置颜色
o.Metallic = _Metallic; //设置金属材质系数
o.Smoothness = _Glossiness; //设置高光系数
o.Alpha = c.a; //设置透明度
}
ENDCG
}
FallBack "Diffuse" //降级着色器
}
这其中"RenderType"="Opaque"为子着色器标签的一组值,详细的解释如下图所示:
3.自定义光照模型
编写表面着色器就是描述一个表面的属性(如反射率,颜色,法线等),并由光照模型完成光照交互的计算。系统内置了Lambert(漫反射)和BlinnPhong(高光)两个光照模型。有时也需要开发自定义光照模型。
自定义的光照模型是由名称为”Lighting”开头的函数实现的。自定义光照模型函数的声明有以下几种形式,用于不同的需求。
(1).half4 Lighting
其在正向渲染路径中用于非与视线方向相关的光照模型(例如,漫反射)。
(2).half4 Lighting
其在正向渲染路径中用于与视线方向相关的光照模型。
(3).half4 Lighting
其用于延时光照路径中的光照模型。
这其中,SurfaceOutput结构体用于和表面着色器函数传输数据。这个结构体也可以自己定义,但必须与表面着色器函数的输出结构体相同。”lightDir”参数为点到光源的单位向量,”viewDir”参数为点到摄像机的单位向量,”atten”参数为光源的衰减系数。
光照模型函数的返回值为经过光照计算的颜色值。下面通过一个带自定义光照模型的表面着色器来详细介绍自定义光照模型:
Shader "Custom/BaseForm3" {
Properties {
_Color ("Color", Color) = (1,1,1,1) //主颜色数值
_MainTex ("Albedo (RGB)", 2D) = "white" {} //2D纹理数值
_Shininess ("Shininess ", Range(0,10)) = 10 //镜面反射系数
}
SubShader {
CGPROGRAM
#pragma surface surf Phong //表面着色器编译指令
sampler2D _MainTex; //2D纹理属性
fixed4 _Color; //主颜色属性
float _Shininess; //镜面反射系数属性
struct Input {
float2 uv_MainTex; //uv纹理坐标
};
//光照模型函数
float4 LightingPhong(SurfaceOutput s, float3 lightDir,half3 viewDir, half atten){
float4 c;
float diffuseF = max(0,dot(s.Normal,lightDir)); //计算漫反射强度
float specF;
float3 H = normalize(lightDir+viewDir); //计算视线与光线的半向量
float specBase = max(0,dot(s.Normal,H)); //计算法线与半向量的点积
specF = pow(specBase,_Shininess); //计算镜面反射强度
c.rgb = s.Albedo * _LightColor0 * diffuseF *atten + _LightColor0*specF;
//结合漫反射光与镜面反射光计算最终光照颜色
c.a = s.Alpha;
return c; //返回最终光照颜色
}
void surf (Input IN, inout SurfaceOutput o) { //表面着色器函数
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;//根据UV坐标从纹理提取颜色
o.Albedo = c.rgb; //设置颜色
o.Alpha = c.a; //设置透明度
}
ENDCG
}
FallBack "Diffuse" //降级着色器
}
4.顶点变换函数
顶点变化函数可以修改顶点着色器中的输入顶点数据以及为表面着色器函数传递顶点数据。这可用于程序性动画,沿法线的挤压等效果。使用表面着色器编译指令vertex:
void
其用于只修改顶点着色器中的输入顶点数据。
half4
其用于修改顶点着色器中的输入顶点数据以及为表面着色器函数传递数据。
其中inout类型的结构体使用了顶点数据结构体,用于给顶点函数输入顶点数据。out类型的结构体为表面着色器中使用的输入结构体,用于顶点变换函数为表面着色器函数传递数据。下面我们通过一个Surface Shader案例来实现顶点变换函数实现吹气膨胀效果从而来详细的了解一下顶点函数。
Shader "Custom/BaseForm4" {
Properties {
_MainTex ("Texture", 2D) = "white" {} //2D纹理数值
_Amount ("Extrusion Amount", Range(0,0.1)) = 0.05 //膨胀系数数值
}
SubShader {
CGPROGRAM
#pragma surface surf Lambert vertex:vert //表面着色器编译指令
struct Input { //Input结构体
float2 uv_MainTex; //uv纹理坐标
};
float _Amount; //定义膨胀系数属性
sampler2D _MainTex; //定义2D纹理
void vert (inout appdata_base v) { //顶点变换函数
v.vertex.xyz += v.normal * _Amount; //通过法线挤压实现充气的效果
}
void surf (Input IN, inout SurfaceOutput o) { //表面着色器函数
o.Albedo=tex2D (_MainTex, IN.uv_MainTex).rgb; //从纹理提取颜色为漫反射颜色赋值
}
ENDCG
}
Fallback "Diffuse" //降级着色器
}
5.最终颜色修改函数
最终颜色修改函数用于修改表面着色器的最终颜色。这可用于绘制物体表面的最终调色。使用表面着色器编译指令finalcolor:
void
其中,Input结构体用于顶点变换函数为最终颜色修改函数传递数据,SurfaceOutput结构体用于为最终颜色修改函数传输数据,inout类型的”color”参数为最终颜色修改函数输出最终颜色。下面通过最终颜色修改函数实现调色的表面着色器来详细介绍最终颜色修改函数。
Shader "Custom/BaseForm5" {
Properties {
_MainTex ("Texture", 2D) = "white" {} //2D纹理数值
_ColorTint ("Tint", Color) = (1.0, 0.6, 0.6, 1.0) //调色数值
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert finalcolor:mycolor //表面着色器编译指令
struct Input { //Input结构体
float2 uv_MainTex; //uv纹理坐标
};
fixed4 _ColorTint; //调色数值属性
sampler2D _MainTex; // 2D纹理属性
void mycolor(Input IN, SurfaceOutput o, inout fixed4 color){ //最终颜色修改函数
color *= _ColorTint; //通过调色数值修改最终颜色
}
void surf (Input IN, inout SurfaceOutput o) { //表面着色器函数
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; //从纹理提取颜色为漫反射颜色赋值
}
ENDCG
}
Fallback "Diffuse" //降级着色器
}