OpenGL ES for Android 绘制旋转的地球

No 图 No Code,我们先来欣赏下旋转的地球:

是不是很酷炫,要想绘制出上面酷炫的效果需要3个步骤:

  1. 计算球体顶点数据
  2. 地球纹理贴图
  3. 通过MVP矩阵旋转地球

计算球体顶点数据

我们知道OpenGL中最基本的图元是三角形,任何复杂的图形都可以分解为一个个的三角形,球体也不例外,假设球体上有“经纬度”,通过“经纬度”将球体分割为一个个的四边形,如下图:
OpenGL ES for Android 绘制旋转的地球_第1张图片
在把这些四边形分割为2个三角形,所以绘制球体的关键是计算“经纬度”相交的点的坐标。

假设球体的中心在坐标的原点(方便计算),半径为radius,n个经度,m个纬度,计算顶点坐标、索引、纹理坐标方法如下:

fun generateSphere(radius: Float, rings: Int, sectors: Int) {
            val PI = Math.PI.toFloat()
            val PI_2 = (Math.PI / 2).toFloat()

            val R = 1f / rings.toFloat()
            val S = 1f / sectors.toFloat()
            var r: Short
            var s: Short
            var x: Float
            var y: Float
            var z: Float

            val numPoint = (rings + 1) * (sectors + 1)
            val vertexs = FloatArray(numPoint * 3)
            val texcoords = FloatArray(numPoint * 2)
            val indices = ShortArray(numPoint * 6)

            var t = 0
            var v = 0
            r = 0
            while (r < rings + 1) {
                s = 0
                while (s < sectors + 1) {
                    x =
                        (Math.cos((2f * PI * s.toFloat() * S).toDouble()) * Math.sin((PI * r.toFloat() * R).toDouble())).toFloat()
                    y = -Math.sin((-PI_2 + PI * r.toFloat() * R).toDouble()).toFloat()
                    z =
                        (Math.sin((2f * PI * s.toFloat() * S).toDouble()) * Math.sin((PI * r.toFloat() * R).toDouble())).toFloat()

                    texcoords[t++] = s * S
                    texcoords[t++] = r * R

                    vertexs[v++] = x * radius
                    vertexs[v++] = y * radius
                    vertexs[v++] = z * radius
                    s++
                }
                r++
            }

            var counter = 0
            val sectorsPlusOne = sectors + 1
            r = 0
            while (r < rings) {
                s = 0
                while (s < sectors) {
                    indices[counter++] = (r * sectorsPlusOne + s).toShort()       //(a)
                    indices[counter++] = ((r + 1) * sectorsPlusOne + s).toShort()    //(b)
                    indices[counter++] = (r * sectorsPlusOne + (s + 1)).toShort()  // (c)
                    indices[counter++] = (r * sectorsPlusOne + (s + 1)).toShort()  // (c)
                    indices[counter++] = ((r + 1) * sectorsPlusOne + s).toShort()    //(b)
                    indices[counter++] = ((r + 1) * sectorsPlusOne + (s + 1)).toShort()  // (d)
                    s++
                }
                r++
            }

            vertexBuffer = GLTools.array2Buffer(vertexs)
            texBuffer = GLTools.array2Buffer(texcoords)
            mIndicesBuffer = GLTools.array2Buffer(indices)
            indicesNum = indices.size
        }

这个顶点的数据计算需要有比较好的立体感。最难的顶点坐标和纹理坐标已经完成。

顶点shader代码如下:

attribute vec4 a_Position;
attribute vec2 a_TexCoord;
uniform mat4 mvpMatrix;
varying vec2 v_TexCoord;
void main()
{
    v_TexCoord = a_TexCoord;
    gl_Position = mvpMatrix * a_Position;
}

片段shader代码如下:

precision mediump float;

uniform sampler2D u_Texture;
varying vec2 v_TexCoord;

void main()
{
    gl_FragColor = texture2D(u_Texture, v_TexCoord);
}

创建program、取参数句柄并生成顶点数据:

 override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
            GLES20.glClearColor(0F, 0F, 0F, 1F)
            createProgram()
            generateSphere(2F,75,150)
            //获取vPosition索引
            vPositionLoc = GLES20.glGetAttribLocation(mProgramHandle, "a_Position")
            texCoordLoc = GLES20.glGetAttribLocation(mProgramHandle, "a_TexCoord")
            mvpMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "mvpMatrix")
            textureLoc= GLES20.glGetUniformLocation(mProgramHandle, "u_Texture")

        }

