OpenGL ES 开发随想:关于Shader

发表于2015-11-09
评论0 3.6k浏览

当你开心地按照教程一步一步编写你的第一个OpenGLES程序,调用各种GL接口,设置好渲染环境,准备好一个正方体模型数据,并且你还别具心思地在每秒30次的回调函数中写了一段代码让这个正方体旋转。看起来一切都准备就绪了,不过这时教程却跳出一段文字:“对了,我们还需要编写一个OpenGL的program,才能让这个程序跑起来”。什么?另外一个程序?嘛,准确来说是由两段shader代码组成的程序。

s27729014

OpenGL经典红宝书

那啥?虽然我们已经通过调用GL接口准备好了大部分OpenGL渲染环境,不过那些是在CPU跑的,GPU这边可什么都不知道。不过这些不是通过OpenGL库就可以搞定吗?其实并不是(OpenGL表示呵呵)。你还需要一个在GPU跑的程序,具体就是我们今天要讲的Shader啦。

是的,Shader就是一段在GPU中跑的代码。其中两个必要的Shader就是上篇文章中提到的VertexShader和FragmentShader。OpenGL程序运行时,Shader代码一般是以字符串的形式保存的,通过调用GL接口把这段字符串编译成一个可以在GPU运行的程序(这里我们称之为glProgram),然后OpenGL库把这个程序发送到GPU运行。

Screen Shot 2015-10-31 at 2.45.31 PM

正常情况下一个glProgram由一个VertexShader和一个FragmentShader共同编译组成。虽然这个编译操作是在程序运行时执行的,不过执行的效率非常高,不会有性能问题。不过这里需要注意的是编译失败的后果很严重 ……就像是你点了个火腿煎饼,然后老板跟你说,“不好意思,我们这边菜单出了点问题,火腿煎饼做不了”。当然你顶多吃不了煎饼,但程序就不是这么一回事了。这里强调的是,我们需要确保每个用到的glProgram都能成功编译。

现在我们已经知道Shader是一段代码,那接下的问题就是这段代码怎么写。这里我们需要用到一门新的编程语言,叫OpenGL着色语言(OpenGL Shading Language),简称GLSL。不过不用担心,这门语言的基本语法跟C/C++差不多,而且是更加简单。下面我们就看一下实际的VertexShader和FragmentShader是什么样子的。

VertexShader

代码示例:

Screen Shot 2015-11-09 at 10.39.04
VertexShader的作用很简单,就是计算出一个坐标点。对,就只有一个坐标点。这里你可能会很疑惑,一个坐标点可以做什么,一个模型有那么多个坐标,这里的坐标是指哪一个?恩~~这里我们指每个坐标点,也就是对于模型的每个坐标点,VertexShader都会运行一次来计算出一个坐标点。想想你在吃石榴的时候不也是一颗一颗吃的吗,咦……难道不是吗?接下来简单讲解一下上面这段代码的几个关键点:

  • main函数,每个shader都必须定义,也就是这段shader的入口。
  • gl_Position: 输出点坐标,一定要赋值,毕竟这个是这个shader的目的。
  • attribute 变量:这个变量的值需要联系VertexBufferObject来理解,模型的点数据最终是打包成一个VertexBufferObject缓存传输到GPU中的。以下图为例,代码中我们定义了两个attribute,对应下图的V部分和UV部分,对于VertexShader计算第一个点时,VertexPosition = vec3(v01,v02,v03),VertexUV = vec2(uv01,uv02)。同理,对于第二点,VertexPosition = vec3(v11,v12,v13),VertexUV = vec2(uv11,uv12)。也就是Attribute变量每计算一个点都取一行VertexBufferObject数据进行赋值。

