Android OpenGL画第一个图形(一)


接着上篇,我们接下来将通过OpenGL ES实现一个简单的几何图形显示,在本次学习过程中我们将会了解到OpenGL的坐标系、顶点、着色器、OpenGL程序以及OpenGL开发过程中的调试相关知识。

考虑到篇幅可能会过长,所以本次的内容我将会分为两篇编写,本篇我们首先了解OpenGL ES的坐标系,顶点和着色器。而在下一篇我们将会就本篇的顶点,着色器编译及关联并绘制出我们第一个图形。

OpenGL中的坐标系

OpenGL如何将定义的坐标映射到屏幕上的实际物理坐标是一个复杂的问题,随着对OpenGL更深入的了解,我们将会逐渐了解更多内容。

目前,我们只需要知道无论是x还是y坐标,OpenGL 都会把屏幕会映射到[-1,1]的范围内。对应关系为屏幕左侧对应x轴的-1,屏幕右侧对应x轴的+1,屏幕底部对应y轴的-1,屏幕顶部对应y轴的+1。不论屏幕的实际形状和大小如何,OpenGL都会使用相同的坐标范围进行映射。要在屏幕上显示任何内容,都需要在这个标准化的坐标范围内进行绘制。

在这里插入图片描述

定义矩形结构

在通过OpenGL画图形之前,我们需要告知OpenGL要画什么。在这里我们要画的第一个图形是二维的矩形。所以我们开发的第一步就是以OpenGL能理解的形式定义一个矩形结构。在OpenGL里,所有东西的结构都是从一个顶点开始的。

1. 顶点介绍

所谓的顶点即一个代表几何对象拐角的点。顶点具有多个附加属性,其中最重要的是位置属性。位置属性决定了顶点在空间中的具体定位。

2. OpenGL中的基本图形

在OpenGL只能绘制点、直线和三角形。三角形是最基本的几何图形,因其结构稳定而被广泛应用于构建复杂对象和纹理的场景。

三角形是构建复杂图形的基础。点和支线可以用于某些效果,但是只有三角形才能用来构建拥有复杂的对象和纹理的场景。在OpenGL里我们把单独的点放在一个组里构建出三角形,再告诉OpenGL如何连接这些点。

我们想要构建的所有东西都要用点、直线和三角形定义。如果想要构建更复杂的图形,例如拱形,那么我们就需要用足够的点拟合这样的曲线。

虽然我们本篇是要画一个矩形,但实际上,矩形可以被看作是由两个三角形组成的。这意味着需要使用点来构建三角形,并将这些三角形组合起来形成矩形的形状。

3. 在代码中定义矩形

一个矩形有4个拐角,因此需要4个顶点来定义其形状。每个顶点都需要一个位置,这个位置在二维空间中由两个坐标(通常是x和y坐标)确定。

接下来让我们写一些代码来存储这些顶点,顶点是二维坐标系中的点,每个顶点需要用两个浮点数来标记其位置,一个表示x轴的位置,另一个表示y轴的位置。

因为一个顶点有两个分量,因此让我们定义了一个常量来记录这个事实。

    private val POSITION_COMPONENT_COUNT = 2

    private var rectangleVertices  = floatArrayOf(
        -0.5f , -0.5f,
         0.5f ,  0.5f,
        -0.5f ,  0.5f,

        -0.5f , -0.5f,
         0.5f , -0.5f,
         0.5f ,  0.5f
    )

在这里使用了浮点数的顺序列表来定义顶点数据,这种数组通常被称为顶点属性(attribute)数组。顶点属性数组目前用来存储顶点的位置信息,这些信息以有小数的十进制数表示。

这个数组表示用六个顶点表示两个三角形。第一个三角形由顶点(-0.5f , -0.5f)、(0.5f , 0.5f)和(-0.5f , 0.5f)连接组成。第二个三角形共用了前面三角形中顶点中的两个,由顶点(-0.5f , -0.5f)、(0.5f , -0.5f)和(0.5f , 0.5f)构成。在OpenGL中表示物体时,需要考虑如何用点、直线和三角形把它们组合出来。

可能细心的同学也已经注意到了,当定义三角形时,顶点总是以逆时针的顺序排列。这种顺序称为卷曲顺序(winding order)。使用一致的卷曲顺序可以优化性能,因为它可以帮助OpenGL确定哪些三角形属于给定物体的前面或后面,从而可以忽略那些无法被看到的后面的三角形。后续我们将了解到更多关于这样的内容。

4. 使数据可以被OpenGL存取

