当前位置: 首页 > news >正文

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正交投影解决
// 透视投影矩阵
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 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代码

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代码

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需要的绘制数据

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代码

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代码

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需要的绘制数据

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 // 相机矩阵偏移量
        )
    }
}

效果图

在这里插入图片描述

相关文章:

  • HBase简介
  • 2025年金三银四经典自动化测试面试题
  • [Unity角色控制专题] (借助ai)详细解析官方第三人称控制器
  • UNIX网络编程学习记录2-第一章
  • Centos 7安装docker
  • 网络安全-攻击流程-应用层
  • 五十天精通硬件设计第32天-S参数
  • DeepSeek在linux下的安装部署与应用测试
  • GPU(Graphics Processing Unit)详解
  • STM32的HAL库开发---ADC
  • Deepseek PHP API调用指南
  • 蓝桥杯篇---IAP15F2K61S2串口
  • STM32的启动流程
  • 基于STM32的智能路灯节能控制系统
  • 【设计模式】01- 一文理解常用设计模式-“创建型模式”篇
  • vue3 分析总结响应式丢失问题原因(二)
  • 【从0做项目】Java搜索引擎(3)
  • 使用 DeepSeek 生成商城流程图
  • Rasa学习笔记
  • 基于HAL库的按钮实验
  • 张家界乒乓球公开赛设干部职级门槛引关注,回应:仅限嘉宾组
  • 影子调查丨起底“三无”拖拉机产销链:出口掩内销,监管如虚设
  • 第二十届华表奖提名名单公布,张译、王一博、马丽、郭帆等入围
  • 伤者升至80人,伊朗港口爆炸源头或为“危险品和化学品仓库”
  • 内蒙古纪检干部刘占波履新呼和浩特,曾参与涉煤腐败倒查20年工作
  • 南国置业:控股股东电建地产拟受让公司持有的房地产开发业务等相关资产和负债