OpenGLES的shader介绍



  • 大家好,今天小白给大家简单介绍下shader,欢迎一起讨论学习。

    一、Shader
    Shader其实就是一段执行在GPU上的程序,此程序使用OpenGL ES SL语言来编写。它是一个描述顶点或像素特性的简单程序。在opengles中常用的shader有两种:vertex shader和fragment shader。Geometry Shader(几何着色器)是继Vertex Shader和Fragment Shader之后,由Shader Model 4引入的新的着色器。还有一个compute Shader由Shader Model 5引入的提供通用计算能力的着色器,不同阶段的着色器可以对应不同版本的GLSL。

    1、Vertex Shader
    1)、对于发送给GPU的每一个Vertex(顶点),都要执行一次Vertex Shader。其功能是把每个顶点在虚拟空间中的三维坐标变换为可以在屏幕上显示的二维坐标,并带有用于z-buffer的深度信息。Vertex Shader可以操作的属性有:位置、颜色、纹理坐标,但是不能创建新的顶点。

    2)、vertex shader主要完成以下工作:1).基于点操作的矩阵乘法位置变换;2).根据光照公式计算每点的color值;3).生成或者转换纹理坐标。

    3)、Vertex Shader输入数据如下:
    a. Attributes

    由 vertext array 提供的顶点数据,如空间位置,法向量,纹理坐标以及顶点颜色,它是针对每一个顶点的数据。属性只在顶点着色器中才有,片元着色器中没有属性。属性可以理解为针对每一个顶点的输入数据。OpenGL ES 2.0 规定了所有实现应该支持的最大属性个数不能少于 8 个。

    注:Vertex Attributes 是每点的属性数据。与一个index序号绑定。外部程序可通过 glBindAttribLocation将一个attribute 名与一个index绑定起来。当然,OPENGL ES 内部会自动绑定所有attributes.外部程序只需通过 glGetAttribLocation获取指定attribute名的index。 给Attribute传值可以通过 glVertexAttribPointer函数或者glVertexAttrib4fv

    b.Uniforms

    uniforms保存由应用程序传递给着色器的只读常量数据。在顶点着色器中,这些数据通常是变换矩阵,光照参数,颜色等。由 uniform 修饰符修饰的变量属于全局变量,该全局性对顶点着色器与片元着色器均可见,也就是说,这两个着色器如果被连接到同一 个program Object,则它们共享同一份 uniform 全局变量集。因此如果在这两个着色器中都声明了同名的 uniform 变量,要保证这对同名变量完全相同:同名+同类型,因为它们实际是同一个变量。此外,uniform 变量存储在常量存储区,因此限制了 uniform 变量的个数,OpenGL ES 2.0 也规定了所有实现应该支持的最大顶点着色器 uniform 变量个数不能少于 128 个,最大的片元着色器 uniform 变量个数不能少于 16 个。

    c.Samplers

    一种特殊的 uniform,在vertex shader中是可选的,用于呈现纹理。sampler 可用于顶点着色器和片元着色器。

    d. Shader program

    由 main 声明的一段程序源码,描述在顶点上执行的操作:如坐标变换,计算光照公式来产生 per-vertex 颜色或计算纹理坐标。

    e、Vertex Shader输出为:
    1).Varying:varying 变量用于存储顶点着色器的输出数据,当然也存储片元着色器的输入数据,varying 变量最终会在光栅化处理阶段被线性插值。顶点着色器如果声明了 varying 变量,它必须被传递到片元着色器中才能进一步传递到下一阶段,因此顶点着色器中声明的 varying 变量都应在片元着色器中重新声明同名同类型的 varying 变量。OpenGL ES 2.0 也规定了所有实现应该支持的最大 varying 变量个数不能少于 8 个。

    2).在顶点着色器阶段至少应输出位置信息-即内建变量:gl_Position,是每个点固有的Varying,表示点的空间位置。其它两个可选的变量为:gl_FrontFacing 和 gl_PointSize。

    2、 Fragment Shader
    1)、Pixel Shader(像素着色器)就是众所周知的Fragment Shader(片元着色器),它计算每个像素的颜色和其它属性。它通过应用光照值、凹凸贴图,阴影,镜面高光,半透明等处理来计算像素的颜色并输出。它也可改变像素的深度(z-buffering)或在多个渲染目标被激活的状态下输出多种颜色。一个Pixel Shader不能产生复杂的效果,因为它只在一个像素上进行操作,而不知道场景的几何形状。

    2)、Fragment Shader输入数据如下:
    a.Varyings

    这个在前面已经讲过了,顶点着色器阶段输出的 varying 变量在光栅化阶段被线性插值计算之后输出到片元着色器中作为它的输入,即上图中的 gl_FragCoord,gl_FrontFacing 和 gl_PointCoord。OpenGL ES 2.0 也规定了所有实现应该支持的最大 varying 变量个数不能少于 8 个。
    b.Uniforms

    前面也已经讲过,这里是用于片元着色器的常量,如雾化参数,纹理参数等;OpenGL ES 2.0 也规定了所有实现应该支持的最大的片元着色器 uniform 变量个数不能少于 16 个。
    c.Samples

    一种特殊的 uniform,用于呈现纹理。

    d.Shader program

    由 main 申明的一段程序源码,描述在片元上执行的操作。
    FragmentShader输出为:在顶点着色器阶段只有唯一的 varying 输出变量-即内建变量:gl_FragColor

    3、 Geometry Shader
    1)、可选着色器。Geometry Shader(几何着色器)是继Vertex Shader和Fragment Shader之后,由Shader Model 4(第四代显卡着色架构)正式引入的第三个着色器。它在Driect3D 10和OpenGL 3.2中开始引入,在OpengGL 2.0+中作为扩展使用,在OpenGL3.x中也成为核心。它的输入为:点、线和三角形;其输出为点、线带和三角形带。

    2)、在Geometry Shader里,我们处理的单元是Primative。虽然根本上都是顶点的处理,但进入vertex shader里的是一次一个的顶点,而进入Geometry Shader的是一次一批的顶点,Geometry Shader掌握着这些顶点所组成的图元的信息。Geometry Shader的处理阶段处于流水线的栅格化之前,也在视锥体裁剪和裁剪空间坐标归一化之前。虽说裁剪过程会剔除部分图元也会分割某些图元, 但就目前来说,不会有其他流水线的可编程阶段会在Geometry Shader之后提供出影响图元的性质(形式和数量)——这是Geometry Shader鉴于其位置的特殊性而拥有的一个重要特点。

    3)、Geometry Shader程序在Vertex Shader程序执行之后执行。

    4、曲面细分着色器

    1)、可选着色器。包括曲面细分控制着色(TCS,Tessellation Control Shader)和曲面细分评估着色(TES,Tessellation Evaluation Shader)。

    2)、TCS作用于一组叫做控制点(CP,Control Points)的顶点组。控制点并不是被定义成像三角形、矩形、五边形等多边形形式,而是定义为一个几何表面,这个表面通常由多项式来定义,而且移动其中一个控制点将会影响整个表面。这个通常在一些图形软件中,用户可以通过移动一组控制点来随意改变模型表面或者曲线形状,一组控制点通常称为一个Patch。下图中的黄色表面就是通过一个16个控制点的patch来定义的:
    http://ogldev.atspace.co.uk/www/tutorial30/patch.jpg

    TCS输入一组patch并处理后输出一组新的patch,开发者在shader中可以对控制点进行变换,也可以删除或者新增控制点(类似于几何着色器可以修改或增删顶点)。另外,出了输出patch,着色器还会计算输出一组称作曲面细分级别(Tessellation Levels,TL)的数据。TL决定了曲面细分的细节程度,即每组patch需要生成多少三角形。上面的操作都发生在着色器中,因此开发者可以使用任意的算法来计算细分等级TL。例如,我们可以定义TL的值为3,如果光栅化三角形覆盖的像素数低于100,像素数在101-500之间TL值为7,再多的TL的值就定义为12.5了(后面会介绍TL的值如何控制曲面细分的精细程度)。另外的算法还有根据离相机的距离来计算细分程度的,都可以使得每组patch根据其自身的特点得到不同的TL值。

    3)、CS结束后进入PG固定功能着色阶段,进行真正的细分操作。这里新手会很容易疑惑,PG并没有真正的对TCS输出的patch进行细分,事实上它甚至没有访问patch的权限。相反,它根据TL的值在特定的空间中进行曲面细分,该空间可以是单位化的2维矩形或者是由三维质心坐标定义的等边三角形:
    http://ogldev.atspace.co.uk/www/tutorial30/domains.png

    4)、三角形的质心坐标系是一个综合三角形的三个顶点的权值来定义三角形内部位置的方法。三角形的顶点由U、V以及W三个分量确定。三角形中的某一个点的位置越靠近一个顶点,则这个顶点的权值就越大,相应的其他两个顶点的权值就会减小。如果这个点正好位于一个顶点上,那么对应这个顶点的权值为1,另外两个顶点的权值都为0。例如质心坐标系的U为(1,0,0),V为(0,1,0)、W为(0,0,1),此时三角形的中心用质心坐标系表示就是(1/3,1/3,1/3)。质心坐标系的一个有趣的特点是,如果将三角形内部一点的三个分量相加得到的结果将总是1。为了简单,之后我们将专注于三角形空间。

    5)、PG根据TL的值在三角形内部生成一系列的点,每个点都是由这个三角形的质心坐标系确定的。开发者可以选择输出的拓扑结构为点或者是三角形。如果选择的是拓扑关系为点,那么PG会直接将其传入渲染管线的下一阶段并按照点来进行光栅化。如果选择的是三角形,PG会将所有顶点连起来这样整个三角面就被细分成了多个小的三角面:


    http://ogldev.atspace.co.uk/www/tutorial30/subdivision.jpg

    6)、整体上TL会告诉PG三角形外边缘上的分段的数量以及三角形边到中心之间环的个数,从而进行三角形的构造。所以上面图片中的这些小三角形与我们之前看到的patch有什么关系呢?事实上这就主要取决于你想使用曲面细分技术去做什么。其中一个非常简单的用法(此教程中我们要用到的)就是跳过曲面的多项式表示,简单说就是让模型中的三角形面直接简单地映射到patch上。那种情况下组成三角形的3个顶点就成了3个控制点,而原始的三角形既是TCS的输入patch也是输出patch。我们用PG来对三角形区域进行曲面细分并且生成由质心坐标表示的“普通”三角形并对这些坐标进行线性组合(例如将他们与原始三角形的属性相乘)来对原始模型的三角形面进行细分。在下一节中我们我们将会介绍patch在几何曲面上的实际应用。要牢记PG在意的不是TCS的输入和输出patch,而是每个patch的TL值。

    7)、至此PG完成了对三角形域的曲面细分,我们还需要使用细分的结果进行进一步的处理,毕竟PG自己无法访问patch,它唯一的输出就是质心坐标和他们的连通性。进入TES着色器阶段后,TES有权限去访问TCS中输出的patch和PG生成的质心坐标。PG对每一个质心坐标都会执行TES着色器,而TES的功能就是为在PG中生成的每一个位于质心坐标系下的顶点都生成一个真正的可用的顶点。因为可以访问patch,TES可以从中获取诸如位置、法线等信息,并且通过这些信息来生成顶点。在PG对一个“小”三角形的三个质心坐标系下的顶点执行TES之后,由TES生成的这三个顶点会被传递到渲染管线的下一阶段传递并把他们当做一个完整的三角形进行光栅化。

    8)、TES与顶点着色器十分相似,总是只有一个输入(质心坐标)和一个输出(顶点)。TES在每次调用过程中只能生成一个顶点,而且它不能丢弃顶点。OpenGL中曲面细分阶段的TES着色器的主要目的就是借助于PG阶段生成的坐标来生成曲面。简单来说就是将质心坐标变换到表示曲面的多项式中并计算出最终结果。结果就是新的顶点的位置,之后这些顶点就能与普通顶点一样进行变换和投影了。如你所见,在处理几何曲面的时候,如果我们选择的TL值越高,我们获得的区域位置就越多,而且通过在TES中对他们进行计算我们得到的顶点就会更多,这样我们就能更好的表示精细的表面。在这一节中表面的计算公式我们简单的使用一个线性组合公式来代替。

    在TES着色器执行之后,产生的新的顶点会被作为三角形传递到渲染管线的下一阶段。在TES之后接下来不管是GS还是光栅化阶段,都和之前的一样了。

    9)、总结一下整个渲染管线的过程:

    a、patch中的每一个顶点都会执行顶点着色器,每个patch中都包含顶点缓存中的多个控制点(CP)(控制点的最大值由驱动和GPU定义);

    b、TCS着色器以顶点处理器处理之后的数据作为输入并生成和输出patch,除此之外它也会产生TLs;

    c、基于配置好的细分空间,通过获取TCS着色器中的TL(细分层级)及其输出的拓扑结构,PG会生成这个空间下的顶点位置和它们的连通性信息;

    d、所有生成的位于细分空间下的位置都会经过TES着色器进行处理;

    在第3步中生成的图元会沿着渲染管线继续传递,这些图元的具体数据来自于TES着色器,然后流程会继续推进到后面的GS阶段和光栅化阶段。

    二、顶点着色与片元着色在编程上的差异
    1)、精度上的差异
    着色语言定了三种级别的精度:lowp, mediump, highp。我们可以在 glsl 脚本文件的开头定义默认的精度。如下代码定义在 float 类型默认使用 highp 级别的精度
    precision highp float;
    在顶点着色阶段,如果没有用户自定义的默认精度,那么 int 和 float 都默认为 highp 级别;而在片元着色阶段,如果没有用户自定义的默认精度,那么就真的没有默认精度了,我们必须在每个变量前放置精度描述符。此外,OpenGL ES 2.0 标准也没有强制要求所有实现在片元阶段都支持 highp 精度的。我们可以通过查看是否定义 GL_FRAGMENT_PRECISION_HIGH 来判断具体实现是否在片元着色器阶段支持 highp 精度,从而编写出可移植的代码。当然,通常我们不需要在片元着色器阶段使用 highp 级别的精度,推荐的做法是先使用 mediump 级别的精度,只有在效果不够好的情况下再考虑 highp 精度。

    2)、attribute 修饰符只可用于顶点着色。这个前面已经说过了。

    3)、或由于精度的不同,或因为编译优化的原因,在顶点着色和片元着色阶段同样的计算可能会得到不同的结果,这会导致一些问题(z-fighting)。因此 glsl 引入了 invariant 修饰符来修饰在两个着色阶段的同一变量,确保同样的计算会得到相同的值。

    本篇转载自:https://blog.csdn.net/hankern/article/details/85316476


Log in to reply