OpenGL ES for Android 绘制旋转的地球

2020-09-11 16:06:09 浏览数 (1)

一个

有态度

的程序员

No 图 No Code,上面旋转的地球是不是很酷炫,下面就让我们开始说说如何绘制旋转地球吧?绘制旋转地球需要3个步骤:

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

计算球体顶点数据

我们知道OpenGL中最基本的图元是三角形,任何复杂的图形都可以分解为一个个的三角形,球体也不例外,假设球体上有“经纬度”,通过“经纬度”将球体分割为一个个的四边形,如下图:

在把这些四边形分割为2个三角形,所以绘制球体的关键是计算“经纬度”相交的点的坐标。

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

代码语言:javascript复制
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代码如下:

代码语言:javascript复制
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代码如下:

代码语言:javascript复制
precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoord;
void main()
{
    gl_FragColor = texture2D(u_Texture, v_TexCoord);
}

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

代码语言:javascript复制
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方式就是开始介绍的顶点数据生成的方法。

地球纹理贴图

地球纹理图片如下:

将地球图片转为纹理,代码如下:

代码语言:javascript复制
override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
            ...
            var bitmap =
                BitmapFactory.decodeResource(context.resources, R.drawable.earth)
            textureId = GLTools.loadTexture(bitmap)
        }

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

MVP矩阵

初始化MVP矩阵代码如下:

代码语言:javascript复制


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矩阵旋转地球

代码语言:javascript复制
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)球体模式和地球的绘制基本一样,只不过是相机位置的不同而已。

0 人点赞