private fun createProgram() {
            var vertexCode =
                AssetsUtils.readAssetsTxt(
                    context = context,
                    filePath = "glsl/sphere_vs.glsl"
                )
            var fragmentCode =
                AssetsUtils.readAssetsTxt(
                    context = context,
                    filePath = "glsl/sphere_fs.glsl"
                )
            mProgramHandle = GLTools.createAndLinkProgram(vertexCode, fragmentCode)
        }

sphere_vs.glsl 和 sphere_fs.glsl分别表示顶点shader和片段shader的文件,存放于assets/glsl目录下,readAssetsTxt为读取assets目录下文件的公用方法。generateSphere方式就是开始介绍的顶点数据生成的方法。

地球纹理贴图

地球纹理图片如下:
OpenGL ES for Android 绘制旋转的地球_第2张图片
将地球图片转为纹理,代码如下:

override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
            ...

            var bitmap =
                BitmapFactory.decodeResource(context.resources, R.drawable.earth)
            textureId = GLTools.loadTexture(bitmap)
        }

GLTools.loadTexture为封装的工具类方法,在OpenGL ES for Android 绘制纹理文章中已经详细介绍,图片纹理的相关内容也可以参考此文章。

MVP矩阵

初始化MVP矩阵

var modelMatrix = FloatArray(16)
var viewMatrix = FloatArray(16)
var projectionMatrix = FloatArray(16)

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

            Matrix.setIdentityM(viewMatrix, 0)
            Matrix.setLookAtM(
                viewMatrix, 0,
                0F, 5F, 10F,
                0F, 0F, 0F,
                0F, 1F, 0F
            )

            Matrix.setIdentityM(projectionMatrix, 0)
            val ratio = width.toFloat() / height
            //设置透视投影
            Matrix.frustumM(projectionMatrix, 0, -ratio, ratio, -1f, 1f, 3f, 20f)
        }

绘制并通过MVP矩阵旋转地球

override fun onDrawFrame(p0: GL10?) {
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)

            GLES20.glUseProgram(mProgramHandle)
            //设置顶点数据
            vertexBuffer.position(0)
            GLES20.glEnableVertexAttribArray(vPositionLoc)
            GLES20.glVertexAttribPointer(vPositionLoc, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer)

            //设置纹理顶点数据
            texBuffer.position(0)
            GLES20.glEnableVertexAttribArray(texCoordLoc)
            GLES20.glVertexAttribPointer(texCoordLoc, 2, GLES20.GL_FLOAT, false, 0, texBuffer)

            //设置纹理
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
            GLES20.glUniform1i(textureLoc, 0)

            updateMvpMatrix()
            GLES20.glUniformMatrix4fv(mvpMatrixLoc, 1, false, mMvpMatrix, 0)

            GLES20.glDrawElements(
                GLES20.GL_TRIANGLES,
                indicesNum,
                GLES20.GL_UNSIGNED_SHORT,
                mIndicesBuffer
            )
        }

var currentRotateDegree = 0F
fun updateMvpMatrix(){
            Matrix.setIdentityM(modelMatrix, 0)
            Matrix.rotateM(modelMatrix, 0, currentRotateDegree++, 0F, 1F, 0F)

            var mTempMvMatrix = FloatArray(16)
            Matrix.setIdentityM(mTempMvMatrix, 0)
            Matrix.multiplyMM(mTempMvMatrix, 0, viewMatrix, 0, modelMatrix, 0)
            Matrix.multiplyMM(mMvpMatrix, 0, projectionMatrix, 0, mTempMvMatrix, 0)
        }

到此地球的绘制就结束了,我们经常听说的天空穹、全景(VR)球体模式和地球的绘制基本一样,只不过是相机位置的不同而已。

更多相关阅读:

  • OpenGL ES for Android 总览

  • OpenGL ES for Android 环境搭建

  • OpenGL ES for Android 绘制纹理

  • OpenGL ES for Android 绘制立方体

你可能感兴趣的:(OpenGL,ES,for,Android)