OpenGL ES -> 投影变换矩阵完美解决绘制GLSurfaceView绘制图形拉伸问题

GLSurfaceView绘制图形拉伸问题

  • 假如在XML文件中声明GLSurfaceView的宽高为
    • android:layout_width="match_parent"
    • android:layout_height="match_parent
  • GLSurfaceView绘制的图形在Open GL ES坐标系中,而Open GL ES坐标系会根据GLSurfaceView的宽高将绘制的图形拉伸,比如绘制一个正方形,有可能绘制成矩形,解决方案:
  • Matrix.frustumM透视投影解决
  • Matrix.orthoM正交投影解决
java 复制代码
// 透视投影矩阵
public static void frustumM(float[] m, int offset,
        float left, float right, float bottom, float top,
        float near, float far) {}

// 正交投影矩阵
public static void orthoM(float[] m, int mOffset,
    float left, float right, float bottom, float top,
    float near, float far) {}

OpenGL ES坐标系

XML文件

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<com.example.myapplication.MyGLSurfaceView
	xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

绘制拉伸正方形

自定义GLSurfaceView代码

kotlin 复制代码
class MyGLSurfaceView(context: Context, attrs: AttributeSet) : GLSurfaceView(context, attrs) {
    private var mRenderer = MyGLRenderer()

    init {
        // 设置 OpenGL ES 3.0 版本
        setEGLContextClientVersion(3)
        setRenderer(mRenderer)
        // 设置渲染模式, 仅在需要重新绘制时才进行渲染,以节省资源
        renderMode = RENDERMODE_WHEN_DIRTY
    }
}

自定义GLSurfaceView.Renderer代码

kotlin 复制代码
class MyGLRenderer : GLSurfaceView.Renderer {
    private var mDrawData: DrawData? = null

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        // 当 Surface 创建时调用, 进行 OpenGL ES 环境的初始化操作, 设置清屏颜色为青蓝色 (Red=0, Green=0.5, Blue=0.5, Alpha=1)
        GLES30.glClearColor(0.0f, 0.5f, 0.5f, 1.0f)
        mDrawData = DrawData().apply {
            initVertexBuffer()
            initShader()
        }
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        // 当 Surface 尺寸发生变化时调用,例如设备的屏幕方向发生改变, 设置视口为新的尺寸,视口是指渲染区域的大小
        GLES30.glViewport(0, 0, width, height)
    }

    override fun onDrawFrame(gl: GL10?) {
        // 每一帧绘制时调用, 清除颜色缓冲区
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
        mDrawData?.drawSomething()
    }
}

GLSurfaceView需要的绘制数据

kotlin 复制代码
class DrawData{
    var mProgram : Int = -1
    var NO_OFFSET = 0
    var VERTEX_POS_DATA_SIZE = 3

    // 1. 准备正方形的顶点数据Float数组, 分配顶点数据Float数组的直接内存
    val vertex = floatArrayOf(
        -0.5f,  0.5f, 0.0f, // 左上
        -0.5f, -0.5f, 0.0f, // 左下
        0.5f, 0.5f, 0.0f, // 右上
        0.5f, -0.5f, 0.0f, // 右下
    )

    val vertexBuffer = ByteBuffer.allocateDirect(vertex.size * 4) // 分配直接内存
        .order(ByteOrder.nativeOrder()) // 使用小端, 即低地址存放低位数据, 高地址存放高位数据
        .asFloatBuffer()

    // 2. 创建顶点缓冲区对象(Vertex Buffer Object, VBO), 并上传顶点数据到缓冲区对象中
    fun initVertexBuffer(){
        vertexBuffer.put(vertex) // 将顶点数据放入 FloatBuffer
        vertexBuffer.position(0) // 在将数据放入缓冲区后,位置指针会指向缓冲区的末尾。重置位置指针为 0,使得在后续操作中可以从缓冲区的开始位置读取数据

        val vbo = IntArray(1)
        GLES30.glGenBuffers(1, vbo, 0) // 生成一个缓冲区对象ID,并存储在数组 vbo 中,存放位置为0
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo[0]) // 绑定生成的顶点缓冲区对象,使其成为当前缓冲区操作的目标
        GLES30.glBufferData(
            GLES30.GL_ARRAY_BUFFER,
            vertex.size * 4, // 数据总字节数 = 顶点数 * Float占4字节
            vertexBuffer,
            GLES30.GL_STATIC_DRAW
        )
    }

    fun initShader()  {
        val vertexShaderCode = """#version 300 es
                                layout (location = 0) in vec4 aPosition;

                                void main() {
                                  gl_Position = aPosition;
                                }""".trimIndent()         // 顶点着色器代码

        val fragmentShaderCode = """#version 300 es
                precision mediump float;
                uniform vec4 vColor;
                out vec4 fragColor;
                
                void main() {
                  fragColor = vColor;
                }""".trimIndent()         // 片段着色器代码

        // 3. 加载顶点着色器和片段着色器, 并创建着色器程序
        val vertexShader = LoadShaderUtil.loadShader(GLES30.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader = LoadShaderUtil.loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentShaderCode)
        mProgram = GLES30.glCreateProgram()
        GLES30.glAttachShader(mProgram, vertexShader)
        GLES30.glAttachShader(mProgram, fragmentShader)
        GLES30.glLinkProgram(mProgram)
        GLES30.glUseProgram(mProgram)
    }

    // 4. 使用着色器程序绘制图形
    fun drawSomething(){
        // 5. 获取顶点数据的位置, 并使用该位置的数据
        val positionHandle = GLES30.glGetAttribLocation(mProgram, "aPosition")
        GLES30.glEnableVertexAttribArray(positionHandle)
        GLES30.glVertexAttribPointer(positionHandle, VERTEX_POS_DATA_SIZE, GLES30.GL_FLOAT, false, 0, 0)

        // 6. 设置片段着色器的颜色
        val colorHandle = GLES30.glGetUniformLocation(mProgram, "vColor")
        GLES30.glUniform4f(colorHandle, 1.0f, 0.5f, 0.5f, 1.0f) // 红色
        // 7. 绘制正方形
        GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, NO_OFFSET, vertex.size / VERTEX_POS_DATA_SIZE)
        GLES30.glDisableVertexAttribArray(positionHandle)
    }
}

