OpenGL ES 2 前言&第一章

文章传送门

OpenGL ES 2.0 for Android教程(二)

OpenGL ES 2.0 for Android教程(三)

OpenGL ES 2.0 for Android教程(四)

OpenGL ES 2.0 for Android教程(五)

OpenGL ES 2.0 for Android教程(六)

OpenGL ES 2.0 for Android教程(七)

OpenGL ES 2.0 for Android教程(八)

OpenGL ES 2.0 for Android教程(九)

本文是一系列的OpenGL ES 2.0 for Android教程。本系列的绝大部分内容机翻(主要是百度翻译)自英文版的《OpenGL ES 2.0 for Android——A Quick-Start Guide》,作者是Kevin Brothaler。这本书的内容非常浅显易懂,适合对OpenGL ES完全没有任何概念的人入门。但是由于这本书是2013年出版的,里面的Android代码示例使用Java编写,因此决定,这一系列的文章构建在翻译机翻书中的内容的基础上,但又不完全照搬,例如,所有的Android代码示例都将采用Kotlin+Compose重新编写

欢迎使用OpenGL ES for Android!

在Android、苹果的iOS和许多其他移动平台上,开发人员通过名为OpenGL的跨平台应用程序编程界面创建2D和3D图形。OpenGL已经在桌面上使用了一段时间,移动平台使用了一种称为OpenGL ES的特殊嵌入式版本。

OpenGL ES的第一个版本将3D应用到了移动设备上,这非常受开发人员的欢迎,因为它易于学习,并且具有定义良好的功能集。然而,该功能集也很有限,无法跟上最强大的智能手机和平板电脑的最新和最强大功能。

进入OpenGL ES 2.0。大多数旧的API被完全删除,并被新的可编程API取代,这使得添加特效和利用最新设备所提供的功能变得更加容易。这些设备现在可以生产与几年前的控制台相匹敌的图形!然而,为了利用这种能力,我们需要学习2.0附带的新API。2012年8月,Khronos集团最终确定了下一个版本OpenGL ES 3.0的规范,该版本与2.0完全兼容,并通过一些高级功能对其进行了扩展。

那么,在Android上使用OpenGL可以做什么呢?我们可以制作出令人惊叹的实时壁纸,并让数百万用户下载。我们可以创造一个引人注目的3D游戏,它有着生动而惊人的图形。随着硬件成本的下降和云存储的日益扩大,这是一个开始的好时机!

包含哪些内容

以下是我们将要讨论的内容:

  • 在本书的第一部分,你将学习如何创建一个简单的空气曲棍球游戏,包括触摸、纹理和基本物理。本项目将教你如何成功初始化OpenGL并将数据发送到屏幕,以及如何使用基本向量和矩阵数学来创建3D世界。您还将了解Android特有的许多细节,例如如何在Dalvik虚拟机和本机环境之间封送数据,以及如何在主线程和渲染线程之间安全地传递数据。
  • 在本书的第二部分,你将在第一部分所学的基础上继续学习。您将使用一些先进的技术,例如照明和地形渲染,然后您将学习如何创建可以在Android主屏幕上运行的实时壁纸。

Android 支持OpenGL ES API版本的状况如下:

  • OpenGL ES 1.0 和 1.1 能够被Android 1.0及以上版本支持
  • OpenGL ES 2.0 能够被Android 2.2及更高版本支持
  • OpenGL ES 3.0 能够被Android 4.3及更高版本支持
  • OpenGL ES 3.1 能够被Android 5.0及以上版本支持

阅读约定

我们将使用OpenGL来代指OpenGL ES 2.0,这是适用于移动和Web的OpenGL的现代版本。

在本书的大部分内容中,我们将使用GLES20类,它是Android软件开发工具包(SDK)的一部分。因为我们的大多数OpenGL常量和方法都在这个类中,所以我通常会省略类名,直接提到常量或方法。我们将使用静态导入来省略代码中的类名。

一些网络上的资料

请随时访问我(该书作者)维护的OpenGL ES教程博客Learn OpenGL ES

以下是Khronos集团维护的一些优秀在线资源列表:

我建议把参考卡打印出来,放在手边,这样你可以在需要的时候快速查阅。Android使用EGL(一个本地平台接口)来帮助设置显示,因此您可能还会发现Khronos EGL API注册表很有用。

第一章