Screen Shot 2015-11-09 at 11.20.50

  • uniform 变量:这个变量可以看成常量,也就在VertexShader计算每个坐标点时,这个值是保持不变的。一般是一些全局的数据,比如说变换矩阵,可以用来对每个点进行变换。
  • varying 变量:是用来把值传到FragmentShader中用的,是的,这两个shader关系还挺紧密的。需要注意的是,verying的定义在两个shader中必须一模一样,不然编译会出错。这里有个坑需要注意一下,对于这种varying定义不一致的错误OpenGL编译器的log是不会跟你说具体哪里错了,就是直接说编译不过了,你自己看着办把。如果你碰到这种无缘无故的编译错误,可以检查一下这个地方。(PS:难道这是对粗心程序员的小小惩罚)
  • 其他变量的定义跟C差不多,就是局部和全局的区别。

FragmentShader

代码示例:

Screen Shot 2015-11-09 at 10.40.16
FragmentShader的作用其实也很简单,就是计算出一个“点”的颜色(这里点的定义参考上篇文章)。跟VertexShader的解释差不多,对于屏幕上每一个有内容的“点”(可简单理解为模型在屏幕上覆盖的地方),FragmentShader都会运行一次来计算出那个点的颜色。
好吧,其实上面说的“很简单”只是开玩笑而已。关于怎么计算出一个点的颜色,可以说是shader中最复杂的地方,也是最神奇的地方。仔细想一下,这里可是直接决定屏幕上显示什么内容的地方。模型贴图,光照效果,阴影等效果都是在这里计算的。具体的方法这里就不讲了,大家有兴趣可以google一下各种教程。提醒一下,如果各位是像我一样的数学渣,最好补一下线性代数的一些东西,因为这里是各种“数学魔法”产生作用的地方。简单讲解一下上面这段代码的几个关键点:

  • main函数:这里是跟VertexShader一样,作为程序的入口。
  • gl_FragColor:输出点颜色,一定要赋值,是这个shader的最终目的。请利用各种“数学魔法”对它进行赋值。
  • attribute 变量:不好意思,我们这里没这种变量,请不要在这里定义这种奇怪的变量。
  • uniform 变量:跟VertexShader一样,请参考上面的解释。
  • varying 变量:这里varying变量的值不一定等于上面VertexShader传过来的值。请仔细想想,VertexShader是对每个模型的点进行计算的,而FragmentShader是对屏幕上“点”进行计算的。举个例子,下面的正方形,我们在VertexShader把4个角的颜色赋值到varying变量中,然后FragmentShader中取到的结果就像下图的结果那样,是4个颜色的过渡值。这个过渡值是GPU帮你算好传到FragmentShader中的,这也是varying这个单词的意义所在。

Screen Shot 2015-11-09 at 10.48.42

关于shader的上面只讲了一些重要概念,具体的语法,用法什么的大家有兴趣可以找资料看看,那些教程肯定会比我这里讲的更详细。

GPU真的很忙

最后,讲一个关于GPU的有意思的事实。从上面的关于shader的解释看,你已经知道这shader代码会运行很多次。那具体是多少次呢?这里我就具体举个例子来算一下有多少次吧。
下面这个画面是我的引擎运行在iPhone5c上面,分辨率为640×1136,这只羊的模型点数为5760,刷新率为30fps,粗略估算这只羊占了屏幕30%的面积。

IMG_0216
对于每一次渲染,我们的VertexShader运行5760次,FragmentShader运行218112(= 640 x 1136 x 0.3)次。那么每秒,VertexShader运行172800次,FragmentShader运行6543360次。

这意味着你写的每一行VertexShader代码每秒执行17万次,每一行FragmentShader代码每秒执行650万次。这个数据仅仅是对于这个模型来算的,如果场景中加入更多模型,并且覆盖了整个屏幕的话,那么实际的次数可能需要X10。

这个模型iPhone5c的GPU上渲染起来还是非常地轻松,所以说GPU的运算能力真的是非常惊人。同时我们在写Shader的时候也需要时刻提醒自己,每一行代码每秒是很有可能会被执行上百万次的,必须地小小谨慎才行。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引