八、OpenGL ES 构建简单物体

许多应用都是合并简单的图形来构建更复杂的物体,我们还缺少一个容易的方法在场景中平移,旋转和来回移动,许多三维应用程序使用一个视图(view)矩阵实现这些,对这个矩阵所做的改动会影响整个场景,就像我们从一个移动相机中观察事物一样,我们将添加一个视图矩阵使它更容易旋转和来回移动。
接下来会学习如何把三角形组织成为三角形带和三角形扇,并把它们合并在一起成为一个单个物体。

来看看最终的一个效果图:

一、合并三角形带和三角形扇

想象一下一个冰球(一个扁平的圆柱体),它其实是由顶部的圆形和侧面的长方形组成

要构建圆柱体的侧面,我们需要使用三角形带(Triangle Strip)来完成,三角形带是指定顶点序列,按顺序每三个顶点画一个三角形,排列成一个长方形,如下图所示:


为了使用三角形带定义一个圆柱体的侧面,我们只需要把这个带卷成一个管子,并确保最后两个顶点与前面的两个顶点一致。

而构建圆形,我们可以使用一个三角形扇,指定顶点序列,第一个为顶点,后续按顺序每两个顶点与第一个顶点形成三角形,如下图所示:

而要构建如下图所示的木槌,我们这需要用2个三角形扇和2个三角形带来完成。

二、添加几何图形的类

为了更容易的构建三角形扇和三角形带,我们将定义一个几何图形的类,他可以容纳一些基本的图形定义,以及一个用来做实际构建工作的ObjectBuilder。

首先让我们以这个几何物体的类作为开始,创建一个Geometry的新类用来管理几何图形的类

class Geometry {

    // 点
    class Point(val x: Float, val y: Float, val z: Float) {
        // 平移
        fun translateY(distance: Float): Point {
            return Point(x, y + distance, z)
        }

    }

    // 圆
    class Circle(val center: Point, val radius: Float) {
        // 缩放
        fun scale(scale: Float): Circle {
            return Circle(center, radius * scale)
        }

    }

    // 圆柱体,它有一个中心,一个半径和高度
    class Cylinder(val center: Point, val radius: Float, val height: Float)
}

三、添加图形构建类

先上代码


import android.opengl.GLES20.*
import com.example.openglstudy.util.Geometry


/**
 * @Author: mChenys
 * @Date: 2020/12/31
 * @Description:构建图形
 */
class ObjectBuilder private constructor(sizeInVertices: Int) {

    // 保存顶点数据的数组
    private val vertexData: FloatArray
    // 用于添加所有的绘制命令的集合
    private val drawList: MutableList<DrawCommand> = ArrayList()
    // 顶点数组填充数据时的index位置
    private var offset = 0

    init {
        // sizeInVertices顶点数
        vertexData = FloatArray(sizeInVertices * FLOATS_PER_VERTEX)
    }

    /**
     * 定义一个绘制接口
     */
    interface DrawCommand {
        fun draw()
    }

    /**
     * 构建数据
     */
    class GeneratedData(val vertexData: FloatArray, val drawList: List<DrawCommand>)

    /**
     * 返回一个构建数据
     */
    private fun build(): GeneratedData {
        return GeneratedData(vertexData, drawList)
    }

