上节回顾

在第一节中,我们介绍过 OpenGL ES 与 GLSL 的主要功能,就是往绘制 buffer 上绘制图片。其中虽然 GLSL 制作的 shader 是穿插在 OpenGL ES 中使用,但是我们在流程中可以看出来,两大 shader(vertex shader 和 fragment shader)相对于 OpenGL ES 其他模块还是比较独立的。这两个 shader 就好比两个函数一样,有输入,有输出。从 OpenGL ES 传入一些参数,在 shader 中进行运算,然后再传出给 GPU 的其他模块。这一节,我们来仔细研究一下 shader 的功能。然后从这一节开始,我们将一共用四节的内容,来说一下 GLSL 的语法和如何使用 GLSL 语言 书写 shader。


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。


GLSL 预处理

首先我们来看预处理。

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_开头的宏定义都已经被保留了,也就是说开发者不可以定义这种规则的宏定义。


GLSL 注释

Shader 的注释语法,与我们熟知的 c、c++的语法完全一样。都是使用/**/ 和//来指明注释的。如果一个完整的注释完全表达在一行中,那么在编译器眼中, 它就是一个空格。

Shader 中除了预处理之外,还有变量,还有 main 函数等,这些语法我们将在之后的文章中进行详细讲解。

本节教程就到此结束,希望大家继续阅读我之后的教程。

谢谢大家,再见!


Shader的重要性

今天和一个资深猎头聊了会天,首先先聊聊猎头这个行业,其实我是不反感这个行业的,甚至我觉得这个行业非常重要,猎头和我们程序员应该是合作关系,通过猎头,程序员可以更好的知道市场需求,在这个时刻会发生变化的时代,提高自己核心竞争力的时候,保证自己不会被市场淘汰。今天聊天的这个猎头,就是一个我已经认识了很久,已经可以称为朋友的资深猎头。

通过他我发现了一个可怕的事情,自研引擎路在哪里,游戏引擎可以分为很多模块,动作、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)