Unity中使用Cg语言如何编写Shader?

这一篇是unity之Shader的基础篇之基础,更多的是一些概念。该篇将分以下几个部分:

1、什么是Shader?
2、GPU和Cg语言
3、Unity中使用Cg语言如何编写Shader?
1、什么是Shader?
1.1、Shder的概念:Shader即着色器,是一款运行在GPU上的程序,用以对三维物体进行着色处理,光与影的计算,纹理颜色的呈现等,从而将游戏引擎中一个个作为抽象的几何数据存在的模型、场景和特效,以和真实世界类似的光与影的形式呈现于人们眼中。
1.2、Shader和Material的关系:Shader负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合起来输出,绘图单元可以依据这个输出来讲图像绘制到屏幕上。输入的贴图或者颜色,加上对应的Shader以及Shader的特定的参数设置,将这些打包存储在一起就得到了一个Material。
姑且可以这么认为Shader就相当于高级语言里面的一个类,当我们需要对某一具体物体使用此Shader是,我们就需要先实例化次shader,从而得到一个它的对象,其实Mataerial就相当于Shader的一个对象。这样我们就能理解在Unity中同样的材质球在被修改后,其他物体挂载了此材质球也会被修改。而如果是使用相同Shader新建的别名材质球,就不会受影响。
2、GPU和Cg语言
2.1、Gpu图形绘制管线分为三个主要阶段:
  1. 应用程序阶段:使用高级编程语言(C、C++、Java等)进行开发,主要和CPU进行打交道,诸如碰撞检测、场景图建立、空间八叉树更新、视锥裁剪等景等经典算法在此阶段执行
  2. 几何阶段:主要负责顶点坐标变换、光照、裁剪、投影以及屏幕映射,该阶段基于GPU进行运算,在该阶段之后得到了经过变化和投影之后的顶点坐标、颜色以及纹理坐标——《实时计算机图像学》
  3. 光栅阶段:基于几何阶段的输出数据,为像素正确配色,以便绘制完整的图形,该阶段进行的都是单个像素的操作,每个像素的信息存储在颜色缓冲器(Color buffer或者Frame buffer)中,光栅化得到的屏幕坐标值通常都是浮点型的而像素都是整数表示,通常最后绘制到屏幕上的都是这两个整数点之间进行插值得到线段上某些点上。

(光栅化:将几何图元变为二维图像的过程)
2.2、GPU上的两个组件:Vertex Program和Fragment Program

  1. Vertex Program:从GPU中提取图元信息(顶点位置、法向量、纹理坐标等)完成顶点坐标空间转换,法线向量空间转换光照计算等操作。
  2. Fragment Program:将Vertex Program的输出作为输入对每个片段的颜色进行计算,最后将处理后的数据送光栅操作模块进行光栅化。
  3. 什么是片段,它和像素有什么不一样?:片段其实就是所有的三维顶点在光栅化之后得到的数据集合,这些数据没有经过深度值的比较,而在屏幕上显示的像素都是经过深度值比较的。

(Fragment Program还有一个突出的特点就是拥有检索纹理的能力,纹理其实就是数组)

2.3、坐标空间
屏幕是二维的,GPU所需要的就是将三维的数据绘制到二维的屏幕上,这样就有坐标空间的转换。根据顶点坐标变换的先后顺序,主要有如下几个坐标空间
  1. Object Space,模型坐标空间:这个过程才有真正意义上的投影,投影主要有两种方法:正投影(也称为平行投影)和透视投影。而裁剪是一个较大的概念,为了减少需要绘制的顶点个数,而识别指定区域内或区域外的图形部分的算法都称之为裁剪,裁剪算法包括:视域剔除、背面剔除、遮挡剔除和视口裁剪。
  2. World Space,世界坐标空间:顶点法向量的计算在此过程
  3. Eye Space,观察坐标空间,在Unity中即为虚拟摄像机坐标空间:以虚拟摄像机为原点,由视线方向、视角和远近平面共同组成一个梯形的三维空间
  4. Clip and Project Space,屏幕坐标空间

并非是先裁剪再投影,因为在不规则的物体中进行裁剪并非容易的事情,裁剪被单独安排在一个单位立方体中进行,这个立方体被称为CVV,CVV的近平面的X,Y坐标对应屏幕像素坐标,Z坐标则代表画面像素深度。视点坐标到屏幕坐标由三部分组成:用透视变换矩阵把顶点从视锥体中变换到裁剪空间的CVV中、在CVV进行图元裁剪、屏幕映射。

2.3、Cg语言
2.3.1、Cg支持7中数据类型:
  1. float,32位浮点数据
  2. half,16位浮点数据
  3. int,32位整形数据
  4. fixed,12位定点数
  5. bool,布尔数据
  6. sampler*,纹理对象的句柄:sampler、sampler1D,sampler2D,sampler3D,samplerCUBE,samplerRECT
  7. string,字符类型

