• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Android OpenGL ES 学习(九) – 坐标系统和实现3D效果

武飞扬头像
夏至的稻穗
帮助1

OpenGL 学习教程
Android OpenGL ES 学习(一) – 基本概念
Android OpenGL ES 学习(二) – 图形渲染管线和GLSL
Android OpenGL ES 学习(三) – 绘制平面图形
Android OpenGL ES 学习(四) – 正交投影
Android OpenGL ES 学习(五) – 渐变色
Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序
Android OpenGL ES 学习(七) – 纹理
Android OpenGL ES 学习(八) –矩阵变换
Android OpenGL ES 学习(九) – 坐标系统和。实现3D效果
代码工程地址: https://github.com/LillteZheng/OpenGLDemo.git

上一章,我们已经学习了矩阵变换,实现了一些特殊的2D效果,这一章,我们来实现更酷的效果 – 3D。效果如下:
学新通

这一章可能会稍微难理解一点,我也是看官网看了几遍,再看懂了一些。所以,这一章说说我的理解,有不对的地方,欢迎大家指正。

前面说到,OpenGL 的坐标范围为 [-1,1] 之间,所以,要求我们在赋值或者矩阵运算的时候,都要进行转换,然后放进 [-1,1] 里面。
把一个物体的顶点坐标,转换成设备坐标,再转换成屏幕坐标,它是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前,还会被变换到多个坐标系统(Coordinate System)。
理解这个过程的好处在于,我们可以理清坐标转换的过程,并在其中加入一些效果,如 3D 效果。

总的来说,OpenGL 共有5个坐标系统。

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

这就是一个顶点在最终被转化为片段之前需要经历的所有不同状态。

一. 坐标概述

为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。我们的顶点坐标起始于局部空间(Local Space),在这里它称为局部坐标(Local Coordinate),它在之后会变为世界坐标(World Coordinate)观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)的形式结束。下面的这张图展示了整个流程以及各个变换过程做了什么:
学新通
意思就是,一个物体,从一个局域坐标到屏幕坐标,需要进过一系列的矩阵变换。

下面解释一下一些专有名词:

1.1 局部空间(Local Space)

是物体相对于自身的原点的坐标,是物体的起点。但是光有这个还不行,因为它没有参照物,放哪里的都可以,所以需要与 模型矩阵 相乘,得到世界坐标。

1.2 世界空间(World Space)

世界空间坐标是一个更大的坐标,你可以随意放在哪个位置,比如放广州,或者深圳,这一步,通常通过矩阵的平移,缩放和放大来实现,但这不能描述物体的物体位置。所以需要与 观察矩阵 相乘,得到一个以人为视角的方向。

1.3 观察空间(View Space)

观察空间也叫 camrea (摄像机)空间 或 用户空间,一个物体,需要有一个观察角度,才能直观地看到这个物体,产生一个以我们为角度的坐标,相当于把物体拉到我们面前。

1.4 裁减空间(Clip Space)

在顶点着色器运行的最后,我们希望把这些坐标都放在一个特定的范围内,超过这个范围的都被裁剪掉。在正交投影那章也讲到,我们实际的屏幕肯定不是 [-1,1] ,这个范围,所以我们需要一些特殊的投影矩阵,帮我们实现把实际物理坐标转换到 [-1,1] 中。

使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。
然后就可以使用 glViewport 或其他渲染模式,把最终的坐标将会被映射到屏幕空间中,并转换成片段。

将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。

1.4.1 正交投影

正交投影,你可以理解成,在用户空间视角,太阳直射这个物体,那么这个物体的影子,跟物体的大小是相等,它的平截面,就是物体本身:
学新通
因此,当我们在画一些二维图形,发现写完坐标之后,长度不太一致,可以使用正交投影去修正 Android OpenGL ES 学习(四) – 正交投影。矩阵公式为:
学新通

1.4.2 透视投影

在实际的生活,我们看东西,离你越远的东西看起来更小。这个奇怪的效果称之为透视(Perspective)。透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,正如下面图片显示的那样:
学新通
在我们认知中,就算铁路再远,它都是两条平行线,永远不可能相交的,但在投影中,它却是可以相交的,为了实现这个理论,引入了 w 分量,也就是齐次坐标(可点击查看齐次坐标)。
我们在坐标的基础上,处于 w 分量,就能得到一个透视的效果
学新通
它的视线方法为:

Matrix.perspectiveM(projectionMatrix,0,45f,aspectRatio,0.3f,100f)

