OpenGL 简介

OpenGL 简介

一、GPU 接口规范

对于刚接触 OpenGL 的初学者,常常会有这样一个疑问: OpenGL 的源码在哪里,如何编译?

然而实际上 OpenGL 并不是一个软件实现,更多的是一个标准协议; OpenGL 更像是一种显卡驱动标准,由各个硬件厂家适配,各个硬件厂商根据 OpenGL 接口规范编撰对应的驱动.

换句话说,对于各个硬件厂商 OpenGL 确实是一个基于 GPU 的软件实现,但是对于普通的应用层开发者 OpenGL 就是一个由硬件厂商提供的驱动程序罢了,也就是为什么你找不到 OpenGL 源代码实现的原因了.

OpenGL 是操作 GPU 的其中一种方法,但绝不是唯一的途径; OpenGLKhronos 进行管理,在 Khronos官网下,你可以找到下面这张图:

Khronos_Brief

在这里,你可能看到一些熟悉的名称,比如 EGLglTFOpenCLOpenGL 以及 Vulkan等等,这里定义了许多标准规范;这里我们仅仅谈及真正直接操作GPU的标准协议,也就是下图中 3D Graphics 的部分:

Khronos_Classification

然后呢,这就是全部了吗? 实际并不是, Khronos 更多像是一个联盟,有着许多成员如 AppleIntelAMDGoogleARMQualcommNvidia 等等成员.

所以,也就会出现"分道扬镳"的现象,比如:

微软下的 Direct X (D3D9、D3D11、D3D12),对标 OpenGL:

Dirext_X

苹果下的 Metal, 还是对标 OpenGL:

Metal

以及英伟达大名鼎鼎的 CUDA, 对标 OpenCL:

CUDA

可以看到关于一个 GPU 接口规范的指定,出现了很多不同的角色比如硬件产商、操作系统提供者等等;为什么会出现这么多种规范呢?其中一部分原因来自于某一种标准实现并不适用于某些场景,但还有其他一部分原因就是跟为什么有这么多中编程语言类似的原因.

二、GPU 接口分类

GPU(Graphic Processing Unit)按我的理解可以分为两大类 GL(Graphic Language) 和 CL(Compute Language):

GPU_Classification

本文讨论的重点在 OpenGL, 故实际上处于 GPUGL 分支.

题外话

实际上很多人会发现 VULKANOpenGL 非常的相似,虽然VULKAN 一方面是为了解决 OpenGL 设计之初留下的问题,但 VULKAN 个人认为另一个积极的意义是在于给予行业一个新的标准化机会;例如 VULKAN 的协议制定苹果就挺上心的 (毕竟"分道扬镳"对于开发者而言意味着成倍的开发成本,对于厂商则很难形成一个开发者愿意投入的生态).

From Arm Mali Developer OpenGL ES 3.x GUIDE

Built from the ground up, Vulkan was intended to replace OpenGL as the main graphics API.

OpenGL had successfully served the industry for more than 20 years, but it was time for a clean

start. The new graphics API was expected to provide a set of benefits across multiple platforms

that the graphics community recognizes and values today.

三、OpenGL 设计结构

如果用一句话来描述 OpenGL 的话,我想应该是基于C/S结构设计的模板模式(设计模式里的那个);在 图形渲染管线 这节,主要介绍的是其模板设计,在 OpenGL 里我们称之为 PipeLine;在 C/S结构 这节,则介绍 OpenGL C/S 结构给 OpenGL 带来的一些对于初学者看起来可能觉得奇奇怪怪的东西.

(1) 图形渲染管线

下图是出自于 es_spec_3.2.pdfDataflow Model:

OpenGL_Pipeline

这张图描述的东西挺多,但对于初学 OpenGL 只需要关心红色标注部分即可.其他部分例如 Framebuffer 的一部分 (Default Framebuffer) 在本章中由 EGL 负责; 而 Compute Shader 则是 OpenGL 提供的 CL 功能,目前讲述的是 GL 的部分,故省略.