object LoadShaderUtil{
    // 创建着色器对象
    fun loadShader(type: Int, source: String): Int {
        val shader = GLES30.glCreateShader(type)
        GLES30.glShaderSource(shader, source)
        GLES30.glCompileShader(shader)
        return shader
    }
}

效果图

透视投影绘制不拉伸的正方形

透视投影PerspectiveProjection

自定义GLSurfaceView代码

kotlin 复制代码
class MyGLSurfaceView(context: Context, attrs: AttributeSet) : GLSurfaceView(context, attrs) {
    private var mRenderer = MyGLRenderer()

    init {
        // 设置 OpenGL ES 3.0 版本
        setEGLContextClientVersion(3)
        setRenderer(mRenderer)
        // 设置渲染模式, 仅在需要重新绘制时才进行渲染,以节省资源
        renderMode = RENDERMODE_WHEN_DIRTY
    }
}

自定义GLSurfaceView.Renderer代码

kotlin 复制代码
class MyGLRenderer : GLSurfaceView.Renderer {
    private var mDrawData: DrawDataWithPerspectiveProjection? = null

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        // 当 Surface 创建时调用, 进行 OpenGL ES 环境的初始化操作, 设置清屏颜色为青蓝色 (Red=0, Green=0.5, Blue=0.5, Alpha=1)
        GLES30.glClearColor(0.0f, 0.5f, 0.5f, 1.0f)
        mDrawData = DrawDataWithPerspectiveProjection().apply {
            initVertexBuffer()
            initShader()
        }
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        // 当 Surface 尺寸发生变化时调用,例如设备的屏幕方向发生改变, 设置视口为新的尺寸,视口是指渲染区域的大小
        GLES30.glViewport(0, 0, width, height)
        mDrawData?.computeMVPMatrix(width.toFloat(), height.toFloat())
    }

    override fun onDrawFrame(gl: GL10?) {
        // 每一帧绘制时调用, 清除颜色缓冲区
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
        mDrawData?.drawSomething()
    }
}

GLSurfaceView需要的绘制数据

kotlin 复制代码
class DrawDataWithPerspectiveProjection {
    var mProgram : Int = -1
    var NO_OFFSET = 0
    var VERTEX_POS_DATA_SIZE = 3

    // 1. 准备正方形的顶点数据Float数组, 分配顶点数据Float数组的直接内存
    val vertex = floatArrayOf(
        -0.5f,  0.5f, 0.0f, // 左上
        -0.5f, -0.5f, 0.0f, // 左下
        0.5f, 0.5f, 0.0f, // 右上
        0.5f, -0.5f, 0.0f, // 右下
    )

    val vertexBuffer = ByteBuffer.allocateDirect(vertex.size * 4) // 分配直接内存
        .order(ByteOrder.nativeOrder()) // 使用小端, 即低地址存放低位数据, 高地址存放高位数据
        .asFloatBuffer()

