Unity Shader技术入门(2)

一、着色器的分类

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> 将名称为name的函数编译为顶点着色器

pragma fragment <name> 将名称为name的函数编译为片元着色器

pragma fragmentoption <option> 添加option到已编译的OpenGL片元程序

pragma target <name> 要编译成哪个着色器目标

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<surfaceFunction><lightModel>[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<Name>(SurfaceOutput s, half3 lightDir, half atten)

其在正向渲染路径中用于非与视线方向相关的光照模型(例如,漫反射)。

(2).half4 Lighting<Name>(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)

其在正向渲染路径中用于与视线方向相关的光照模型。

(3).half4 Lighting<Name> _PrePass(SurfaceOutput s, half4 light)

其用于延时光照路径中的光照模型。

这其中,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:<Name>,其中”Name”为顶点函数的名称。顶点函数的声明有以下几种形式,用于不同的需求。

void <Name> (inout appdata_full v)。

其用于只修改顶点着色器中的输入顶点数据。

half4 <Name>(inout appdata_full v, out Input o)。

其用于修改顶点着色器中的输入顶点数据以及为表面着色器函数传递数据。

其中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:<Name>,其中”Name”为最终颜色修改函数的名称。最终颜色修改函数的声明形式如下。

void <Name> (Input IN, SurfaceOutput o, inout fixed4 color)

其中,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" //降级着色器

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335