看下图:
学新通
其中 fov 为视觉空间的观察角度,这样看起来比较真实,near 和 far 表示平截面的近平面和远平面,通常设置近距离为0.1f,而远距离设为100.0f,处于这个范围都会被渲染。

二. 进入3D

现在我们按照上面的步骤,实现3D 的效果,上面几个步骤组成一起是:

学新通
注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

顶点着色器保持不变,保留一个 matrix 即可,我们最后再把 model,view 和 projection 结合起来。

private const val VERTEX_SHADER = """#version 300 es
        uniform mat4 u_Matrix;
        layout(location = 0) in vec4 a_Position;
        layout(location = 1) in vec2 aTexture;
        out vec4 vTextColor;
        out vec2 vTexture;
        void main()
        {
            // 矩阵与向量相乘得到最终的位置
            gl_Position = u_Matrix * a_Position;
            vTexture = aTexture;
        
        }
"""

定义一个单位矩阵,并设置 model,view,projection 和最后矩阵结果 mvpMatrix:

private fun getIdentity() =  floatArrayOf(
    1f, 0f, 0f, 0f,
    0f, 1f, 0f, 0f,
    0f, 0f, 1f, 0f,
    0f, 0f, 0f, 1f
)
//获取矩阵
val modelMatrix = getIdentity()
val viewMatrix = getIdentity()
val projectionMatrix = getIdentity()
val mvpMatrix = getIdentity()

2.1 模型矩阵

首先,先使用模型矩阵,把局部空间变成世界空间:

//设置 M
Matrix.rotateM(modelMatrix,0,-55f,1f,0f,0f)

这里向x轴旋转 -55 °

2.2 视图矩阵

接着,再使用视图矩阵,将世界空间,转成视图空间:

//设置 V
Matrix.translateM(viewMatrix,0,0f,0f,-3f)

OpenGL 满足右手坐标系,所以如果要把物体往我们这边靠,就是负的,所以这里向 z 轴移动了 3f 。

2.3 投影矩阵

这里使用透视矩阵,用来模拟除以 w 分量,实现躺平效果:

 //设置 P
 Matrix.perspectiveM(projectionMatrix,0,45f,aspectRatio,0.1f,100f)

最后再把他们组合起来:

 //组合成 mvp,先 v x m
 Matrix.multiplyMM(mvpMatrix,0, viewMatrix,0, modelMatrix,0)
 //然后是 p x v x m
 Matrix.multiplyMM(mvpMatrix,0, projectionMatrix,0, mvpMatrix,0)

 val u_Matrix = getUniform("u_Matrix")
 GLES30.glUniformMatrix4fv(u_Matrix,1,false, mvpMatrix,0)

最后传入顶点着色器进行渲染。

我们的顶点坐标已经使用模型、观察和投影矩阵进行变换了,最终的物体应该会:

  • 稍微向后倾斜至地板方向。
  • 离我们有一些距离。
  • 有透视效果(顶点越远,变得越小)

学新通

三. 3D 立方体

终于到了这个环节,为了实现一个立方体,我们需要准备36个点,6个面 x 每个面有2个三角形组成 x 每个三角形有3个顶点),这36个点可以从 这里 获取。

然后我们需要改变 GlSurface 的渲染模式,改成持续绘制:

 renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY

然后,为了不重新去计算 EBO 的三角形排列,所以,我们使用

GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 36)

来绘制,同理,你需要去掉 EBO 加载数据的赋值:

/*        //创建 ebo
        GLES30.glGenBuffers(1, ebo, 0)
        //绑定 ebo 到上下文
        GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, ebo[0])
        //EBO 数值
        GLES30.glBufferData(
            GLES30.GL_ELEMENT_ARRAY_BUFFER,
            indexData.capacity() * 4,
            indexData,
            GLES30.GL_STATIC_DRAW
        )*/