上图就是 OpenGL 渲染管线整体的数据流图,仔细观察图中的左下方可以发现, OpenGL 按照颜色对不同的部分进行了区分,例如 Fixed Function Stage 以及 Programmable Stage 等等; 对于大部分的开发者而言需要关注 Fixed Function Stage 中的 Vertex Shader 以及 Fragement Shader,以及理解 Fixed Function Stage 中的 RasterizationPer-Fragement Operations 以及 Tessellation Primitive Gen 等步骤.某些 Fixed Function Stage 对于开发者而言属于不可控制的节点,但是只有充分理解其行为才能在其他可编程部分写出自己想要的程序.

题外话: 为什么要引入 Pipeline 的概念呢?

Each pipeline is controlled by a monolithic object created from a description of all of the shader

stages and any relevant fixed-function stages. Linking the whole pipeline together allows the

optimization of shaders based on their input/outputs and eliminates expensive draw time state

validation. (from vulkan spec)

(2) C/S结构

谈起 OpenGL C/S 结构,就不得不提 OpenGL 对于开发者的一个强约束限制,就是: 只能在 OpenGL 所在的 context 线程里操作 OpenGL 的接口.

语言上可能很难说清楚这个 C/S 到底是什么结构,但是图例可能会相对清楚,例如使用 EGL 来创建 OpenGL context 时,在 eglInitialize 调用前后,可以看到这样有意思的现象:

EGL_BEFORE
EGL_AFTER

在前后两个接口调用前,程序的堆栈发生了明显的变化,这些多出来的线程主要就是用来辅助 OpenGL Server 运行的;实际上直到调用EGL接口eglMakeCurrent前,都是不能调用 OpenGL 接口的.其实也很好理解, Server 都没运行起来, Client 怎么能请求呢?

实际上,很多人说创建 OpenGL context 是为了给 OpenGL 提供一个渲染使用的画布;在我看来这种说法固然没有什么错误,不过我觉得更大一部分的贡献是在于运行起 OpenGL Server. 创建 OpenGL context 有很多种方式,例如使用 EGLSDLGLFWQT等等非常多种;为什么存在这种多种创建方式的主要原因在于: OpenGL 希望自己与硬件无关,而创建 OpenGL context 往往需要涉及到具体的硬件信息,比如这个 context 是运行在哪一个 Display 上 (OpenGL 标准里有针对于此详细的信息,有兴趣可以自己翻阅下).

在嵌入式上, EGLOpenGL ES 以及 GLSL ES 往往是一起使用的三剑客; 在这里实际上已经简单涉及到了 EGL 了,受限于篇幅本章就不再展开 EGL 相关的细节.

由于 OpenGL 只是一个规范而非实现,所以上面的现象在不同硬件平台,不同版本里面的现象可能是不同的;OpenGL 标准里也有明确写明,这要能保证接口调用的结果是符合预期的,OpenGL 不关注具体实现.

那这种 C/S 结构对于我们使用 OpenGL 有什么影响呢? 最大的影响就是在于我们只能在 OpenGL context 所在的线程调用 OpenGL 的接口,详细阅读 EGL 标准你会知道所有 OpenGL 接口都存在一个阴式的入参,就是 OpenGL Context; 这种接口设计广泛存在于 OpenGL 的各种接口,在深入学习 OpenGL 之后应该会更加有感触,这里由于篇幅也不再展开了.

四、渲染管线

在这节里,会对 图形渲染管线 小结中部分出现的环节进行描述,旨在理解一个完整 OpenGL 渲染管线的运行流程以及其逻辑.

(1) 背景补充

在真正描述渲染管线前,需要知道以下几个常识:

  • 两点确定一条直线
  • 三点确定一个三角形
  • n边形可以由 (n-2) 个三角形组成

OpenGL 中,所有图形均有点(point)、线(Line Segment)、三角形(Triangle)组成;换句话说对于 OpenGL 而言最基础的元素是,在 OpenGL 里称之为顶点(Vertex),对应的可编程节点则是 Vertex Shader.