    // 2. 创建顶点缓冲区对象(Vertex Buffer Object, VBO), 并上传顶点数据到缓冲区对象中
    fun initVertexBuffer(){
        vertexBuffer.put(vertex) // 将顶点数据放入 FloatBuffer
        vertexBuffer.position(0) // 在将数据放入缓冲区后,位置指针会指向缓冲区的末尾。重置位置指针为 0,使得在后续操作中可以从缓冲区的开始位置读取数据

        val vbo = IntArray(1)
        GLES30.glGenBuffers(1, vbo, 0) // 生成一个缓冲区对象ID,并存储在数组 vbo 中,存放位置为0
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo[0]) // 绑定生成的顶点缓冲区对象,使其成为当前缓冲区操作的目标
        GLES30.glBufferData(
            GLES30.GL_ARRAY_BUFFER,
            vertex.size * 4, // 数据总字节数 = 顶点数 * Float占4字节
            vertexBuffer,
            GLES30.GL_STATIC_DRAW
        )
    }


    fun initShader()  {
        val vertexMapShaderCode = """
            #version 300 es
            uniform mat4 uMVPMatrix;
            layout (location = 0) in vec4 aPosition;
            void main() {
                gl_Position = uMVPMatrix * aPosition;
            }""".trimIndent()

        val fragmentShaderCode = """#version 300 es
                precision mediump float;
                uniform vec4 vColor;
                out vec4 fragColor;
                
                void main() {
                  fragColor = vColor;
                }""".trimIndent()         // 片段着色器代码

        // 3. 加载顶点着色器和片段着色器, 并创建着色器程序
        val vertexShader = LoadShaderUtil.loadShader(GLES30.GL_VERTEX_SHADER, vertexMapShaderCode)
        val fragmentShader = LoadShaderUtil.loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentShaderCode)
        mProgram = GLES30.glCreateProgram()
        GLES30.glAttachShader(mProgram, vertexShader)
        GLES30.glAttachShader(mProgram, fragmentShader)
        GLES30.glLinkProgram(mProgram)
        GLES30.glUseProgram(mProgram)
    }

    // 4. 使用着色器程序绘制图形
    fun drawSomething(){
        // 新增矩阵传递代码
        val matrixHandle = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix")
        GLES30.glUniformMatrix4fv(matrixHandle, 1, false, mMVPMatrix, 0)

        // 5. 获取顶点数据的位置, 并使用该位置的数据
        val positionHandle = GLES30.glGetAttribLocation(mProgram, "aPosition")
        GLES30.glEnableVertexAttribArray(positionHandle)
        GLES30.glVertexAttribPointer(positionHandle, VERTEX_POS_DATA_SIZE, GLES30.GL_FLOAT, false, 0, 0)

        // 6. 设置片段着色器的颜色
        val colorHandle = GLES30.glGetUniformLocation(mProgram, "vColor")
        GLES30.glUniform4f(colorHandle, 1.0f, 0.5f, 0.5f, 1.0f) // 红色
        // 7. 绘制正方形
        GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, NO_OFFSET, vertex.size / VERTEX_POS_DATA_SIZE)

        GLES30.glDisableVertexAttribArray(positionHandle)
    }

    // 最终变化矩阵
    private val mMVPMatrix = FloatArray(16)
    // 投影矩阵
    private val mProjectionMatrix = FloatArray(16)
    // 相机矩阵
    private val mViewMatrix = FloatArray(16)

    private var mViewPortRatio = 1f

    fun computeMVPMatrix(width: Float, height: Float) {
        // 1. 设置透视投影矩阵,为了让近平面宽高比与屏幕宽高比一致
        takeIf { width > height }?.let {
            mViewPortRatio = width / height
            Matrix.frustumM(
                mProjectionMatrix, // 透视投影矩阵
                NO_OFFSET, // 偏移量
                -mViewPortRatio, // 近平面的坐标系左边界
                mViewPortRatio, // 近平面的坐标系右边界
                -1f, // 近平面的坐标系的下边界
                1f, // 近平面坐标系的上边界
                1f, // 近平面距离相机距离
                2f // 远平面距离相机距离
            )
        } ?: run {
            mViewPortRatio = height / width
            Matrix.frustumM(
                mProjectionMatrix, // 透视投影矩阵
                NO_OFFSET, // 偏移量
                -1f, // 近平面坐标系左边界
                1f, // 近平面坐标系右边界
                -mViewPortRatio, // 近平面坐标系下边界
                mViewPortRatio, // 近平面坐标系上边界
                1f, // 近平面距离相机距离
                2f // 远平面距离相机距离
            )
        }

        // 2. 设置相机矩阵
        // 相机位置(0f, 0f, 1f)
        // 物体位置(0f, 0f, 0f)
        // 相机方向(0f, 1f, 0f)
        Matrix.setLookAtM(
            mViewMatrix, // 相机矩阵
            NO_OFFSET, // 偏移量
            0f, // 相机位置x
            0f, // 相机位置y
            1f, // 相机位置z
            0f, // 物体位置x
            0f, // 物体位置y
            0f, // 物体位置z
            0f, // 相机上方向x
            1f, // 相机上方向y
            0f // 相机上方向z
        )

        // 3. 设置最终变化矩阵
        Matrix.multiplyMM(
            mMVPMatrix, // 最终变化矩阵
            NO_OFFSET, // 偏移量
            mProjectionMatrix, // 投影矩阵
            NO_OFFSET, // 投影矩阵偏移量
            mViewMatrix, // 相机矩阵
            NO_OFFSET // 相机矩阵偏移量
        )
    }
}

效果图

相关推荐
脸大是真的好~3 分钟前
EasyExcel的使用
java·excel
小宋10215 分钟前
Java 项目结构 vs Python 项目结构:如何快速搭一个可跑项目
java·开发语言·python
JavaGuide26 分钟前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
吃虫子的人31 分钟前
记录使用Arthas修改线上源码重新加载的一次过程
java·arthas
figo10tf1 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端
我有与与症1 小时前
从页面加载过程看 Kuikly 的多线程架构
kotlin
zhangyi_viva1 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
橙露1 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot
doupoa1 小时前
内存指针是什么?为什么指针还要有偏移量?
android·c++