    // 静态方法
    companion object {
        private const val FLOATS_PER_VERTEX = 3 //每个顶点3个分量(x,y,z)

        // 计算圆的顶点数量的方法
        private fun sizeOfCircleInVertices(numPoints: Int): Int {
            return 1 + (numPoints + 1) //中心点+圆周上的点+最后一个和第一个重复的点
        }

        // 计算长方形(圆柱体的侧面是一个卷起来的长方形,由一个三角形带组成)的顶点数量
        private fun sizeOfOpenCylinderInVertices(numPoints: Int): Int {
            return (numPoints + 1) * 2 //长方形上边的顶点和下边顶点一样,所以要乘以2,然后还要+2是因为围着圆的第一个顶点要重复2次才能使圆闭合
        }

        /**
         * 一个冰球由顶部的圆和侧面的圆柱体组成,因此所有的顶点数量就是这2部分的总和
         */

        // 创建冰球
        fun createPuck(puck: Geometry.Cylinder, numPoints: Int): GeneratedData {
            // 总的顶点数量 = 画圆的顶点数 + 圆柱体的顶点数
            val size = (sizeOfCircleInVertices(numPoints)
                    + sizeOfOpenCylinderInVertices(numPoints))
            // 创建builder对象,传入总顶点数
            val builder = ObjectBuilder(size)
            // 创建圆
            val puckTop = Geometry.Circle(
                puck.center.translateY(puck.height / 2f), // 由于center.y在圆柱体的中间位置,所以顶部的圆的y坐标应该是center.y+height/2
                puck.radius //圆的半经
            )
            // 添加圆
            builder.appendCircle(puckTop, numPoints)
            // 添加圆柱体
            builder.appendOpenCylinder(puck, numPoints)
            return builder.build()
        }

        // 创建木槌(底部是一个扁的圆柱体,上面是一个圆柱体的手柄),因此构造木槌其实和构造2个冰球差不多
        fun createMallet(
            center: Geometry.Point,
            radius: Float,
            height: Float,
            numPoints: Int
        ): GeneratedData {
            // 总顶点数,2个圆(顶部圆柱的上表面+底部圆柱的上表面)+2个侧面(顶部圆柱的侧面+底部圆柱的侧面)
            val size = (sizeOfCircleInVertices(numPoints) * 2
                    + sizeOfOpenCylinderInVertices(numPoints) * 2)
            // 创建builder对象,传入总顶点数
            val builder = ObjectBuilder(size)

            // 木槌的底部圆柱的高度占总高度的25%
            val baseHeight = height * 0.25f
            val baseCircle = Geometry.Circle(
                center.translateY(-baseHeight), // 底部圆
                radius
            )
            val baseCylinder = Geometry.Cylinder(  // 圆柱体
                baseCircle.center.translateY(-baseHeight / 2f),
                radius, baseHeight
            )
            builder.appendCircle(baseCircle, numPoints)
            builder.appendOpenCylinder(baseCylinder, numPoints)

            // 木槌的顶部圆柱的高度占总高度的75%
            val handleHeight = height * 0.75f
            val handleRadius = radius / 3f
            val handleCircle = Geometry.Circle(
                center.translateY(height * 0.5f),
                handleRadius
            )
            val handleCylinder = Geometry.Cylinder(
                handleCircle.center.translateY(-handleHeight / 2f),
                handleRadius, handleHeight
            )
            builder.appendCircle(handleCircle, numPoints)
            builder.appendOpenCylinder(handleCylinder, numPoints)
            return builder.build()
        }


    }


    // 添加圆
    private fun appendCircle(circle: Geometry.Circle, numPoints: Int) {
        // 计算当前的开始顶点位置
        val startVertex = offset / FLOATS_PER_VERTEX
        // 计算圆形的顶点数量
        val numVertices = sizeOfCircleInVertices(numPoints)

        // 先确定圆心顶点(x,y,z),因为我们只使用了一个数组保存所有物体的数据,所以需要需要计算每个顶点的偏移量
        vertexData[offset++] = circle.center.x
        vertexData[offset++] = circle.center.y
        vertexData[offset++] = circle.center.z

        // 接着围绕圆心的点按扇形展开
        for (i in 0..numPoints) {
            // 为了生成一个圆周边的点,我们首先需要一个循环来计算它扫过的弧度,它的范围涵盖从0到360°的整个圆,或者0到2π弧度
            val angleInRadians = (i.toFloat() / numPoints.toFloat()
                    * (Math.PI.toFloat() * 2f))
            // 需要找到圆周上的一个点的x坐标,我们需要调用cos函数, x坐标 = 圆心x+cos角度*半径
            vertexData[offset++] = ((circle.center.x
                    + circle.radius * Math.cos(angleInRadians.toDouble())).toFloat())
            // y轴坐标其实刚好和圆心顶点的y坐标一样,按照右手坐标系统可知,当俯视看这个圆的时候,y的坐标刚好在圆的表面上
            vertexData[offset++] = circle.center.y
            // z轴坐标也就是垂直x轴和y轴的(按照右手坐标系统可知),需要调用sin函数来求, z坐标 = 圆心z+sin角度*半径
            vertexData[offset++] = ((circle.center.z
                    + circle.radius * Math.sin(angleInRadians.toDouble())).toFloat())
        }
        // 加到绘制列表中
        drawList.add(object : DrawCommand {
            override fun draw() {
                glDrawArrays(GL_TRIANGLE_FAN, startVertex, numVertices)
            }
        })
    }