而另外一个可编程节点则是 Fragment Shader;在非常早期的时候 Fragment Shader 其实是叫做 Pixel Shader; OpenGLPixel 替换为了 Fragment 的一个原因是在于 Pixel 强调的是一个点的概念而 Fragment 则是代表一个面的概念;在实际屏幕显示时,实际上对应的是一块块像素块,而像素块并不一定是正方形,所以后来修改为 Fragment 强调面的概念.对于初学者而言,可能 Pixel Shader 会更加容易理解.

(2) GLSL

OpenGL 整个渲染管线中存在可编程节点, OpenGL 将它们统称为 Shader. 值得注意的是,并不是所有可编程节点都是需要的;在大多数场景下,我们只需要处理 Vertex ShaderFragment Shader 就足够了.

为了实现可编程的这个目标, OpenGL 需要搭配一门 GPU 编程语言使用,即 GLSL(OpenGL Shading Language); 实际上对于大部分 GPU 开发者而言,最终的主要任务就是编写和优化 GLSL, 高效的 GLSL 能够节约 GPU 的使用资源以及缩短 GPU 的处理时间.

在本章中由于篇幅也不打算展开 GLSL 的细节描述,在之后以案例的感性形式给出一个 GLSL 的使用.这里给出一个示例的 GLSL 写法,对应亮度擦除(转场)的实现:

# vertex
attribute vec3 Position;
attribute vec2 iUv;
varying   vec2 oUV;
uniform mat4 WorldViewProj;

void main(void)
{
    oUV         =  iUv;
    gl_Position =  WorldViewProj * vec4(Position, 1.0);
}
# fragment
varying vec2 oUV;
uniform sampler2D TexA;
uniform sampler2D TexB;
uniform sampler2D TexL;
uniform float Progress;

void main(void)
{
    float softness = 0.03f;
    float luma = texture(TexL, oUV).x;
    float time = mix(0.0f, 1.0f + softness, Progress);

    vec4 acolor = texture(TexA, oUV);
    vec4 bcolor = texture(TexB, oUV);

    if (luma <= time - softness)
    {
        gl_FragColor = bcolor;
    }
    else if (luma >= time)
    {
        gl_FragColor = acolor;
    }
    else
    {
        float alpha = (time - luma) / softness;
        gl_FragColor = mix(acolor, bcolor, alpha);
    }
}

GLSL 中,变量简单地将可以分为 attributevaryinguniform 三种. 由于历史的原因 attribute 也称之为 in,而 varying 则称之为 output.可以在 GLSL 标准中找到下列表述:

Not all language constructs present in earlier versions of the language are available in later

versions e.g. attribute and varying qualifiers are present in v1.00 but not v3.00. However, the

functionality of GLSL ES 3.20 is a super-set of GLSL ES 3.10.

可以看到目前 GLSL 更推崇使用 in 代替废弃的 attribute,而使用 out 去代替废弃的 varying;但是个人认为它们还是尤其意义的,尤其是对于初学者而言,例如 OpenGL 可只有 glBindAttribLocation 接口,可没有 glBindInputLocation;为了做一下兼容处理,一般会在 GLSL 程序前写入一段宏定义:

#if __VERSION__ >= 130
    #define attribute in
    #define varying out
#endif

如果我们将 Shader 理解为一个函数,那么就存在入参和出参,分别对应于 attributevarying;这里可以抛出一个问题, attribute 入参是如何传递进去的? 要回答这个问题,需要回到 OpenGL Pipeline 的数据流图:

OpenGL_Pipeline

在整个 PipeLine 运行之初存在一个 Vertex Puller 的环节,Vetex Shader 中的 attribute 就是从这里传递进去的;那么 Fragment Shaderattribute呢? 仔细观察上面 vertexfragment 的代码以及结合 varying 这个单词的含义,就能找到答案.注意对你而言,只有 Vetex Shader 的入参是你真正自己传递进去的.

GLSL 中除了 attributevarying,还存在一个广泛使用的类型 uniform, 一般我们可以将其理解为常量即可; uniform 可以分为两大类,详细可看下 OpenGL ES 标准中的 7.6 Uniform Variables:

  • Named uniform blocks
  • Default uniform block

