老
孟
一个
有态度
的程序员
No 图 No Code,上面旋转的地球是不是很酷炫,下面就让我们开始说说如何绘制旋转地球吧?绘制旋转地球需要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)球体模式和地球的绘制基本一样,只不过是相机位置的不同而已。