    // 添加圆柱
    private fun appendOpenCylinder(cylinder: Geometry.Cylinder, numPoints: Int) {
        // 计算当前的开始顶点位置
        val startVertex = offset / FLOATS_PER_VERTEX
        // 计算圆柱体的顶点数量
        val numVertices = sizeOfOpenCylinderInVertices(numPoints)
        // 圆柱的底部顶点中心的y坐标
        val bottomCenterY: Float = cylinder.center.y - cylinder.height / 2f
        // 圆柱的顶部顶点中心的y坐标
        val topCenterY: Float = cylinder.center.y + cylinder.height / 2f

        // 循环填侧面的顶点
        for (i in 0..numPoints) {
            // 角度
            val angleInRadians = (i.toFloat() / numPoints.toFloat()
                    * (Math.PI.toFloat() * 2f))
            // x坐标 = 圆心x+cos角度*半径
            val xPosition: Float = ((cylinder.center.x
                    + cylinder.radius * Math.cos(angleInRadians.toDouble())).toFloat())

            // z坐标 = 圆心z+sin角度*半径
            val zPosition: Float = ((cylinder.center.z
                    + cylinder.radius * Math.sin(angleInRadians.toDouble())).toFloat())
            // 圆柱侧面的底部顶点(x,y,z)
            vertexData[offset++] = xPosition
            vertexData[offset++] = bottomCenterY
            vertexData[offset++] = zPosition
            // 圆柱侧面的顶部顶点(x,y,z)
            vertexData[offset++] = xPosition
            vertexData[offset++] = topCenterY
            vertexData[offset++] = zPosition
        }
        // 加到绘制列表中
        drawList.add(object : DrawCommand {
            override fun draw() {
                glDrawArrays(GL_TRIANGLE_STRIP, startVertex, numVertices)
            }
        })
    }


}

首先在构造方法初始化的时候定义了一个用于存储所有顶点的数组,然后定义了一个静态接口DrawCommand和构建数据的静态内部类GeneratedData

接着在伴生类中定义计算圆面和圆柱体侧面顶点数量的方法sizeOfCircleInVertices和sizeOfOpenCylinderInVertices,圆是由三角形扇构建的,所以顶点数量=中心点+圆周上的点+最后一个和第一个重复的点;而圆柱体侧面是由三角形带构建的,所以顶点数量=长方形上边顶点+下边顶点+最有一个重复的顶点。

接着在伴生类中定义createPuck方法用来创建冰球

一个冰球由顶部的圆和侧面的圆柱体组成,因此所有的顶点数量就是这2部分的总和,中心点坐标位于圆柱体的中心,所以顶部的圆面的圆周上的y坐标需要通过center.y+(height/2),然后通过appendCircle和appendOpenCylinder来添加圆形和圆柱侧面,需要注意的是构建圆形的时候,按照右手坐标系统可知圆周上的点的x和y坐标是需要借助z坐标来求的,因为俯瞰这个圆的时候,圆心的y坐标刚好落在圆的表面,而z轴刚好和x轴垂直,所以x坐标 = 圆心x+cos角度*半径,z坐标 = 圆心z+sin角度*半径

而圆柱的侧面是由三角形带组成的,侧面的上边(可以看做是顶部圆周)的y坐标是由center.y+(height/2)来求,而侧面的下边(可以看做是底部圆周)的y坐标是由center.y-(height/2)来求,如下图所示:

y坐标确定了,剩下的x和z坐标可以参考前面生成圆周顶点的算法,x坐标 = 圆心x+cos角度*半径,z坐标 = 圆心z+sin角度*半径