其中特别指出 Uniforms in the default uniform block are program object-specific state., 其实就是再说有些 uniform 类似于一个句柄,我们只能通过 GLSL 内置接口去操作,例如上面出现的 sampler2D 去操作;而另外的 uniform 而是开发者传递进去的.

其实这里我个人觉得将 uniform 描述为常量在初期是会造成一些理解上的偏差的. 例如 uniform 都是一个常量了,为什么我不能直接将其内嵌入代码即可? 这实际是在于 uniform 只是在一次 Pipeline 过程中才是常量;例如在上面的例子中,存在 uniform 变量 Progress, 实际程序运行的过程中它以 60hz 的速率,从 0.0f 到 1.0f 以 1.0f/60 步进的速率发生着改变,不然图像怎么会出现变化呢?

然后不同 Shader 存在着不同的内置变量,例如 Vertex 存在内置变量 gl_Position 对应着一个顶点的坐标, 而 Fragment 则存在内置变量 gl_FragColor 对应着一个像素点信息.

OpenGL 的坐标分为 w、y、z、w, 即非笛卡尔坐标,当 w 为 1 则可以理解为笛卡尔坐标系; 此外,坐标的取值分为为 [-1.0f, 1.0f], (0.0, 0.0) 代表中心坐标,也是为什么你会经常看到一个类似于 WorldViewProjuniform 变量, 世界坐标系投影.

这里又引出一个很有趣的问题,假设我们需要绘制一张 1080P 的图片; 那么需要两个三角形,对应6个坐标顶点, gl_Position 需要被赋值 6 次; 而 gl_FragColor 则需要被赋值 1080*1920 次. 这里可以停下来思考一个问题, vertex 中的 oUVfragment 中的 oUV 数量是一致的吗? uniform 变量 Progress 到底存在几个?

回答得了这个问题,就打开了 GPU GL 并行处理的大门; 这个问题将会在 Rasterization 栅格化中解释.

(3) 栅格化

对于开发者而言,传入的只是几个顶点坐标,而实际上渲染时肯定针对的是一块像素区域;从 VertexFragment 的过程实际上大体就是 Rasterization 栅格化.

在讲解 Rasterization 之前,给出一个案例的描述,可以思考一下为什么出来一张长这样子的图:

# vertex
attribute vec3 Position;
attribute vec2 iUv;
varying   vec2 oUV;
uniform mat4 WorldViewProj;

void main(void)
{
    oUV         =  iUv;
    gl_Position =  WorldViewProj * vec4(Position, 1.0);
}
# fragment
varying vec2 oUV;
uniform sampler2D TexA;
uniform sampler2D TexB;

void main(void)
{
    float val = (oUV.x + oUV.y) / 2;
    gl_FragColor = vec4(val, val, val, val);
}

并且在 Vertex Puller 中输入以下数据:

// Position(3 scalar)        iUv(2 scalar)
   // one rectangle
   0.0      0.0      0.0      0.0  0.0
   1920.0   0.0      0.0      1.0  0.0
   1920.0   1080.0   0.0      1.0  1.0
   // another rectangle
   0.0      0.0      0.0      0.0  0.0
   1920.0   1080.0   0.0      1.0  1.0
   0.0      1080.0   0.0      0.0  1.0

图例如下:

DrawRect

实际你在屏幕输出可以看到下面这样的效果:

RasterizationDemo

OpenGL 的基础元素有点(Point)、线(Line Segment)以及三角形(Triangle),其栅格化过程各有不同,在这里选取三角形(Triangle)进行表述(实际上大多数场景也只会用到Triangle).

此节所有描述均来自于 OpenGL ES 中的 13.7 Polygons.

对于上面演示的这个例子如果以 1080P 作为屏幕尺寸,那么右上三角形输入三个顶点(vertex)则应该输出 1080*1920/2 个像素点(fragment); 这个过程我们称之为 Rasterization 栅格化,对应于 OpenGL ES 标准中的 Fixed-Function Primitive Assembly and Rasterization.

