LearnOpenGL - Android OpenGL ES 3.0 多线程同步,fence、glWaitSync 和 glClientWaitSync 的运用

系列文章目录

一、前言

本章讨论 OpenGL ES 中多线程技术的运用。首先,说明我们为什么需要这项技术,在 ARM OpenGL ES 教程 thread_sync 中提到

当我们转向更复杂的图形应用程序时,可能会想要使用多线程(MT)。一个典型的情况是,我们的图形应用程序需要执行大量的数学运算。在这种情况下,可能通过将工作量移至不同于管理图形操作的线程来提高性能。另一个常见的例子是我们想让图形用户界面(GUI)在一个独立的线程中运行。

多线程的好处非常重要。MT使得我们的应用程序始终保持响应,并且不只是与应用程序的GUI有关。任何设备都在后台持续执行着大量的额外任务,尤其是与网络相关的服务。因此,在次要线程中执行重型任务变得至关重要。

如今,更多的嵌入式设备配备了多处理器架构。如果我们真的想要充分利用这些处理器,那么我们肯定需要MT。只有当处理器需要管理多个并发线程时,像超线程和多核处理器这样的技术才能被有效地利用。

多线程编程模型隐含了同步的需要。每个多线程应用程序都需要以协调的方式管理其不同的线程。线程之间的同步可以通过MT库中可用的标准互斥体、信号量和条件变量来实现。

本文提到的示例代码你可以在 ThreadSyncDrawer 中找到

二、从 OpenGL 的 C/S 架构说起

OpenGL是一种规范或接口,它主要用于管理应用程序与图形硬件之间的交互。不论渲染任务的复杂性如何,调用OpenGL API的时间基本上是很短的。这是因为当你调用OpenGL API时,你实际上只是向发送了一条命令,图像渲染的实际工作是在GPU上完成的。

我们将这种与GPU交互的方式称之为客户端/服务器(C/S)架构。在这里,调用OpenGL API的CPU作为客户端,而执行渲染任务的GPU则作为服务器。

在OpenGL的上下文(GL Context)中,有一个命令队列。每当你调用OpenGL API时,你就在向这个队列发送一个命令。每个线程都可以绑定到一个OpenGL上下文,这意味着不同的线程具有不同的命令队列。

在GPU中也有一个命令队列,但是它只有一个。在硬件驱动的协调下,来自不同线程的命令将被逐一送入GPU的命令队列中。GPU将按照命令的顺序来执行这些命令。

在这里插入图片描述
上图中,有三个线程,每个线程执行了一些 OpenGL API,往在 GL Context 的命令队列上发送了命令,我在图中对每个命令做了编号。现在提问:按照上图所示,GPU 上的执行顺序可能是?

在多线程环境中,由于线程调度的不确定性,命令的执行顺序可能有多种。不过,每个线程的命令在各自的GL context中的执行顺序是固定的,即1.1必须在1.2前执行,1.2必须在1.3前执行,同理,2.1在2.2前,2.2在2.3前,3.1在3.2前,3.2在3.3前。

假设这三个线程的命令进入 GPU 的命令队列,那么可能的执行顺序就非常多。以下是一些可能的顺序:

  • 1.1、1.2、1.3、2.1、2.2、2.3、3.1、3.2、3.3
  • 1.1、2.1、3.1、1.2、2.2、3.2、1.3、2.3、3.3
  • 2.1、2.2、2.3、1.1、1.2、1.3、3.1、3.2、3.3
  • 2.1、1.1、3.1、2.2、1.2、3.2、2.3、1.3、3.3
  • 3.1、2.1、1.1、3.2、2.2、1.2、3.3、2.3、1.3
  • 等等

可以看到虽然在 CPU 上各个线程执行时间有先有后,但由于线程调度和硬件驱动调用的不确定性,在 GPU 上命令执行的顺序实际上有多种可能。

三、OpenGL 的同步接口

3.1 glFlush 和 glFinish

在 OpenGL ES 2.0 版本中,无法做到跨线程之间的同步,我们能够使用的工具很少,只有 glFlush 和 glFinish。

glFlush和glFinish都用于控制OpenGL的执行。两者的区别在于他们控制执行流程的方式和时机。

