OpenGLES->投影变换矩阵完美解决绘制GLSurfaceView绘制图形拉伸问题
- 人工智能
- 2025-09-06 15:30:02

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 /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? puteMVPMatrix(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 // 相机矩阵偏移量 ) } } 效果图
OpenGLES->投影变换矩阵完美解决绘制GLSurfaceView绘制图形拉伸问题由讯客互联人工智能栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“OpenGLES->投影变换矩阵完美解决绘制GLSurfaceView绘制图形拉伸问题”
上一篇
为什么要选择3D机器视觉检测
下一篇
Rasa学习笔记