在第一节中,我们介绍过 OpenGL ES 与 GLSL 的主要功能,就是往绘制 buffer 上绘制图片。其中虽然 GLSL 制作的 shader 是穿插在 OpenGL ES 中使用,但是我们在流程中可以看出来,两大 shader(vertex shader 和 fragment shader)相对于 OpenGL ES 其他模块还是比较独立的。这两个 shader 就好比两个函数一样,有输入,有输出。从 OpenGL ES 传入一些参数,在 shader 中进行运算,然后再传出给 GPU 的其他模块。这一节,我们来仔细研究一下 shader 的功能。然后从这一节开始,我们将一共用四节的内容,来说一下 GLSL 的语法和如何使用 GLSL 语言 书写 shader。
OpenGL ES 2.0 对应的 Shader 有两种,vertex shader 和 fragment shader。
Vertex shader 的输入为顶点相关的信息数据,输出分为两部分,必须输出的部分是该顶点最终显示在屏幕上的坐标信息,可选输出的是该顶点对应的其他信息(比如颜色、纹理坐标等)。Vertex shader 一次只能操作一个顶点。
Fragment shader 的输入为屏幕上像素点的信息(坐标以及坐标对应像素从 Vertex Shader 传过来的颜色、纹理坐标等信息)。Fragment shader 不能修改一个像素的位置,在操作一个像素点的时候,也不能访问旁边的像素点。Fragment Shader 通过对顶点信息进行操作,输出每个像素点的颜色。输出值用于更新绘制 buffer 中的 color buffer 或者其他目标 buffer。
Shader 文件看上去其实和一个普通的.c 或者.cpp 文件很像。有预处理,会定义一些变量,有 main 函数,会根据变量进行一些运算,并得到一些结果。
在 Vertex shader 中 main 函数中最终得到的结果是顶点坐标 gl_Position。
Fragment shader 结构和 VS 基本相同。在 main 函数中计算得到的最终结果是像素点的颜色值 gl_FragColor。
VS 和 PS 相似,但是又存在一些不同之处,里面存在着一些我们从未见过的关键词。从现在开始的四节内容,我们将来学习 GLSL 语法,以及如何使用 GLSL 语言编写 Shader。
首先我们来看预处理。
Shader 中预处理的语法与我们熟悉的语法类似。Shader 支持如下这些预处理的方式。
如果一行中只有一个#,这种用法是支持的,但是这一行会被忽略掉。#前只能放置空格或者 tab,而在其后面如果有内容的话,则是指令。
#define 和#undef 与 c/c++中类似,都是用于定义宏定义。后面选择可以跟或不跟宏参数。
#if、#ifdef、#ifndef、#else、#elif、#endif 这些条件判断宏与 c++中也类似, 但是稍微有一些区别,在这些宏的后面只能跟随数字运算或者 define 定义的宏定义。未被 define 定义的识别符不会被默认为 0,如果使用它们会导致 error。不支持字母常量。
在操作符中,不支持连字符##或者 sizeof 等。
我们也稍微举几个和 c++一致的例子,比如&&与操作符,只有在左边不为 0 的时候才计算右边的运算。再比如||或操作符,只有在左边为 0 的时候,才进行右边的运算。没有参加运算的地方,如果使用到了一个未定义的标识符,不会报错。
预处理中的运算是在编译的时候进行。
#error 会将错误信息放到 shader 的 log 中,我们可以通过 OpenGL ES 的 API 获取 shader 的 log。#error 后面的所有信息,一直到新的一行的开始为止,都会 出现在 shader 的 log 中。存在#error 的 shader,我们会认为是一个错误的 shader。
#pragma 是用于通过它后面的参数,控制编译。比如它后面写的是 STDGL, 是用于限定指令不能以 STDGL 开头,因为这些已经被预留了。再比如它后面可以跟 optimize(on)或者 optimize(off),这个是用于在开发和调试 shader 的时候关闭 和开启优化使用的,必须用在 shader 的函数之外,默认情况下所有的 shader 的优化都是打开的。
pragma 后面还可以跟 debug(on)或者 debug(off),用于开启 debug 信息。必须用在 shader 的函数之外,默认情况下所有的 shader 的 debug 都是关闭的。
由于在 GLSL 预处理的时候就会进行一些检查,如果需要引入一些 extension, 那么就需要在早一些被引入。在预处理中,引入 extension 是通过了#extension 来引入,引入的时候#extension 后面跟上 extension 的名字或者跟上 all,all 的意思是编译器支持的所有 extension。然后,再在后面跟上冒号和 behavior。
当 behavior 为 required 的时候,就是指当前 shader 需要用到某个 extension, 如果指定的 extension 不支持,返回 error。
当 behavior 为 enable 的时候,就是指当前 shader 会打开某个 extension 的语法。比 require 的需求稍微弱一点,所以如果指定的 extension 不支持,返回 warn。
使用#extension all required 和#extension all enable 都会返回 error。
当 behavior 为 warn 的时候,也是打开了某个 extension 的语法,如果该 extension 定义了一些 warn 的情况,那么除非是在另外一个已经被 enable 或者 require 的 extension 支持,否则一旦遇到这些 warn 的情况,就会报 warn。如果 #extension 后面跟的是 all 的话,所有 extension 中定义的 warn 一旦遇到,就都会报 warn。如果指定的 extension 不支持,返回 warn。
当 behavior 为 disable 的时候,指定的 extension 就不被支持。如果指定的 extension 不支持,则返回 warn。如果#extension 后面跟的是 all 的话,那么所有 的 extension 规定的语法就都被 disable 了,仅使用 main spec 中的语法。
编译器的初始状态相当于执行了指令:#extension all:disable。指定当前 shader 按照 spec 规定的语法规则,并没有引入任何 extension。
#extension 是从底层控制 extension 的方式,一般指定哪些 extension 组合需要被支持。从顺序上说,后面的#extension 会覆盖前面的#extension。而#extension all 会覆盖前面所有的。不过只有 warn 和 disable 支持 all。
extension 的定义必须在所有非预处理语法之前,extension 的定义是有使用范围的,如果没有特殊指明,那么使用范围就是当前 shader。如果必要的话,linker可以提高 extension 的使用范围,从而扩展到所有包含的 shader 中。
每个 extension 都有对应的宏定义,该宏被定义在 extension 的实现中。所有在 shader 中,可以使用#ifdef 判断某个宏是否被定义了。如果被定义了,说明该宏对应的 extension 被引入了,那么就可以在代码中使用 extension 引入的语法, 如果没有被定义,则不能使用该 extension 引入的新语法。
#version 用于定义该 shader 使用语言的版本号。如果使用 GLSL100,那么这里就要在#version 后面写个 100。这个数值如果写小于 100,或者大于最新的 GLSL 的版本都不对。所有的 shader 中理论上都应该存在该字段,但是在 GLSL100 中, #version 100 这个预处理并不是必须的,因为默认 GLSL 的版本号就是 100。所以如果一个 shader 中没有写#version,那么就默认为是#version 100。
#version 必须写在一个 shader 的最前面,前面只能有注释或者空格。
#line 后面会跟一个常数,比如#line 10。执行了这个指令后,紧随其后的一行代码则被认为是 line 10。
#line 后面也可以跟两个常数,比如#line 10 100。执行了这个指令后,紧随其后的一行代码则被认为是 line 10。而且该行的第一个 string 的 number 会被认 为是 100。然后从该行往下,行号和 string number 都会递加,一直到下一个#line 指令。
上述所说的这些指令中,假如在一个 shader 中使用到了两个冲突的指令, 那么结果就是未定义。编译器有可能报错,也可能不报错,这个要看编译器的具体实现。
在 shader 中存在一些预定义的宏定义,比如 LINE,这个宏定义代表着其当前所在行的行数+1。
再比如 FILE,代表着当前文件的文件名。
VERSION 代表当前 GLSL 的版本,常用的就是 GLSL1.0 和 1.3,对应的 VERSION 就是 100 和 130。
GL_ES 在 ES 系统中会被定义成 1,这个主要是用于判断当前 shader 是否运 行在 ES 的系统中。
所有的以__开头或者以 GL_开头的宏定义都已经被保留了,也就是说开发者不可以定义这种规则的宏定义。
Shader 的注释语法,与我们熟知的 c、c++的语法完全一样。都是使用/**/ 和//来指明注释的。如果一个完整的注释完全表达在一行中,那么在编译器眼中, 它就是一个空格。
Shader 中除了预处理之外,还有变量,还有 main 函数等,这些语法我们将在之后的文章中进行详细讲解。
本节教程就到此结束,希望大家继续阅读我之后的教程。
谢谢大家,再见!
今天和一个资深猎头聊了会天,首先先聊聊猎头这个行业,其实我是不反感这个行业的,甚至我觉得这个行业非常重要,猎头和我们程序员应该是合作关系,通过猎头,程序员可以更好的知道市场需求,在这个时刻会发生变化的时代,提高自己核心竞争力的时候,保证自己不会被市场淘汰。今天聊天的这个猎头,就是一个我已经认识了很久,已经可以称为朋友的资深猎头。
通过他我发现了一个可怕的事情,自研引擎路在哪里,游戏引擎可以分为很多模块,动作、UI、物理、地形、角色、粒子、渲染等等,而在unity如日中天的今天,昔日牛牛的自研引擎开发者还能做什么。面临着中年危机,所擅长的东西不再被市场认可,那么留在原公司养老,貌似是唯一的出路了。但假如公司倒闭或者清理老员工呢。
我是从去年年初开始从自研引擎转到研究UE4,并通过UE4做游戏demo,在PS4上完成了一款让索尼工作人员惊叹的demo后,由于PS4的市场占有率问题,公司没有下决定将这个项目做下去,然后我就转做对Unity的项目支持,帮助公司正在使用Unity的一个项目组。
转眼间,对项目组的Unity支持也大半年过去了,我也慢慢总结出来,自研引擎开发者在Unity上可以做什么。幸亏我是一直坚守在图形学领域,从手机GPU,到android app,再到游戏引擎的渲染部分,OpenGL ES就是我的武器,那么在unity上,我能做的一是优化,优化内存、CPU、GPU,降低游戏的功耗;二是效果,当游戏功耗降低到一定程度后,通过图形学高级算法,提升游戏的画面质量。而图形学高级算法的实现,大部分离不开Shader。
另外通过和猎头的聊天,如果一个简历跳槽次数过多,且每家公司时间较短,是无法通过他们的背景调查的。各位看官,珍重。
原创技术文章,撰写不易,转载请注明出处:电子设备中的画家|王烁 于 2017 年 7 月 10 日发表,原文链接(http://geekfaner.com/shineengine/blog4_OpenGLESv2_3.html)