栅格化的本质是数据内插(interpolation),即如何使用三个顶点(vertex)内插出1080*1920/2 个像素点(fragment)?这里就要谈及内插公式,这里不想讲得过于复杂就附带上 OpenGL ES 中官方的描述,并做扼要的说明:

RasterizationPolygons1
RasterizationPolygons2

  • p : barycentric coordinates, 三角形中任意一点
  • abc : p 与三角形三个顶点相连接,可以分割出三个子三角形; abc就是子三角形和三角形的比例关系,故 a+b+c=1
  • associated datunm : 相关数据,比如内置变量 gl_Position,自定义输出变量 varying oUV
  • w : clip coordinate; 被剪辑过的坐标,这里简单可以理解为没有剪辑,传入是什么就是什么

结合下面这张图以及输入的值,找几个坐标好好算一算,就可以理解这个内插公式了:
RasterizationDemo

五、案例演示

(1) 素材源

TexA

TexA

TexB

TexB

TexL

TexL

Vertex
// Position(3 scalar)        iUv(2 scalar)
   // one rectangle
   0.0      0.0      0.0      0.0  0.0
   1920.0   0.0      0.0      1.0  0.0
   1920.0   1080.0   0.0      1.0  1.0
   // another rectangle
   0.0      0.0      0.0      0.0  0.0
   1920.0   1080.0   0.0      1.0  1.0
   0.0      1080.0   0.0      0.0  1.0

(2) 渐变转场

FadeVal 从 0.0 至 1.0 均匀变化.

Fade vertex shader
attribute vec3 Position;
attribute vec2 iUv;
varying   vec2 oUV;
uniform mat4 WorldViewProj;

void main(void)
{
    oUV         =  iUv;
    gl_Position =  WorldViewProj * vec4(Position, 1.0);
}
Fade fragemet shader
varying vec2 oUV;
uniform sampler2D TexA;
uniform sampler2D TexB;
uniform float FadeVal;

void main(void)
{
    gl_FragColor = mix(texture(TexA, oUV), texture(TexB, oUV), FadeVal);
}
渐变转场效果图

TransitionFade

(3) 亮度擦除转场

演示百叶窗效果,本质原理为查表;Progress 从 0.0 至 1.0 均匀变化.

Luma vertex shader
attribute vec3 Position;
attribute vec2 iUv;
varying   vec2 oUV;
uniform mat4 WorldViewProj;

void main(void)
{
    oUV         =  iUv;
    gl_Position =  WorldViewProj * vec4(Position, 1.0);
}
Luma fragment shader
varying vec2 oUV;
uniform sampler2D TexA;
uniform sampler2D TexB;
uniform sampler2D TexL;
uniform float Progress;

void main(void)
{
    float softness = 0.03f;
    float luma = texture(TexL, oUV).x;
    float time = mix(0.0f, 1.0f + softness, Progress);

    vec4 acolor = texture(TexA, oUV);
    vec4 bcolor = texture(TexB, oUV);

    if (luma <= time - softness)
    {
        gl_FragColor = bcolor;
    }
    else if (luma >= time)
    {
        gl_FragColor = acolor;
    }
    else
    {
        float alpha = (time - luma) / softness;
        gl_FragColor = mix(acolor, bcolor, alpha);
    }
}
亮度擦除转场效果图

TransitionLuma

结语

在写这篇文章之前我想了很久,最终我决定不去描述 OpenGL 的各个细节,比如 OpenGL 的接口定义、如何调用、如何从 CPU 向 GPU 传输数据等等; 想以概括性的方式描述一下 OpenGL 即可;如果想深入了解 OpenGL ,可以读读 ES 2.0知识串讲,个人非常喜欢(所以本章的写法侧重点也与此不同,不想重复).

此外,本章所描述的大部分实现你都可以从 MMP 这个仓库中找到,详细可以阅读下 调试.

MMP 是业余时我自己开发的一个SDK,目前主要参考了几个开源程序,比如 ppsspp (很有意思的开源模拟器)以及 obs; 个人觉得都是相当优秀的开源项目,值得学习以及投入.

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值