glFlush()是一种用于迫使之前的OpenGL命令开始执行的方法。当你在程序中调用glFlush(),OpenGL会把所有待执行的命令放入图形处理器(GPU)的命令队列中。这意味着,无论你的程序在何时何地调用glFlush(),你都可以确保所有已经发送到GPU的命令都将最终被执行。然而,调用glFlush()并不意味着等待所有命令完成执行。你的程序会在glFlush()命令返回后继续运行,同时GPU也会开始处理命令队列中的命令。

另一方面,glFinish()不仅会迅速开始执行所有尚未开始执行的OpenGL命令,还会一直阻塞,直到所有的命令都完成执行,并且所有的结果都已返回到你的程序。当glFinish()返回时,你可以确定所有之前的OpenGL命令都已经完成了执行。由于glFinish()会等待所有OpenGL命令完成执行,所以通常会因为大量的等待时间而导致程序性能下降。除非必要,否则通常不推荐频繁使用glFinish()。

总的来说,glFlush()和glFinish()都是OpenGL的同步命令,可以用来控制OpenGL命令的执行流程。然而,glFlush()是非阻塞的,只要求命令尽快开始执行,而glFinish()则是阻塞的,需要等待所有命令完成。

3.2 同步对象

3.2.1 fence

在 OpenGL ES 3.0 中引入了同步对象,这使得 OpenGL ES 的多线程编程更具有控制性。你可以使用下面的 API 来创建一个同步对象

GLsync glFenceSync(GLenum condition, GLbitfield flags);

其中 condition 必须是 GL_SYNC_GPU_COMMANDS_COMPLETE,而 flags 填 0 即可。

同步对象有两种可能的状态:已触发(signaled)和未触发(unsignaled)。当使用glFenceSync创建一个同步对象时,它处于未触发状态。它还会在OpenGL命令流中插入一个栅栏(fence)命令,并将其与同步对象关联起来。当glFenceSync命令的条件得到满足时,即命令流中直到栅栏位置的所有命令都执行完毕时,状态会转变为已触发。

3.2.2 glClientWaitSync 和 glWaitSync

OpenGL 提供了两个函数来等待同步对象触发,分别是

GLenum glClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
void glWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);

函数glClientWaitSync会阻塞所有CPU操作,直到同步对象被发出信号。如果同步对象在超时时间内没有被发出信号,函数会返回一个状态码来表示这一点。

而对于glWaitSync,行为稍有不同。图形命令在GPU上是严格按照顺序执行的,所以当命令流中达到一个同步对象时,可以保证所有之前的命令都已经完成。
应用程序实际上不会等待同步对象被发出信号;只有GPU会等待。因此,glWaitSync将立即返回到应用程序。由于应用程序不等待函数返回,所以不存在挂起的危险,因此必须将标志值设为零。此外,超时实际上将取决于实现,因此指定特殊的超时值GL_TIMEOUT_IGNORED以明确这一点。

3.2.2 举个例子

为了说明 glWaitSyncglClientWaitSync 机制原理和区别,我们来看一个具体的实例。

设想有两个线程T0和T1,其中T0线程负责将图像绘制到纹理上,而T1线程需要使用T0线程准备好的纹理进行特效处理。也就是说,T1需要等待T0完成绘制。为了让T1等待T0,我们在T0中创建一个同步对象,并同时向T0的命令队列发送一个栅栏(fence)命令。在T1上,我们可以使用glWaitSync命令来让GPU等待,以确保正确的执行顺序:

  • T0:1.1、1.2、1.3(fence)
  • T1:2.1(glWaitSync)、2.2、2.3

在多线程环境中,由于线程调度的不确定性,T0和T1在GPU上的执行顺序可能有多种。假设当前GPU上的命令队列是这样的:

  • GPU:2.1(glWaitSync)、2.2、2.3、1.1、1.2、1.3(fence)

当GPU执行了2.1(glWaitSync)后,它发现这是一个glWaitSync命令,于是停止执行之后的命令。这里的“之后的命令”是指在同一OpenGL上下文中的命令。也就是说,GPU并不会完全停下来,它仍会执行其他上下文的命令。因此,GPU继续执行1.1、1.2、1.3(fence)。当fence命令执行完毕后,同步对象被触发,于是GPU可以继续执行T1上的其他命令,也就是2.2和2.3。总的来说,在这种情况下,GPU的执行顺序为:2.1(glWaitSync)、1.1、1.2、1.3(fence)、2.2、2.3。

