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

news2025/2/19 5:02:22

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

效果图

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2297929.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

玩转大语言模型——使用Kiln AI可视化环境进行大语言模型微调数据合成

系列文章目录 玩转大语言模型——使用langchain和Ollama本地部署大语言模型 玩转大语言模型——三分钟教你用langchain提示词工程获得猫娘女友 玩转大语言模型——ollama导入huggingface下载的模型 玩转大语言模型——langchain调用ollama视觉多模态语言模型 玩转大语言模型—…

图书管理项目(spring boot + Vue)

想要该项目的话&#xff0c;就 jia 我&#xff0c;并在评论区给我说一下&#xff0c;只需要1元&#xff0c;我把整个项目发给你 jia微&#xff1a;18439421203&#xff08;名字叫&#xff1a;Bingo&#xff09; 运行图片&#xff1a;

【机器学习】简单线性回归算法及代码实现

线性回归算法 一、摘要二、线性回归算法概述三、损失函数的定义和衡量标准四、简单线性回归的求解和应用五、机器学习算法一般求解思路 一、摘要 本文讲解了线性回归算法的基础知识和应用&#xff0c;强调线性回归主要用于解决回归问题。通过分析房产价格与房屋面积的关系&…

AI-大模型(3)-MoE模型

1.什么是MOE模型 多个领域专家共同工作&#xff0c;并行计算。 2.MOE如何工作 gate层&#xff1a;根据输入Token选择专家 基于Token来选择专家 Gate层选择专家 除专家外&#xff0c;其他层共享一个token可以选择多个专家 一个token 可以选择一个专家或者多个专…

PySide(PyQT)使用场景(QGraphicsScene)进行动态标注的一个demo

用以标注图像的一个基本框架demo import sys from PySide6.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QMainWindow, QLabel, QGraphicsPixmapItem from PySide6.QtGui import QPixmap, QPainter, QTransform from PySide6.QtCore import Qt, QPointF, S…

w206基于Spring Boot的农商对接系统的设计与实现

&#x1f64a;作者简介&#xff1a;多年一线开发工作经验&#xff0c;原创团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339;赠送计算机毕业设计600个选题excel文…

C++类和对象进阶:拷贝构造函数深度详解

拷贝构造函数 拷贝构造函数前言引入拷贝构造函数特征拷贝构造函数建议参数加上const 拷贝构造函数参数传值会引发无穷递归的解释内置类型传参拷贝自定义类型传参拷贝详细解释 编译器生成的默认拷贝构造函数默认构造函数做了什么&#xff1f;深拷贝与浅拷贝简单实现一个深拷贝。…

像取快递一样取文件?

看到一个很有意思的项目&#xff0c;像我们做软件分享的感觉会有用&#xff0c;就是现在服务器费用太贵了&#xff0c;如果自建的话感觉不是很值得。 FileCodeBox FileCodeBox 是一个轻量级的文件分享系统&#xff0c;它基于匿名口令分享文本和文件&#xff0c;无需注册登录&…

DeepSeek 助力 Vue 开发:打造丝滑的返回顶部按钮(Back to Top)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 Deep…

【前端开发学习笔记15】Vue_8

手动添加Pinia到Vue项目&#xff1a; 在实际开发中&#xff0c;Pinia配置可在项目创建时自动添加。初次学习从零开始&#xff1a; 1. 用Vite创建空的Vue3项目&#xff0c;命令为npm create vuelatest。 2. 按官方文档将pinia安装到项目中。 import { createApp } from vue im…

通过docker启用rabbitmq插件

创建文件&#xff0c;docker-compose.yml services:rabbitmq:image: rabbitmq:4.0-managementports:- "5672:5672"- "15672:15672"volumes:- ./data/rabbitmq/data:/var/lib/rabbitmq # 持久化数据- ./data/rabbitmq/plugins/rabbitmq_delayed_message_ex…

对比 LVS 负载均衡群集的 NAT 模式和 DR 模式,比较其各自的优势 与基于 openEuler 构建 LVS-DR 群集

一、 对比 LVS 负载均衡群集的 NAT 模式和 DR 模式&#xff0c;比较其各自的优势 NAT 模式 部署简单&#xff1a;NAT 模式下&#xff0c;所有的服务器节点只需要连接到同一个局域网内&#xff0c;通过负载均衡器进行网络地址转换&#xff0c;就可以实现负载均衡功能。不需要对…

C++17 中 std::lcm:从入门到精通

文章目录 一、引言二、std::lcm 的基本概念三、入门示例四、计算多个整数的最小公倍数五、std::lcm 的实现原理六、在实际项目中的应用七、注意事项八、总结 一、引言 在 C 编程中&#xff0c;处理数学运算时&#xff0c;计算最小公倍数&#xff08;Least Common Multiple&…

html 点击弹出视频弹窗

一、效果: 点击视频按钮后,弹出弹窗 播放视频 二、代码 <div class="index_change_video" data-video-src="</

代码随想录算法【Day44】

Day44 1143.最长公共子序列 class Solution { public:int longestCommonSubsequence(string text1, string text2) {vector<vector<int>> dp(text1.size() 1, vector<int>(text2.size() 1, 0));for (int i 1; i < text1.size(); i) {for (int j 1; …

项目总结:java agent的使用

测试团队会做java agent的事&#xff0c;实现测试模拟&#xff0c;各种数据采集等等工作&#xff0c;而这些不需要开发改代码来做到&#xff0c;只需要挂载下agent。 目录 javaagent认识和例子代码例子&#xff1a;java.lang.instrument自定义实现一个javaagentagent jar测试 回…

如何借助NoETL指标平台实现数据分析、决策的提效?

通常&#xff0c;企业通过明确分析目标、定位所需分析的数据&#xff0c;再通过多渠道汇集销售数据、客户反馈、市场调研等信息&#xff0c;经过数据清洗、缺失值处理及格式标准化等手段&#xff0c;运用描述性统计、回归分析、聚类分析及关联规则挖掘等多样分析方法&#xff0…

大模型语言简介

大模型语言能做什么 信息提取 将长段文字中的信息抽取出来并且以结构化的方式输出。相比起传统NLP的方式&#xff0c;大模型在泛化能力上有非常大的提升&#xff0c;并且开发成本要低2个数量级。应用场景包括&#xff1a;论文论点论据提取、用户画像提取、舆情分析、病例结构…

手动配置IP

手动配置IP&#xff0c;需要考虑四个配置项&#xff1a; 四个配置项 IP地址、子网掩码、默认网关、DNS服务器 IP地址&#xff1a;格式表现为点分十进制&#xff0c;如192.168.254.1 子网掩码&#xff1a;用于区分网络位和主机位 【子网掩码的二进制表达式一定是连续的&#…

Golang 进阶训练营

一、Golang 的 slice、map、channel 1.1 slice vs array a : make([]int, 100) //切片 b : [100]int{} //数组array需指明长度&#xff0c;长度为常量且不可改变 array长度为其类型中的组成部分&#xff08;给参数为长度100的数组的方法传长度为101的会报错&#xff09; array在…