引言
 

shader到底是干什么用的?shader的工作原理是什么? 

其实当我们对这个问题还很懵懂的时候,就已经开始急不可耐的要四处搜寻有关shader的资料,恨不得立刻上手写一个出来。但看了一些资料甚至看了不少cg的语法之后,我们还是很迷茫,UNITY_MATRIX_MVP到底是个什么矩阵?它和v.vertex相乘出来的又是什么玩意?当这些问题困扰我们很久之后,我们才发现,原来我们是站在浮沙上筑高台,根基都没有打牢当然不可能盖得起高楼大厦了。

那根基是什么呢?大牛曰,计算机图形学。 

shader中文名叫着色器,顾名思义,它的作用可以先简单理解为给屏幕上的物体画上颜色。而什么东西负责给屏幕上画颜色?当然是GPU,所以我们写shader的目的就是告诉GPU往屏幕哪里画、怎么画。说到这其实大家应该很明白了,如果我们连GPU的工作原理都不知道,何谈指挥它?

说到计算机图形学,包括我在内很多同学都非常害怕它,因为里面包含了各种艰深的理论、变换,大量的公式什么的。其实我们大可不必一开始就吓倒自己,先从基本概念开始,慢慢来,总有一天我们也会成为大牛~!

最后,这篇文章不算是原创,最多算是摘要+读后感,很多概念性文字都是我从书里搬过来后再加上自己的理解,算是和大家一起学习,有理解不当之处还请多多指教。 

废话不多说,让我们来进入第一章的学习,
GPU的渲染管线。
 



正文
 

所谓GPU的渲染管线,听起来好像很高深的样子,其实我们可以把它理解为一个流程,就是我们告诉GPU一堆数据,最后得出来一副二维图像,而这些数据就包括了”视点、三维物体、光源、照明模型、纹理”等元素。

在各种图形学的书中,渲染管线主要分为三个阶段:应用程序阶段、几何阶段、光栅阶段。 


1,应用程序阶段。 

这个阶段相对比较好理解,就比如我们在Unity里开发了一个游戏,其实很多底层的东西Unity都帮我们实现好了,例如碰撞检测、视锥剪裁等等,这个阶段主要是和CPU、内存打交道,在把该计算的都计算完以后,在这个阶段的末端,这些计算好的数据(顶点坐标、法向量、纹理坐标、纹理)就会通过数据总线传给图形硬件,作为我们进一步处理的源数据。


2,几何阶段。 

主要负责顶点坐标变换、光照、裁剪、投影以及屏幕映射,改阶段基于GPU进行运算,在该阶段的末端得到了经过变换和投影之后的顶点坐标、颜色、以及纹理坐标。简而言之,几何阶段的主要工作就是“
变换三维顶点坐标”
和“
光照计算
”。

问题随之而来,为什么要变换顶点坐标?我是这么理解的,比如你有一个三维游戏场景,场景中的每个模型都可以用一个向量来确定它的位置,但如何让计算机根据这些坐标把模型正确的、有层次的画在屏幕上?这就是我们需要变换三维顶点坐标的原因,最终目的就是让GPU可以将这些三维数据绘制到二维屏幕上。

根据顶点坐标变换的先后顺序,主要有如下几个坐标空间:
Object space
,模型坐标空间;
World space
,世界坐标空间;
Eye space
,观察坐标空间;
Clip and Project space
,屏幕坐标空间。下图就是GPU的整个处理流程,深色区域就是顶点坐标空间的变换流程,大家了解一下即可,我们需要关注的是每个坐标空间的具体含义和坐标空间之间转换的方法。

图片:image1.png


2.1,从object space到world space 

object space有两层核心含义,第一,object space中的坐标值就是模型文件中的顶点值,这些值是在建立模型时得到的,例如一个.max文件,里面包含的数据就是object space的坐标。第二,object space的坐标与其他物体没有任何参照关系,这是object space和world space区分的关键。world space坐标的实际意义就有有一个坐标原点,物体跟坐标原点相比较才能知道自己的确切位置。例如在unity中,我们将一个模型导入到场景中以后,它的transform就是世界坐标。


2.2,从world space到eye space 