在T0和T1线程中,如果使用glClientWaitSync,其行为将有所不同。T0和T1的命令队列如下:

  • T0:1.1、1.2、1.3(fence)

  • T1:2.1(glClientWaitSync)、2.2、2.3

首先,因为T1需要等待T0的同步对象,所以T1中的2.1(glClientWaitSync)命令在CPU上的执行时间肯定会晚于T0中的1.3(fence)。而且,因为glClientWaitSync会在CPU上阻塞,所以2.2和2.3在CPU上不会立即执行,也就不会立即发送给GPU。因此,当前GPU上的命令队列可能如下:

  • GPU:2.1(glClientWaitSync)、1.1、1.2、1.3(fence)

GPU会按照命令队列的顺序执行。当执行完成1.3(fence)命令后,CPU会收到信号,解除阻塞,接着执行2.2和2.3。因此,在这个情况下,GPU的执行顺序可能为:2.1(glClientWaitSync)、1.1、1.2、1.3(fence)、2.2、2.3。

额外要说的是,一个同步对象可以被多个 glWaitSync 调用等待。当与同步对象关联的fence命令被执行(即纹理绘制等操作完成),同步对象会变为已发信号状态。在这种状态下,所有等待这个同步对象的 glWaitSync 调用都将收到通知,并允许之后的命令开始执行。

换句话说,一旦同步对象收到信号,所有等待它的 glWaitSync 调用都将返回,不仅仅是其中一个。

因此,即使有更复杂的情况,例如有三个线程分别是 T0 、T1 和 T2,其中 T0 负责绘制纹理,T1 和 T2 需要等待纹理绘制完毕后进行其他的处理,这种情况下,我们仍然能够使用 glWaitSyncglClientWaitSync 来实现同步。

四、多线程中的上下文管理

在 OpenGL 多线程编程中会涉及到如何管理 OpenGL Context 的问题,有两种方式来进行多线程之间共享上下文。

第一种方式,只持有一个 OpenGL Context,在多个线程中切换上下文,使其当前线程绑定那唯一的上下文。操作顺序如下:

  • 将上下文设为线程1的当前上下文:eglMakeCurrent(display, surface, surface, context);
  • 在线程1中执行 OpenGL ES 操作…
  • 将上下文从线程1解绑:eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
  • 将上下文设为线程2的当前上下文:eglMakeCurrent(display, surface, surface, context);
  • 在线程2中执行 OpenGL ES 操作…

在第二种方式中,每个线程都有其自己的渲染上下文,并且因为上下文是相互独立的,它们可以同时是当前上下文。为每个线程创建单独的渲染上下文具有显著优势。首先,我们无需担心从一个线程解绑再绑定到另一个线程的唯一渲染上下文,这常常是导致应用程序挂起的常见原因。其次,更重要的是,我们不会影响性能,因为命令队列可以保持持续供应。每次从一个线程解绑渲染上下文时,该线程将无法执行任何图形操作。这种上下文切换会降低应用程序的性能,特别是当这些切换频繁发生时。

需要注意的是,我们需要指定一个共享上下文(shared context),然后基于这个共享上下文创建其他所有上下文,这样我们就能在不同的上下文之间共享资源。

五、Show the code

接下来看一个实际的例子,该例子来自于 ARM 的 OpenGL ES 教程 ThreadSync.java,这份代码主要逻辑使用的是 C/C++ 实现,基于它我重写成 Kotlin 以便各位看官理解。所有代码可以在 ThreadSyncDrawer

在这份代码中,你可以看到 fence 命令和 glWaitSync 之间是如何做线程同步的,以及如何管理多线程中的 OpenGL Context,希望对你有所帮助。

这份代码中,有两个线程:

  1. 主线程,主要负责将纹理绘制到屏幕上。
  2. 子线程,负责更新纹理上的数据。

如果两个线程之间不做同步,那么你会看到偶尔有画面失真的情况出现,如下图:

我们看具体代码,首先是主线程:

override fun prepare(context: Context) {
        // .... 与之前代码一致

        // set texture image data
        animateTexture()
        val byteBuffer: ByteBuffer = ByteBuffer.wrap(textureData)
        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, texWidth, texHeight, 0,
            GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, byteBuffer)
        MyGLUtils.checkGlError("texImage2D")

        // unbind texture
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0)

        if(useFence.get()){
            mainThreadSyncObject = GLES30.glFenceSync(GLES30.GL_SYNC_GPU_COMMANDS_COMPLETE, 0)
            Log.d(TAG,"Use of GL Fence enabled.")
        }

        createWorkingThread()
    }

主线程的 prepare 函数中,主要是做一些准备工作,这些代码基本上在前面几章中都有详细的解释了,不再赘述,让我们重点与之前代码有区别的地方:

  1. animateTexture(): 是一个自定义方法,用于在纹理上应用动画效果。这个方法参考了 ThreadSync.cpp 中的实现

  2. byteBuffer: 创建一个ByteBuffer对象,将纹理数据存储在其中。

  3. GLES30.glTexImage2D(): 这个函数会将纹理数据传递给OpenGL ES,用于创建2D纹理。参数分别是纹理目标,纹理的mipmap层级,纹理的内部格式,纹理的宽度和高度,边缘的宽度,源数据格式,源数据类型和存储纹理数据的ByteBuffer。

  4. if(useFence.get()): 这里根据useFence标志位的状态来确定是否启用GL Fence。如果启用,则调用GLES30.glFenceSync()创建一个用于同步OpenGL指令的GL Fence对象。

  5. createWorkingThread(): 用于创建工作线程的方法。

接下来看下 draw 方法中的逻辑:

override fun draw() {
        if(useFence.get())
        {
            if(workingThreadSyncObject != 0L)
            {
                GLES30.glWaitSync(workingThreadSyncObject, 0, GLES30.GL_TIMEOUT_IGNORED)
            }
            else
            {
                return
            }
        }

        shader.use()
        shader.setInt("texture0", 0)

        GLES30.glViewport(0, 0, screenWidth, screenHeight)

        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)

        GLES30.glBindVertexArray(vaos[0])
        GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[0])

        GLES30.glDrawElements(GLES30.GL_TRIANGLES, indices.size, GLES30.GL_UNSIGNED_INT, 0)

        // unbind vao
        GLES30.glBindVertexArray(0)

        if(useFence.get())
        {
            if(mainThreadSyncObject == 0L)
            {
                Log.i(TAG, "mainThreadSynobj == NULL at the end of renderframe.")
            }

            mainThreadSyncObject = GLES30.glFenceSync(GLES30.GL_SYNC_GPU_COMMANDS_COMPLETE, 0)
        }
    }
  1. 该函数主要在执行绘制(draw)功能。绘制过程可能涉及线程同步和防止数据竞争,需要使用OpenGL的fence来进行同步等待。具体看代码中if(useFence.get())的部分。

  2. 使用GLES30.glWaitSync函数会让当前(在这里就是GL)线程等待一个fence sync object。如果workingThreadSyncObject不为0(即fence sync object存在),GL线程会进入等待状态,直到此fence被触发。

  3. 如果workingThreadSyncObject为0,那么函数直接返回,不执行后面的绘制过程。

  4. 使用shader,设置其属性"texture0"为0。

  5. GLES30.glViewport将视图设置成占满整个屏幕。

  6. GLES30.glClear用来清除屏幕,使其为单色。

  7. GLES30.glBindVertexArray和GLES30.glBindTexture是准备绘制素材,即准备好VAO和纹理。

  8. GLES30.glDrawElements进行实际的绘制。其中的参数GL_TRIANGLES表示用三角形进行绘制,indices.size表示索引数目,GL_UNSIGNED_INT表示索引是无符号整形。

  9. 绘制完成后,使用GLES30.glBindVertexArray(0)来解绑VAO。

  10. 最后使用GLES30.glFenceSync创建一个新的fence sync object,并将其赋值给mainThreadSyncObject,用于之后的线程同步。如果mainThreadSyncObject之前为0(旧的fence不存在),则打印一条信息。

接下来,我们来看子线程的代码:

private fun createWorkingThread(){
        // get shared context
        val sharedEGLContext = EGL14.eglGetCurrentContext()

        val workingThread = Thread{
            // shared egl context
            val eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
            val version = IntArray(2)
            if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {
                throw RuntimeException("unable to initialize EGL14")
            }

            val attribList = intArrayOf(
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_ALPHA_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
                EGL14.EGL_NONE
            )

            val configs = arrayOfNulls<EGLConfig>(1)
            val numConfigs = IntArray(1)
            if (!EGL14.eglChooseConfig(
                    eglDisplay, attribList, 0,
                    configs, 0, configs.size, numConfigs, 0
                )
            ) {
                throw RuntimeException("unable to find RGB888+recordable ES2 EGL config")
            }
            val attribListContext = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
            val eglContext = EGL14.eglCreateContext(eglDisplay, configs[0], sharedEGLContext, attribListContext, 0)

            if (eglContext == null || eglContext == EGL14.EGL_NO_CONTEXT) {
                throw RuntimeException("Failed to create new context.")
            }

            // Create a new surface to make current
            val attribListSurface = intArrayOf(EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE)
            val eglSurface = EGL14.eglCreatePbufferSurface(eglDisplay, configs[0], attribListSurface, 0)

            // eglMakeCurrent
            if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
                throw RuntimeException("eglMakeCurrent failed")
            }

            // loop for updating text data

            while(!threadExit.get())
            {
                Thread.sleep(1000 / 60)

                animateTexture()

                if(useFence.get())
                {
                    if(mainThreadSyncObject != 0L)
                    {
                        GLES30.glWaitSync(mainThreadSyncObject, 0, GLES30.GL_TIMEOUT_IGNORED)
                    }else
                    {
                        continue
                    }

                    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[0])
                    val byteBuffer: ByteBuffer = ByteBuffer.wrap(textureData)
                    GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, texWidth, texHeight, 0,
                        GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, byteBuffer)

                    workingThreadSyncObject = GLES30.glFenceSync(GLES30.GL_SYNC_GPU_COMMANDS_COMPLETE, 0)
                }else
                {
                    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[0])
                    val byteBuffer: ByteBuffer = ByteBuffer.wrap(textureData)
                    GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, texWidth, texHeight, 0,
                        GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, byteBuffer)
                }


            }

        }
        workingThread.start()
    }
  1. 这个函数首先被命名为“createWorkingThread”,暗示它是在创建一个工作线程。线程创建过程中会用到共享的EGL context参数。

  2. 接着创建一个新线程。在该线程中,首先获取默认的EGLDisplay,并初始化它。

  3. 设置一个int型数组”attribList”作为参数,用来为配置创建EGLContext。

  4. EGL14.eglChooseConfig函数则是用来选择一个符合这些条件的配置并返回。

  5. 在线程中获取新的EGLcontext,如果没有成功创建的情况下,会报出运行时异常。

  6. 然后创建一个对应的PbufferSurface,这是EGL中的一个off-screen surface。

  7. 使用EGL14.eglMakeCurrent把刚刚创建的context和surface关联起来。

  8. 创建一个循环,在这个循环里,每隔一定时间(16ms,相当于60帧的间隔)就调用“animateTexture()”更新纹理。

  9. 在使用fence的情况下,如果主线程的sync object存在,则等待其触发。然后更新texture,并创建一个新的工作线程的sync object以供下次循环使用。

  10. 如果不使用fence,那么直接更新texture。

  11. 循环结束,线程工作完成。此后,新创建的线程会自行运行,更新纹理数据。

参考