然后绘制木槌的时候其实就是绘制2个圆柱体,如下图所示,总顶点数=2个圆(顶部圆柱的上表面+底部圆柱的上表面)+2个侧面(顶部圆柱的侧面+底部圆柱的侧面)

最后所有顶点的绘制都会添加到drawList中,当调用build方法的时候就会返回一个封装了所有顶点绘制命令的GeneratedData数据类。

四、更新物体

既然有了物体构建器,那就不用使用点来表示木槌了,新建一个Puck类表示冰球


import com.example.openglstudy.data.VertexArray
import com.example.openglstudy.objects.ObjectBuilder.DrawCommand
import com.example.openglstudy.programs.ColorShaderProgram
import com.example.openglstudy.util.Geometry


/**
 * @Author: mChenys
 * @Date: 2021/1/11
 * @Description: 冰球
 */
class Puck(val radius: Float, val height: Float, val numPointsAroundPuck: Int) {

    private val POSITION_COMPONENT_COUNT = 3
    private var vertexArray: VertexArray // 顶点数组
    private var drawList: List<DrawCommand> // 绘制列表

    init {
        // 构建冰球的顶点数据
        val generatedData: ObjectBuilder.GeneratedData = ObjectBuilder.createPuck(
            Geometry.Cylinder(Geometry.Point(0f, 0f, 0f), radius, height),
            numPointsAroundPuck // 需要多少个点构建
        )
        vertexArray = VertexArray(generatedData.vertexData);
        drawList = generatedData.drawList;
    }

    // 把顶点数据绑定到着色器程序定义的属性上
    fun bindData(colorShaderProgram: ColorShaderProgram) {
        vertexArray.setVertexAttributePointer(
            0,
            colorShaderProgram.getPositionAttributeLocation(),
            POSITION_COMPONENT_COUNT,
            0
        )
    }

    // 绘制顶点
    fun draw() {
        for (drawCommand in drawList) {
            drawCommand.draw()
        }
    }
}

接着创建Mallet类表示木槌


import com.example.openglstudy.data.VertexArray
import com.example.openglstudy.objects.ObjectBuilder.DrawCommand
import com.example.openglstudy.programs.ColorShaderProgram
import com.example.openglstudy.util.Geometry


/**
 * @Author: mChenys
 * @Date: 2021/1/11
 * @Description:木槌
 */
class Mallet(val radius: Float, val height: Float, val numPointsAroundMallet: Int) {
    
    private val POSITION_COMPONENT_COUNT = 3
    private val vertexArray: VertexArray
    private val drawList: List<DrawCommand>

    init {
        val generatedData = ObjectBuilder.createMallet(
            Geometry.Point(0f, 0f, 0f), radius, height, numPointsAroundMallet
        )
        vertexArray = VertexArray(generatedData.vertexData);
        drawList = generatedData.drawList;
    }

    fun bindData(colorProgram: ColorShaderProgram) {
        vertexArray.setVertexAttributePointer(
            0,
            colorProgram.getPositionAttributeLocation(),
            POSITION_COMPONENT_COUNT, 0
        )
    }

    fun draw() {
        for (drawCommand in drawList) {
            drawCommand.draw()
        }
    }
}

创建桌子Table


import android.opengl.GLES20.GL_TRIANGLE_FAN
import android.opengl.GLES20.glDrawArrays
import com.example.openglstudy.Constants.BYTES_PER_FLOAT
import com.example.openglstudy.data.VertexArray
import com.example.openglstudy.programs.TextureShaderProgram


/**
 * @Author: mChenys
 * @Date: 2021/1/11
 * @Description: 桌子
 */
class Table {
    
    private val POSITION_COMPONENT_COUNT = 2
    private val TEXTURE_COORDINATES_COMPONENT_COUNT = 2
    private val STRIDE: Int = (POSITION_COMPONENT_COUNT
            + TEXTURE_COORDINATES_COMPONENT_COUNT) * BYTES_PER_FLOAT

    private val VERTEX_DATA = floatArrayOf( // Order of coordinates: X, Y, S, T
        // Triangle Fan
        0f, 0f, 0.5f, 0.5f,
        -0.5f, -0.8f, 0f, 0.9f,
        0.5f, -0.8f, 1f, 0.9f,
        0.5f, 0.8f, 1f, 0.1f,
        -0.5f, 0.8f, 0f, 0.1f,
        -0.5f, -0.8f, 0f, 0.9f
    )