所谓eye space,就是以摄像机为原点,由视线方向、视角和远近平面,共同组成的一个梯形体,如下图,称之为视锥(viewing frustum)。近平面,是梯形体较小的矩形面,也是靠近摄像机的平面,远平面就是梯形体较大的矩形,作为投影平面。在这个梯形体的内的数据是可见的,超出的部分会被视点去除,也叫视锥剪裁。

例如在游戏中的漫游功能,屏幕的内容随摄像机的移动而变化,这是因为GPU将物体的顶点坐标从world space转换到了eye space。 

图片:image2.png

2.3,从eye space到project and clip space 

eye space坐标转换到project and clip space坐标的过程其实就是一个投影、剪裁、映射的过程。因为在不规则的视锥体内剪裁是一件非常困难的事,所以前人们将剪裁安排到一个单位立方体中进行,这个立方体被称为规范立方体(CCV),CVV的近平面(对应视锥体的近平面)的x、y坐标对应屏幕像素坐标(左下角0、0),z代表画面像素深度。所以这个转换过程事实上由三步组成:

(1),用透视变换矩阵把顶点从视锥体变换到CVV中; 

(2),在CVV内进行剪裁; 

(3),屏幕映射:将经过前两步得到的坐标映射到屏幕坐标系上。 


2.4,primitive assembly(图元装配)和triangle setup(三角形处理) 

到目前为止我们得到了一堆顶点的数据,这一步就是根据这些顶点的原始连接关系还原出网格结构。网格由顶点和索引组成,这个阶段就是根据索引将顶点链接到一起,组成线、面单元,然后进行剪裁,如果一个三角形超出屏幕以外,例如两个顶点在屏幕内,一个顶点在屏幕外,这时我们在屏幕上看到的就是一个四边形,然后把这个四边形切成两个小的三角形。

现在我们得到了一堆在屏幕坐标上的三角形面片,这些面片是用于光栅化的。 


3,光栅化阶段。 

经过上面的步骤之后,我们得到了每个点的屏幕坐标值,和我们需要绘制的图元,但此时还有两个问题: 

(1)屏幕坐标是浮点数,但像素是用整数来表示的,如何确定屏幕坐标值所对应的像素? 

(2)如何根据已确定位置的点,在屏幕上画出线段或者三角形? 

对于问题1,绘制的位置只能接近两指定端点间的实际线段位置,例如,一条线段的位置是(10.48, 20.51),转换为像素位置就是(10,21)。 

问题2,涉及到具体的画线和填充算法,有兴趣的话可以研究。 

这个过程结束后,顶点和图元已经对应到像素,之后的流程就是如何处理像素,即给像素赋予颜色值。 

给像素赋予颜色的阶段称为Pixel Operation,是在更新帧缓存之前,执行最后一系列针对每个片段的操作,其目的是计算出每个像素的颜色值。在这个阶段,被遮挡的面通过一个被称为深度测试的过程消除。

pixel operation包含下面这些流程: 

(1)消除遮挡面; 

(2)Texture operation,纹理操作,根据像素的纹理坐标,查询对应的纹理值; 

(3)Blending,通常称为alpha blending,根据目前已经画好的颜色,与正在计算的颜色的alpha值混合,形成新的颜色。 

(4)Filtering,将正在计算的颜色经过某种滤镜后输出。 

该阶段之后,像素的颜色值被写入帧缓存中。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

学习着色器,并理解着色器的工作机制,就要对OpenGL的固定功能管线有深入的了解。

首先要知道几个OpenGL的术语

渲染(rendering):计算机根据模型(model)创建图像的过程。 模型(model):根据几何图元创建的物体(object)。 几何图元:包括点、直线和多边形等,它是通过顶点(vertex)指定的。

最终完成了渲染的图像是由在屏幕上绘制的像素组成的。在内存中,和像素有关的信息(如像素的颜色)组织成位平面的形式,位平面是一块内存区域,保存了屏幕上每个像素的一个位的信息。例如,它指定了一个特定像素的颜色中红色成分的强度。位平面又可以组织成帧缓冲区(framebuffer)的形式,后者保存了图形硬件为了控制屏幕上所有像素的颜色和强度所需要的全部信息。