2.3.2、内置的向量类型是基于上述数据类型

  1. float4为float类型的4元向量,bool3为bool类型的三元向量,向量最长不能超过4元
  2. Cg中向量、矩阵与数据是完全不同的、向量和矩阵是内置的数据类型,而数据是一种数据结构,在其他高级语言中数组,向量和矩阵都是一种数据结构

2.3.3、Cg表达式与控制语句:关系操作符、逻辑操作符、数学操作符、位移操作符以及Swizzle操作符:

float4(a, b, c, d).xyz 等价于 float3(a, b, c)

float4(a, b, c, d).xyy 等价于 float3(a, b, b)

float4(a, b, c, d).wzyx 等价于 float4(d, c, b, a)

float4(a, b, c, d).w 等价于 float d

中float a 和float1 a是基本等价的,两者可以进行类

型转换;float、bool、half等基本类型声明的变量也可以使用swizzle操作符。例

如:

float a = 1.0;

float4 b = a.xxxx;

2.3.4、Cg语言的其他一些特性

  1. 数组形参:Cg中不存在指针机制,数组作为函数形参传递的是数组的完全拷贝注意:形参数组不必指定长度,如果指定了长度在调用函数是实参数组的长度和形参数组的长度必须一致,最好不要指定长度。
  2. CG函数重载方式和C++基本一致
  3. 由于着色程序分为定点程序很片段程序,两者对应着图形流水线上的不同阶段,所以两个程序各有且只有一个入口函数,当程序今夕编译时必须要指定入口函数,除非入口函数为main.
  4. 如何确定定点和片段程序的入口函数?顶点程序:接收应用程序传递的顶点数据(通常位于模型坐标空间)进行坐标空间转换和光照处理,输出投影坐标和计算得到的光照颜色片段程序:接收从顶点程序输出的数据并进行像素颜色计算。
  5. 所以通常通过观察程序的输入输出语义绑定就可以区分函数对应的是顶点程序还是片段程序

2.3.5、齐次坐标的重要性齐次坐标的本质是什么?

1、两条平行线是否可以相交一点?

在欧氏几何空间,同一平面的两条平行线不能相交,这是我们都熟悉的一种场景。
然而,在透视空间里面,两条平行线可以相交,例如:火车轨道随着我们的视线越来越窄,最后两条平行线在无穷远处交于一点。

2、为什么叫齐次坐标?

简而言之,齐次坐标就是用N+1维来代表N维坐标

3、齐次坐标的本质是什么?

我们可以在一个2D笛卡尔坐标末尾加上一个额外的变量w来形成2D齐次坐标,因此,一个点(X,Y)在齐次坐标里面变成了(x,y,w),并且有
X = x/w

Y = y/w

例如,笛卡尔坐标系下(1,2)的齐次坐标可以表示为(1,2,1),如果点(1,2)移动到无限远处,在笛卡尔坐标下它变为(∞,∞),然后它的齐次坐标表示为(1,2,0),因为(1/0, 2/0) = (∞,∞),我们可以不用”∞”来表示一个无穷远处的点了

证明两条直线可以相交:

3、Unity中如何使用Cg编写Shader

3.1、Shader程序的基本结构:首先是一些属性定义,用来指定这段代码将有哪些输入。接下来是一个或者多个的子着色器,在实际运行中,哪一个子着色器被使用是由运行的平台所决定的。子着色器是代码的主体,每一个子着色器中包含一个或者多个的Pass。在计算着色时,平台先选择最优先可以使用的着色器,然后依次运行其中的Pass,然后得到输出的结果。最后指定一个回滚,用来处理所有Subshader都不能运行的情况(比如目标设备实在太老,所有Subshader中都有其不支持的特性)。

3.2,Shader的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Shader"Cg basic shader"
{
// 定义Shader的名字
    SubShader{// Unity会选择最适合GPU的subshader块
        Pass{// 一个shader会有多个Pass块
            CGPROGRAM// 开始Unity的shader
#pragma vertex vert //定义一个顶点程序,名为vert
#pragma fragment frag//定义一个片段程序,名为frag
    float4
vert(float4 vertexPos : POSITION) : SV_POSITION
    {
                returnmul(UNITY_MATRIX_MVP,
vertexPos);
//这行代码的作用是使用Unity内置的矩阵UNITY_MATRIX_MVP将顶点输入参数vertexPos进行转换,并将转换后的值作为返回顶点程序的输出参数,片段程序的输入参数
    }
    float4
frag(
void)
: COLOR
// f片段程序
    {
        returnfloat4(1.0,
0.0, 0.0, 1.0);)
    //该片段程序返回一个片段输出参加(也即语义COLOR),该代码的意思是设置一个不透明的红色((red = 1, green = 0, blue = 0, alpha = 1))
    }
ENDCG// here ends the part in Cg
}
    }
}