    private var vertexArray: VertexArray

    init {
        vertexArray = VertexArray(VERTEX_DATA)
    }

    fun bindData(textureProgram: TextureShaderProgram) {
        vertexArray.setVertexAttributePointer(
            0,
            textureProgram.getPositionAttributeLocation(),
            POSITION_COMPONENT_COUNT,
            STRIDE
        )
        vertexArray.setVertexAttributePointer(
            POSITION_COMPONENT_COUNT,
            textureProgram.getTextureCoordinatesAttributeLocation(),
            TEXTURE_COORDINATES_COMPONENT_COUNT,
            STRIDE
        )
    }

    fun draw() {
        glDrawArrays(GL_TRIANGLE_FAN, 0, 6)
    }
}

五、更新着色器

现在还需要更新颜色着色器,因为我们用每个顶点的位置而不是每个顶点的颜色定义了冰球和木槌,因此不得不把颜色作为一个uniform传递进去,修改ShaderProgram类,加入一个新的常量 protected val U_COLOR = "u_Color" 

然后更新ColorShaderProgram,移除所有aColorLocation的引用,包括getColorAttributeLocation方法,还需要更新顶点着色器源码simple_vertex_shader4.glsl

uniform mat4 u_Matrix;
attribute vec4 a_Position;
void main()
{
    gl_Position = u_Matrix * a_Position;
}

和片段着色器源码simple_fragment_shader4.glsl

precision mediump float;

uniform vec4 u_Color;

void main()
{
    gl_FragColor = u_Color;
}

然后在ColorShaderProgram中使用这个着色器

class ColorShaderProgram(context: Context) : ShaderProgram(
    context,
    R.raw.simple_vertex_shader4,
    R.raw.simple_fragment_shader4
) {
    // Uniform locations
    private var uMatrixLocation = 0
    private var uColorLocation = 0
    // Attribute locations
    private var aPositionLocation = 0

    init {
        // Retrieve uniform locations for the shader program.
        uMatrixLocation = glGetUniformLocation(program, U_MATRIX);
        uColorLocation = glGetUniformLocation(program, U_COLOR);
        // Retrieve attribute locations for the shader program.
        aPositionLocation = glGetAttribLocation(program, A_POSITION);
    }

    fun setUniforms(
        matrix: FloatArray?,
        r: Float,
        g: Float,
        b: Float
    ) {
        glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0)
        glUniform4f(uColorLocation, r, g, b, 1f)
    }

    fun getPositionAttributeLocation(): Int {
        return aPositionLocation
    }
}

六、更新Renderer类

前面我们学习了使用正交投影矩阵来调整宽高比,使用透视投影矩阵获取三维投影以及使用模型矩阵来移动物体。
接下来需要学习视图矩阵,视图矩阵是模型矩阵的扩展,它的使用时出于同样的目的,只不过它是平等的引用与场景中的每个物体
因为它影响的是所有物体,所以功能上等同于一个相机,来回的移动相机就会从不同的视角看见哪些东西了。


class AirHockeyRenderer(private val context: Context) : GLSurfaceView.Renderer {

    private val projectionMatrix = FloatArray(16) // 投影矩阵
    private val modelMatrix = FloatArray(16) // 模型矩阵
    private val viewMatrix = FloatArray(16) // 视图矩阵
    private val viewProjectionMatrix = FloatArray(16)
    private val modelViewProjectionMatrix = FloatArray(16)

    private lateinit var table: Table
    private lateinit var mallet: Mallet
    private lateinit var puck: Puck

    private lateinit  var textureProgram: TextureShaderProgram
    private lateinit var colorProgram: ColorShaderProgram

    private var texture = 0


    override fun onSurfaceCreated(
        glUnused: GL10?,
        config: EGLConfig?
    ) {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
        table = Table()
        mallet = Mallet(0.08f, 0.15f, 32)
        puck = Puck(0.06f, 0.02f, 32)
        textureProgram = TextureShaderProgram(context)
        colorProgram = ColorShaderProgram(context)
        texture = loadTexture(context, R.drawable.air_hockey_surface)
    }