OpenGL的固定功能管线

理清了基本的概念,下面了解了一些关于OpenGL渲染管线的知识.看了这个之后对于OpenGL的学习我想应当是很有帮助.关于这么一篇的原文则是GLSL-LIGHTSOURCE 教程一个开篇部分.点击这里访问原文。原文是英文的,以下是中文的翻译,点击访问下文的原文地址。

关于渲染管线将什么呢?无非就是在OpenGL的管道当中各个部分的功能以及如何在管道当中形成了我们想要的最终的一幅图.(像素).而管线当中的操作可分为以下几个部分:

阶段1. 指定几何对象.

如:点 线 三角形.等一些几何图元..OpenGL绘制几何图元的方法有以下三种:

  • <1> 一次一个顶点.即使用glBegin()  glVertex() glEnd() 指定几何对象.
  • <2> 使用顶点数组..如glDrawArrays.glDrawElements.等.一次性的绘制大量图元.

上面这两种模式则是立即模式.即指定完图元之后会被立即渲染.即将所有数据发往渲染管线后立即被渲染.

  • <3>显示列表模式.它存储于OpenGL服务端 (接收OpenGL命令的一端),操作函数有 glNewList、 glEndList、 glCallList .

阶段2   顶点处理操作:

不管以上的几何对象是如何指定的,所有的几何数据都将会经过这个阶段,这个阶段负责的则是逐个顶点的操作.

在这个阶段能做的工作则是:

  1.  顶点变换:根据模型视图和投影矩阵变换
  2. 光照计算和法线变换(法线矩阵 是模型矩阵的左上角3*3的逆矩阵)和法线规格化
  3.  纹理坐标变换.(纹理矩阵)
  4. 材质状态:纹理坐标生成

而最重要的则是变换以及光照. 每个顶点在这个阶段分别是单独处理的.

这个阶段所接收到的数据则是每个顶点的属性特征..输出则是变换后的顶点数据.

阶段3  图元组装

在顶点处理之后,顶点的全部属性都已经被确定。在这个阶段顶点将会根据应用程序送往的图元规则如GL_POINTS 、GL_TRIANGLES 等将会被组装成图元。

阶段4 图元处理(裁剪 消隐)

  • <1>这个步骤第一个所做的应当是裁剪操作,会将图元与用户定义的裁剪平面,即glClipPlane 和模型投影矩阵所建立的视景比较. 这将会裁剪且丢弃位于视景和裁剪平面外部的图元.不在予以处理.
  • <2> 其次.若是采用透视投影 那么.将会对每个顶点的x,y z坐标分别除以w.
  • <3>紧接着,则是由视口变换将顶点坐标变换至窗口坐标.
  • <4> 执行消隐操作

阶段5  栅格化操作

  • <1>由图元处理传递过来的图元数据.在此将会被分解成更小的单元并对应帧缓冲区的各个像素.这些单元被称之为片元. 一个片元可能包含窗口左边、深度、颜色、纹理坐标等属性.
  • <2> 片元的属性则是图元上顶点数据等经过插值而确定的..这里生成的片元将会包含主颜色和次颜色.   glShadeMode() 函数的作用将会这里体现.即使用插值(平滑着色) 或者使用最后一个顶点颜色(平面着色)
  • <3> 点宽 线宽.多边形模式,正面背面等一些特征也将是这阶段发生作用.
  • <4> 反走样也是这个阶段起作用.

阶段6 片元处理

  • <1>上纹理:通过纹理坐标取得纹理内存中相对应的颜色。
  • <2> 雾化:通过片元距离当前视点位置修改颜色.
  • <3> 颜色汇总..这个与混合完全不同概念.将纹理,主定义的颜色,雾化的颜色,次颜色光照阶段计算的颜色 汇总一起.

阶段7  逐个片元的操作

  • <1> 所有的一些测试 像素所有权 剪切(glScissor) Alpha测试(glAlphaFunc) 模版测试(glStencilFunc) 深度测试 (glDephtFunc) 混合(glBlendFunc)

这些操作将会最后影响其在帧缓冲区的颜色值.