原书章节花费大量篇幅描述如何配置Android环境,这里省略。

GLSurfaceView

我们先来介绍SurfaceView与它的子类GLSurfaceView,后者能够操纵OpenGL ES,本书的重点。

SurfaceView是一个特殊的View,特殊在于它拥有一层独立的绘图表面,而基础控件中,例如TextView或者Button,它们都将自己的UI绘制在宿主窗口的绘图表面之上,也就是说它们的绘制均在主线程进行。SurfaceView拥有独立于宿主窗口的绘图表面表明它的绘制实际上可以在子线程进行,这非常有利于实现复杂的绘图渲染,例如播放视频。

作为子类的GLSurfaceView封装了OpenGL初始化的细节方面,例如在后台线程上配置显示和渲染。这种渲染是在显示器的一个特殊区域进行的,称为表面(surface);这有时也称为viewport。GLSurfaceView类还可以轻松处理标准的Android活动生命周期,它提供了onResume()onPause(),方便启动和暂停绘制。

GLSurfaceView的绘制行为由接口GLSurfaceView.Renderer指定,该接口有以下三个方法:

  • override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?)

    GLSurfaceView在创建Surface时调用此函数。这会在应用程序第一次运行时发生,也可能在设备唤醒或用户切换回我们的活动时调用。实际上,这意味着在应用程序运行时,可以多次调用此方法。

  • override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int)

    GLSurfaceView在创建Surface后以及在大小发生更改时调用此函数,这些变化包括GLSurfaceView的大小或设备屏幕方向的变化。我们应该使用此方法来响应GLSurfaceView容器的改变。

  • override fun onDrawFrame(gl: GL10?)

    GLSurfaceView在绘制一帧内容时调用此方法。我们必须进行一些绘制操作,即使只是清除屏幕。在这个方法返回后,渲染缓冲区将被交换并显示在屏幕上,因此如果我们不绘制任何东西,我们可能会得到糟糕的闪烁效果。

GL10类型的参数是OpenGL ES 1.0 API的残余。如果我们在编写OpenGL ES 1.0渲染器,我们会使用这个参数,但是对于OpenGL ES 2.0,我们在GLES20类上调用静态方法。

GLSurfaceView将在单独的线程上调用渲染器方法。默认情况下,GLSurfaceView将持续渲染,通常以显示器的刷新率进行渲染,但我们也可以将GLSurfaceView配置为仅在请求时渲染,调用GLSurfaceView类的setRenderMode(),传入GLSurfaceView.RENDERMODE_WHEN_DIRTY。我们还可以调用queueEvent()以在后台渲染线程上发布Runnable。

TextureView

GLSurfaceView实际上创建了自己的窗口,并在视图层次中打了一个“洞”,以便显示底层的OpenGL曲面。对于许多用途来说,这已经足够好了;然而,由于GLSurfaceView是单独窗口的一部分,因此它不像常规View那样具有动画效果或变换效果。

从Android4.0开始,安卓提供了一个TextureView,可以用来渲染OpenGL,而不需要单独的窗口,这意味着TextureView 可以像任何常规View一样进行操作、设置动画和转换。由于TextureView类中没有内置OpenGL初始化,因此使用TextureView的一种方法是执行自己的OpenGL初始化并在TextureView上运行该初始化;另一个方法是获取GLSurfaceView的源代码,并将其改编为TextureView

补充:为什么在绘制时我们需要不断地清除屏幕?

如果我们已经在每一帧上绘制了整个屏幕,那么清除屏幕似乎浪费时间,我们为什么需要这样做?

在软件中呈现所有内容的年代,清除屏幕通常是浪费时间的。开发人员通过假设所有内容都将被重新绘制来进行优化,因此无需擦除前一帧中的内容。他们这样做是为了节省本来会被浪费的处理时间,但这有时会导致著名的“镜子大厅”(hall of mirrors)效应,如在某些游戏中所看到的:由此产生的视觉效果就像是在一个镜子的大厅中,上一帧的部分内容仍留在屏幕中。

这种优化在今天已不再有用。最新的GPU的工作方式有所不同,它们使用特殊的渲染技术,屏幕被清除实际上可以帮助GPU更快地工作。通过让GPU清除屏幕,我们节省了把前一帧的画面复制到这一帧时浪费的时间。由于目前GPU的工作方式,清除屏幕也有助于避免出现闪烁或无法绘制的问题。保留旧内容可能会导致意外的结果。