OpenGL ES 3.0 英文版 第1章——OpenGL ES 3.0简介   第1章简单介绍OpenGL ES,概述了OpenGL ES 3.0图形管线,讨论了OpenGL ES 3.0的设计理念和限制,最后介绍了OpenGL ES 3.0中使用的一些约定和类型。   第2章——你好,三角形:一个OpenGL ES 3.0示例   第2章介绍绘制三角形的一个简单OpenGL ES 3.0示例。我们的目的是说明OpenGL ES 3.0程序的样子,向读者介绍一些API概念,并说明如何构建和运行OpenGL ES 3.0示例程序。   第3章——EGL简介   第3章介绍EGL——为OpenGL ES 3.0创建表面和渲染上下文的API。我们说明与原生窗口系统通信、选择配置和创建EGL渲染上下文及表面的方法,传授足够多的EGL知识,你可以了解到启动OpenGL ES 3.0进行渲染所需的所有知识。   第4章——着色器和程序   着色器对象和程序对象是OpenGL ES 3.0中最基本的对象。第4章介绍创建着色器对象、编译着色器和检查编译错误的方法。这一章还说明如何创建程序对象、将着色器对象连接到程序对象以及链接最终程序对象的方法。我们讨论如何查询程序对象的信息以及加载统一变量(uniform)的方法。此外,你将学习有关源着色器和程序二进制代码之间的差别以及它们的使用方法。   第5章——OpenGL ES着色语言   第5章介绍编写着色器所需的着色语言的基础知识。这些着色语言基础知识包括变量和类型、构造器、结构、数组、统一变量、统一变量块(uniform block)和输入/输出变量。该章还描述着色语言的某些更细微的部分,例如精度限定符和不变性。   第6章——顶点属性、顶点数组和缓冲区对象   从第6章开始(到第11章为止),我们将详细介绍管线,教授设置和编程图形管线各个部分的方法。这一旅程从介绍几何形状输入图形管线的方法开始,包含了对顶点属性、顶点数组和缓冲区对象的讨论。   第7章——图元装配和光栅化   在前一章讨论几何形状输入图形管线的方法之后,第7章将讨论几何形状如何装配成图元,介绍OpenGL ES 3.0中所有可用的图元类型,包括点精灵、直线、三角形、三角形条带和三角扇形。此外,我们还说明了在顶点上进行坐标变换的方法,并简单介绍了OpenGL ES 3.0管线的光栅化阶段。   第8章——顶点着色器   我们所介绍的管线的下一部分是顶点着色器。第8章概述了顶点着色器如何融入管线以及OpenGL ES 着色语言中可用于顶点着色器的特殊变量,介绍了多个顶点着色器的示例,包括逐像素照明和蒙皮(skinning)。我们还给出了用顶点着色器实现OpenGL ES 1.0(和1.1)固定功能管线的示例。   第9章——纹理   第9章开始介绍片段着色器,描述OpenGL ES 3.0中所有可用的纹理功能。该章提供了创建纹理、加载纹理数据以及纹理渲染的细节,描述了纹理包装模式、纹理过滤、纹理格式、压缩纹理、采样器对象、不可变纹理、像素解包缓冲区对象和Mip贴图。该章介绍了OpenGL ES 3.0支持的所有纹理类型:2D纹理、立方图、2D纹理数组和3D纹理。   第10章——片段着色器   第9章的重点是如何在片段着色器中使用纹理,第10章介绍编写片段着色器所需知道的其他知识。该章概述了片段着色器和所有可用的特殊内建变量,还演示了用片段着色器实现OpenGL ES 1.1中所有固定功能技术的方法。多重纹理、雾化、Alpha测试和用户裁剪平面的例子都使用片段着色器实现。   第11章——片段操作   第11章讨论可以适用于整个帧缓冲区或者在OpenGL ES 3.0片段管线中执行片段着色器后适用于单个片段的操作。这些操作包括剪裁测试、模板测试、深度测试、多重采样、混合和抖动。本章介绍OpenGL ES 3.0图形管线的最后阶段。   第12章——帧缓冲区对象   第12章讨论使用帧缓冲区对象渲染屏幕外表面。帧缓冲区对象有多种用法,最常见的是渲染到一个纹理。本章提供API帧缓冲区对象部分的完整概述。理解帧缓冲区对象对于实现许多高级特效(如反射、阴影贴图和后处理)至关重要。   第13章——同步对象和栅栏   第13章概述同步对象和栅栏,它们是在OpenGL ES 3.0主机应用和GPU执行中同步的有效图元。我们讨论同步对象和栅栏的使用方法,并以一个示例作为结束。   第14章——OpenGL ES 3.0高级编程   第14章是核心章节,将本书介绍的许多主题串联在一起。我们已经选择了高级渲染技术的一个样本,并展示了实现这些功能的示例。该章包含使用法线贴图的逐像素照明、环境贴图、粒子系统、图像后处理、程序纹理、阴影贴图、地形渲染
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值