阶段8  帧缓冲操作

  • <1>这个阶段执行帧缓冲的写入等操作等..最后产生了显示出来的像素.

glColorMask、glStrncilMask、glDepthMask、glClearDepht、glClearStencil、glClearColor 等.将在这个阶段影响写入的值.

以上只是讨论OpenGL 图元绘制的基本过程 那么基于像素图像绘制.几乎形同之上..只是在光栅化处理前的操作不一样.即经过像素解码 像素传输.栅格化 最后形成片元...片元之后的处理完全一样..

可编程管线可以替换的功能

在着色器编程领域..你将可实现

  • Vertex Shader(顶点着色器) 替换 顶点处理阶段
  • Fragment Shader(片元着色器,又叫像素着色器) 替换 片元处理阶段
  • Geometry Shader(几何着色器) 替换 图元组装阶段..

因为这三个阶段所决定都是最重要效果的阶段..对于这些的可编程将带来非常大的好处以及可控制的渲染!!

在前面的固定功能管线提到了,在阶段5:栅格化操作 过程中, 片元的属性会由图元上顶点数据等经过插值而确定。在顶点着色器处理完毕后,OpenGL都会将顶点与顶点之间的片元(基本上可以理解为像素)的属性(如位置坐标、纹理坐标)进行线性插值。所以,在纹理坐标为(1,0)和(0,0)中间的片元会得到一个(0.5,0)的纹理坐标,在纹理坐标为(0,0)和(1,1)之间的片元会得到一个(0.5,0.5)的纹理坐标。然后将这些经过差值处理之后的片元交给片元着色器处理。片元着色器确定最终的片元颜色。

-----------------------------------------------------------------------------------------------------------------------------

  渲染管线过程:

       3D 物体经过一系列的变化(过程),最终显示在玩家的眼帘的过程。

  3d(面片),3D Object ,经过一系列复杂的过程,这个过程硬件变化是看不到的

  输入的是一个面片,输出的是一个形状

  贴图:要分uv,要对uv做纹理坐标的映射。

  拿到美术资源,有贴图,是错觉 ,美术为了便于设计,在制作时候是有贴图,分uv,及映射,

  提交给引擎是图片和模型是分离的,还有一个材质球是索引所有图片的,材质球里面带有参数。

  黑盒里面包含的步骤:

   顶点处理:

       通过一系列的坐标系转换,讲模型的顶点在摄像机前进行位移,并最终投影到摄像机的投影屏幕上。

  本地坐标  

  世界坐标

  观察坐标

  投影坐标

   本地--世界 

        将每个物体模型的位置从以自己为中心的坐标系,摆放到整体游戏世界的具体某一个位置中

  到世界坐标系后才能形成一个场景。

  世界--观察

       将每个物体模型的位置以世界为参照系,转换到以观察者为参考系。

  观察--投影

       将每个物体模型的位置从观察者所在的坐标体系中,转到观察者投影平面上

       最特殊的一次坐标系转换(非线性,模型倍扭曲)

  光照: 在观察坐标系下进行。 (观察可以从不同的角度去观察object)

       

-------------------------------------------------------------------------------------------------

   面处理:通常有引擎来处理

    面的组装

    point list  Triangle List  Line List Triangle strip Line strip Triangle Fan

    面截取

    面剔除

      U3D Shader可以设置

      HRESULT SetRenderState(D3DRS_CULLMODE,value)

      可选择的值有:

      D3DCULL_NONE:对任何顶点排列方式都不进行剔除

      D3DCULL_CW:  顺时针排列的顶点被认为是反面,对他们进行剔除

      D3DCULL_CCW: 逆时针排列的顶点被认为是反面,对他们进行剔除

    注:正反面是根据法线

    视锥剔除:去掉在视锥外的面的部分

    在截取过程中,落在屏幕外面的面的部分已经被去除,视锥剔除阶段主要处理落

在近端截除平面和远端截除平面之外的面的部分。

   解决方式:通过硬件提供的深度缓存Depth Buffer或者z-Buffer来判读。

 这里的深度指面距离镜头所在平面的距离。  

-------------------------------------------------------------------------------------------------

   光栅化

   像素处理