关于“hall of mirrors”效应的相关资料:

https://en.wikipedia.org/wiki/Noclip_mode

导入依赖

本系列教程示例采用Kotlin 1.6.10以及Compose 1.1.1编写。

根目录下的build.gradle添加:

dependencies {
    classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10'
}

上述语句是为了保持kotlin版本为1.6.10

在app下面的build.gradle添加:

android {
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion "1.1.1"
    }
}

dependencies {
    def compose_version = "1.1.1"
    
    implementation "androidx.core:core-ktx:1.7.0"
    implementation "androidx.appcompat:appcompat:1.4.1"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
    implementation "com.google.android.material:material:1.5.0"
    
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    implementation "androidx.activity:activity-compose:1.4.0"

    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"

    testImplementation "junit:junit:4.13.2"
    androidTestImplementation "androidx.test.ext:junit:1.1.3"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
}

声明使用OpenGL ES 2.0

在AndroidManifest添加:

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

创建一个简单的GLSurfaceView

class BaseGLSurfaceView
@JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
): GLSurfaceView(context, attrs) {

    init {
        setEGLContextClientVersion(2)
        setRenderer(SampleRenderer())
    }
}

setEGLContextClientVersion()要求创建OpenGL ES 2.0的上下文,将Surface的绘制配置为使用OpenGL ES 2.0。然后我们调用setRenderer()添加渲染器

SampleRenderer

class SampleRenderer: GLSurfaceView.Renderer {
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        GLES20.glClearColor(1F, 0F, 0F, 1F)
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
    }

    override fun onDrawFrame(gl: GL10?) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
    }
}

首先,onSurfaceCreated()内调用了glClearColor(),参数依次是red、green、blue、alpha。我们将红色设置为最大强度,屏幕将在被清除时变为红色(the screen will become red when cleared)(因为clearColor就是clear事件发生之后应当显示的颜色)。

其次,onSurfaceChanged()内调用了glViewport()。这个方法设置了viewport的大小,这告诉了OpenGL可用于渲染的surface的大小。

最后,在onDrawFrame()中,我们调用glClear(GL_COLOR_BUFFER_BIT)擦除了屏幕上的所有颜色,并使用我们之前调用glClearColor()来定义的颜色填充屏幕。

创建简单的Compose视图

创建MainActivity.kt并添加以下代码:

class MainActivity : ComponentActivity() {
    
    private var lifecycleState by mutableStateOf(Lifecycle.Event.ON_ANY)
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 观察lifecycle状态
        lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                super.onResume(owner)
                lifecycleState = Lifecycle.Event.ON_RESUME

            }

            override fun onPause(owner: LifecycleOwner) {
                super.onPause(owner)
                lifecycleState = Lifecycle.Event.ON_PAUSE
            }
        })
        
        setContent {
            // 在Compose下托管传统视图
            AndroidView(
                factory = {
                    BaseGLSurfaceView(context = this)
                },
                update = {
                    // lifecycleState更新时被回调
                    when(lifecycleState) {
                        Lifecycle.Event.ON_PAUSE -> {
                            it.onPause()
                        }
                        Lifecycle.Event.ON_RESUME -> {
                            it.onResume()
                        }
                        else -> {}
                    }
                }
            )
        }
    }
}

这里的setContent()函数是Compose特有的扩展,接收@Composable内容。但是这次我们不需要使用普通的Compose控件,我们使用特殊的Compose控件AndroidView()在Compose中托管一个传统的View。

AndroidView()factory参数接收一个创建传统View的lambda,意在指定创建View的方式。update参数则会捕捉对State的使用,并在State更新时自动重新执行update参数传入的lambda。

我们的界面状态lifecycleStatemutableStateOf创建并托管,这个状态保存了Activity是否位于前台的信息,我们使用这个信息来控制BaseGLSurfaceView,在Activity不可见时停止刷新界面以节省绘制运算,接收状态并控制BaseGLSurfaceView的代码位于传入update的lambda参数内。而lifecycleState的数据更新取决于LifecycleObserver

控制生命周期非常重要,这样我们的GLSurfaceView就可以适当地暂停和恢复背景渲染线程,以及释放和更新OpenGL上下文。

最终效果

我们现在可以运行代码并看见效果:

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