现在已经完成了顶点的定义,但是在OpenGL可以存取他们之前,我们仍然需要完成另外一部分。主要的问题是这些代码运行环境与OpenGL运行环境使用了不同的语言,我们需要理解如下两个主要的概念:

  1. 当我们在模拟器或设备上编译和运行Java/kotlin代码时,它并不是直接运行在硬件上;相反,它运行在一个特殊的环境上,即Java虚拟机(Java virtual machine)。运行在虚拟机上的代码不能直接访问本地环境(native environment),除非通过特定的API。
  2. 虚拟机使用了垃圾回收(garbage collection)机制。这意味着,当虚拟机检测到一个变量、对象或者其他内存片段不再被使用时,就会把这些内存释放掉以备重用;它也能腾挪内存以提高空间使用效率。本地环境并不是这样工作的,它不期望内存块会被移来移去或者被自动释放。

Android之所以这样设计,是因为开发者在开发程序的时候不必关心特定的CPU或者机器架构,也不必关心底层的内存管理。这通常都能工作得很好,除非要与本地系统交互,比如OpenGL,OpenGL作为本地系统库直接运行在硬件上,没有虚拟机,也没有垃圾回收或内存压缩。

4.1 Java/Kotlin 调用本地代码

虚拟机方案是Android的主要特点之一,但是,如果代码运行在虚拟机内部,那它怎么与OpenGL通信呢?有两种技术,第一种技术是使用Java本地接口(JNI),这个技术已经由Android软件开发包提供了;当调用android.opengl.GLES20包里的方法时,软件开发包实际上就是在后台使用JNI调用本地系统库的。

4.2 把内存从Java/Kotlin堆复制到本地堆

改变内存分配的方式,Java有一个特殊的类集合,它们可以分配本地内存块,并且把Java的数据复制到本地内存。本地内存可以被本地环境存取,而不受垃圾回收器的管控。

    private val BYTE_PER_FLOAT = 4
    private var vertexData:FloatBuffer

整型常量BYTES_PERFLOAT:定义了一个表示每个浮点数(float)所占用字节数的常量。由于一个浮点数有32位(bit)精度,而每个字节(byte)有8位,因此每个浮点数占用4个字节。
FloatBuffer类型变量:引入了一个FloatBuffer类型的变量,它用于在本地内存中存储浮点数数据。
本地内存存储:FloatBuffer允许Java的数据被复制到本地内存,这样本地环境(如OpenGL)就可以直接访问这些数据,而不受Java虚拟机的垃圾回收器的影响。

接着完善代码:

    vertices = ByteBuffer.allocateDirect(rectangleVertices.size*BYTE_PER_FLOAT)
                         .order(ByteOrder.nativeOrder())
                         .asFloatBuffer()
    vertices.put(rectangleVertices)

本地内存分配:使用ByteBuffer.allocateDirect()分配一块本地内存,这块内存不会被Java的垃圾回收器管理。
内存大小计算:内存大小应为顶点数组长度乘以每个浮点数占用的字节数(BYTES_PER_FLOAT),因为每个浮点数占用4个字节。
字节序设置:通过调用order(ByteOrder.nativeOrder()),设置字节缓冲区按照本地字节序组织内容,确保跨平台的一致性。
转换为FloatBuffer:通过调用asFloatBuffer(),将字节缓冲区转换为FloatBuffer,以便使用浮点数进行操作。
数据复制:使用vertexData.put(tableVerticesWithTriangles)将数据从虚拟机内存复制到本地内存。

虽然一般情况下不需要手动释放分配的本地内存,但如果程序中创建了大量ByteBuffer,可能需要学习内存管理和堆碎片化技术。理解数据从虚拟机传递到OpenGL的过程非常重要,这涉及到跨越本地代码边界时的差异。

OpenGL管道

相关的数据已经被复制到OpenGL可以访问的本地内存中。在屏幕上绘制内容之前,数据需要在OpenGL的渲染管线(pipeline)中传递。着色器是渲染管线中的子例程,用于告诉图形处理单元(GPU)如何绘制数据。

存在两种类型的着色器,它们需要在绘制任何内容到屏幕之前被定义。

顶点着色器(Vertex Shader):负责生成每个顶点的最终位置。对每个顶点执行一次,确定其在屏幕上的最终位置。一旦顶点的最终位置确定,OpenGL会将可见顶点组装成点、直线和三角形。
片段着色器(Fragment Shader):为构成点、直线或三角形的每个片段生成最终的颜色。对每个片段执行一次,一个片段可以类比为屏幕上的一个像素。一旦最终颜色生成,OpenGL将这些颜色写入帧缓冲区(Framebuffer)。

OpenGL首先通过顶点着色器处理顶点数据。然后,使用片段着色器为渲染的几何形状的每个片段确定颜色。最终,颜色数据被写入帧缓冲区,由Android系统显示到屏幕上。

OpenGL 管道概述:
在这里插入图片描述

为什么要引入着色器:

在着色器出现之前,OpenGL使用固定功能的渲染管线,这限制了可实现的效果,如场景中的光线数量和雾化效果。这些固定API虽然易于使用,但难以扩展,限制了自定义效果的实现,例如卡通着色。随着图形硬件的提升,OpenGL的设计者意识到需要更新API以适应这些变化。 在OpenGL ES2.0中,引入了着色器和可编程API,同时完全移除了固定功能的API。 使用着色器,用户现在可以控制每个顶点和片段的绘制方式,实现了更高级的渲染效果。 着色器的引入打开了实现自定义渲染效果的新世界,包括按像素实现光照和卡通着色等。 只要能够用着色器语言描述,就可以实现任何理想的自定义效果。

1. 创建顶点着色器

在项目res/raw目录下的创建文件simple_vertex_shader.glsl

attribute vec4 a_Position;

void main(){
    gl_Position = a_Position;
}

着色器语言:着色器使用GLSL(OpenGL Shading Language)定义,其语法结构与C语言相似。
顶点属性:每个顶点着色器调用时,会在a_Position属性中接收当前顶点的位置,该属性被定义为vec4类型。
vec4类型:vec4是一个包含4个分量的向量,通常表示为x、y、z和w坐标。其中,x、y、z代表三维空间中的一个点,而w是齐次坐标,更多细节后续将会逐一了解。
默认值:如果未指定,OpenGL默认将向量的前三个坐标设置为0,并将第四个坐标设置为1。
属性关键字:使用关键字attribute将顶点的属性(如颜色和位置)传递给着色器。
主要入口点:main()函数是着色器的主要入口,它将接收到的位置复制到输出变量gl_Position。
gl_Position的作用:顶点着色器必须给gl_Position赋值,OpenGL使用这个变量的值作为当前顶点的最终位置,并将这些顶点组装成点、直线和三角形。
main()函数:这是着色器的主入口,在这里main()函数的任务是将传入的顶点位置复制到输出变量gl_Position。OpenGL使用gl_Position中的值来确定顶点在屏幕上的最终位置,并将这些顶点组装成点、直线和三角形。

2. 创建片段着色器

我们已经创建了顶点着色器,就有了为每个顶点生成最终位置的子例程。但我们仍然需要创建一个为每个片段生成最终颜色的子例程。

光栅化技术

光栅化是将几何图形转换为像素的过程,这是渲染管线中的一个步骤。移动设备的显示屏由成千上万个像素组成,每个像素能显示数百种颜色中的一种。大多数显示器通过三个单独的子组件(红色、绿色、蓝色)发出光来显示颜色,人眼将这些颜色混合,创造出更广泛的颜色范围。

由于像素非常小,人眼将单独的红、绿、蓝光混合,形成丰富的颜色效果。通过组合足够多的像素,可以显示出文本、图片等视觉内容。

OpenGL通过光栅化将点、直线和三角形分解成小片段,这些片段可以映射到显示屏的像素上,生成图像。每个片段类似于显示屏上的一个像素,包含单一的纯色。每个片段有4个分量来表示颜色,分别是红色(Red)、绿色(Green)、蓝色(Blue)和阿尔法(Alpha),其中红绿蓝用于表示颜色,阿尔法用于表示透明度。
关于这个颜色模型是如何工作的,我们下一篇将会讲到更多细节。

编写代码

片段着色器的主要任务是确定每个片段的最终颜色。对于基本图元的每个片段,片段着色器都会被调用一次。如果一个三角形映射到10000个片段,片段着色器将被调用10000次。

在res/raw下创建simple_fragment_shader.glsl

precision mediump float;

uniform vec4 u_Color;

void main(){
   gl_FragColor = u_Color;
}

精度限定符:片段着色器可以定义浮点数据类型的默认精度:lowp(低精度)、mediump(中等精度)、highp(高精度)。只有部分硬件支持在片段着色器中使用highp。顶点着色器默认精度设置为highp,因为顶点位置的精确度非常重要。
精度与性能权衡:高精度数据类型提供更精确的结果,但可能会降低性能。出于兼容性和性能的考虑,片段着色器选择了mediump。
uniform变量:uniform不同于属性(attribute),它为所有顶点提供相同的值,除非显式改变。u_Color是一个uniform变量,是一个四分量向量,代表红色、绿色、蓝色和阿尔法值。
颜色生成:片段着色器的main()函数将uniform中定义的颜色复制到输出变量gl_FragColor。着色器必须给gl_FragColor赋值,OpenGL将使用这个颜色作为当前片段的最终颜色。

小结

本篇我们就先了解到这里,更加精彩内容将会在下一篇体现,本篇我们学到了如何定义顶点属性数组,并将该数组复制到本地内存中,以便OpenGL可以访问。写了顶点着色器和片段着色器,理解到着色器是一种在GPU上运行的特殊程序。

下一篇将了解如何读取和编译这些着色器,并将顶点着色器和片段着色器链接在一起形成一个完整的OpenGL程序。一旦着色器被编译并链接,将学习如何将所有内容整合,并指示如何显示我们所指定的矩形。

  • 16
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值