为了更好的展示,我们也让渲染角度,不断的累加,这样效果更明显,完整的代码如下:

        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)

        GLES30.glBindVertexArray(vao[0])
        Matrix.setIdentityM(modelMatrix, 0)
        Matrix.setIdentityM(viewMatrix, 0)
        Matrix.setIdentityM(projectionMatrix, 0)
        Matrix.setIdentityM(mvpMatrix, 0)

        angle  = 1
        angle %= 360
        //设置 M
        Matrix.rotateM(
            modelMatrix, 0,
            angle,
            0.5f,
            1.0f,
            0f
        )

        //设置 V
        Matrix.translateM(
            viewMatrix,
            0,
            0f,
            0f,
            -4f
        )

        //设置 P
        Matrix.perspectiveM(projectionMatrix, 0, 45f, aspectRatio, 0.3f, 100f)

        //组合成 mvp,先 v x m
        Matrix.multiplyMM(mvpMatrix, 0, viewMatrix, 0, modelMatrix, 0)
        //然后是 p x v x m
        Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, mvpMatrix, 0)

        val u_Matrix = getUniform("u_Matrix")
        GLES30.glUniformMatrix4fv(u_Matrix, 1, false, mvpMatrix, 0)
        //useVaoVboAndEbo
        texture?.apply {
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, id)
        }

        //GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP, 6, GLES30.GL_UNSIGNED_INT, 0)
        GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 36)
学新通

学新通
咦,看起来有点奇怪,怎么感觉所有面都有点穿插了,立方体的某些本应被遮挡住的面被绘制在了这个立方体其他面之上。
之所以会这样,是因为 OpenGL 绘制时,会覆盖之前的像素,所以有些三角形就覆盖在部分三角形上了。

处理这个也比较方便,就是开始 Z 缓冲。

3.1 Z缓冲

Z缓冲也叫深度缓冲(Depth Buffer),看官网怎么解释:

GLFW会自动为你生成这样一个缓冲(就像它也有一个颜色缓冲来存储输出图像的颜色)。深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的。

什么意思呢,比如第一个已经绘制了一个矩形,OpenGL 也会有一个颜色缓冲,但第二个在画的时候,发现前面已经有缓存了,OK,那我就画没被缓存或者说遮挡的部分。

默认它是关闭,所以需要打开:

//开启z轴缓冲,深度测试
  GLES30.glEnable(GLES30.GL_DEPTH_TEST)

因为我们使用了深度测试,我们也想要在每次渲染迭代之前清除深度缓冲(否则前一帧的深度信息仍然保存在缓冲中)。就像清除颜色缓冲一样,我们可以通过在glClear函数中指定DEPTH_BUFFER_BIT位来清除深度缓冲:

GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT or GLES30.GL_DEPTH_BUFFER_BIT)

效果:
学新通

3.2 渲染多个矩形

现在我们想画更多的立方体,每个立方体看起来都是一样的,区别在于它们在世界的位置及旋转角度不同。
所以,我们只需要改变的它模型矩阵就可以了,但我们又像让每个立方体有大小之分,所以也改变它的视图矩阵,这样跟清晰点。
由于屏幕不大,我们设置4个立方体,它的位置如下:

var mulPosition = floatArrayOf(

    0.0f, 0.0f, 0.0f,
    1.2f, 1.2f, -1.0f,
    -1.5f, -1.3f, -2.5f,
    -1.3f, 1.3f, -1.5f
)

然后 for 循环中,去把每个分量的值拿出来即可。

        for (i in 0..boxCount) {
            Matrix.setIdentityM(modelMatrix, 0)
            Matrix.setIdentityM(viewMatrix, 0)
            Matrix.setIdentityM(projectionMatrix, 0)
            Matrix.setIdentityM(mvpMatrix, 0)

            angle  = 1
            angle %= 360
            //设置 M
            Matrix.rotateM(
                modelMatrix, 0,
                angle,
                mulPosition[i * 3]   0.5f,
                mulPosition[i * 3   1]   1.0f,
                mulPosition[i * 3   2]
            )

            //设置 V
            Matrix.translateM(
                viewMatrix,
                0,
                mulPosition[i * 3],
                mulPosition[i * 3   1],
                mulPosition[i * 3   2] - 4f - boxCount
            )

            //设置 P
            Matrix.perspectiveM(projectionMatrix, 0, 45f, aspectRatio, 0.3f, 100f)

            //组合成 mvp,先 v x m
            Matrix.multiplyMM(mvpMatrix, 0, viewMatrix, 0, modelMatrix, 0)
            //然后是 p x v x m
            Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, mvpMatrix, 0)

            val u_Matrix = getUniform("u_Matrix")
            GLES30.glUniformMatrix4fv(u_Matrix, 1, false, mvpMatrix, 0)
            //useVaoVboAndEbo
            texture?.apply {
                GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, id)
            }

            //GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP, 6, GLES30.GL_UNSIGNED_INT, 0)
            GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 36)
        }
学新通

效果:
学新通

参考:
https://learnopengl-cn.github.io/01 Getting started/08 Coordinate Systems/

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgagegh
系列文章
更多 icon
同类精品
更多 icon
继续加载