    override fun onSurfaceChanged(glUnused: GL10?, width: Int, height: Int) {
        // Set the OpenGL viewport to fill the entire surface.
        glViewport(0, 0, width, height)
        perspectiveM(
            projectionMatrix, 45f, width.toFloat()
                    / height.toFloat(), 1f, 10f
        )
        /**
         * 创建视图矩阵
         * 参数1:目标矩阵数组
         * 参数2:目标矩阵数组的开始位置
         * 参数3,4,5:这是眼睛所在的位置,场景中的所有东西看起来都像是从这个点观察他们一样
         * 参数6,7,8:这是眼睛正在看的地方,这个位置出现在整个场景中心
         * 最后3个参数:表示头指向的方向,upY=1表示你的头笔直向上
         */
        setLookAtM(
            viewMatrix, 0,
            0f, 1.2f, 2.2f,
            0f, 0f, 0f,
            0f, 1f, 0f
        )

        /**
         * 通过setLookAtM的设置,把眼睛设置(0,1.2,2.2)意味着场景中的所有东西都出现在你眼睛下方1.2个单位和你前面2.2的单位
         * 把中心设置为(0,0,0)意味着你将向下看你前面的原点
         * 把up设置为(0,1,0)意味着你的头是笔直向上的,这个场景不会选择到任何一边
         */

    }


    override fun onDrawFrame(glUnused: GL10?) {
        // Clear the rendering surface.
        glClear(GL_COLOR_BUFFER_BIT)

        // 将投影矩阵和视图矩阵相乘并赋值给viewProjectionMatrix中
        multiplyMM(
            viewProjectionMatrix,
            0,
            projectionMatrix,
            0,
            viewMatrix,
            0
        )

        // 绘制桌子
        positionTableInScene()
        textureProgram.useProgram()
        textureProgram.setUniforms(modelViewProjectionMatrix, texture)
        table.bindData(textureProgram)
        table.draw()

        // 绘制2个木槌
        positionObjectInScene(0f, mallet.height / 2f, -0.4f)
        colorProgram.useProgram()
        colorProgram.setUniforms(modelViewProjectionMatrix, 1f, 0f, 0f)
        mallet.bindData(colorProgram)
        mallet.draw()

        positionObjectInScene(0f, mallet.height / 2f, 0.4f)
        colorProgram.setUniforms(modelViewProjectionMatrix, 0f, 0f, 1f)
        // Note that we don't have to define the object data twice -- we just
        // draw the same mallet again but in a different position and with a
        // different color.
        mallet.draw()

        // 绘制冰球
        positionObjectInScene(0f, puck.height / 2f, 0f)
        colorProgram.setUniforms(modelViewProjectionMatrix, 0.8f, 0.8f, 1f)
        puck.bindData(colorProgram)
        puck.draw()
    }

    private fun positionTableInScene() {
        // The table is defined in terms of X & Y coordinates, so we rotate it
        // 90 degrees to lie flat on the XZ plane.
        // 初始化模型矩阵
        setIdentityM(modelMatrix, 0)
        // 旋转模型矩阵
        rotateM(modelMatrix, 0, -90f, 1f, 0f, 0f)
        // 将模型矩阵和viewProjectionMatrix相乘并赋值给modelViewProjectionMatrix
        multiplyMM(
            modelViewProjectionMatrix, 0, viewProjectionMatrix,
            0, modelMatrix, 0
        )
    }

    // The mallets and the puck are positioned on the same plane as the table.
    private fun positionObjectInScene(
        x: Float,
        y: Float,
        z: Float
    ) {
        // 初始化模型矩阵
        setIdentityM(modelMatrix, 0)
        // 移动模型矩阵
        translateM(modelMatrix, 0, x, y, z)
        // 将viewProjectionMatrix和模型矩阵相乘并赋值给modelViewProjectionMatrix
        multiplyMM(
            modelViewProjectionMatrix, 0, viewProjectionMatrix,
            0, modelMatrix, 0
        )
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值