Android显示系统(02)- OpenGL ES - 概述
Android显示系统(03)- OpenGL ES - GLSurfaceView的使用
Android显示系统(04)- OpenGL ES - Shader绘制三角形
Android显示系统(05)- OpenGL ES - Shader绘制三角形(使用glsl文件)
Android显示系统(06)- OpenGL ES - VBO和EBO和VAO
Android显示系统(07)- OpenGL ES - 纹理Texture
Android显示系统(08)- OpenGL ES - 图片拉伸
一、前言:
前面介绍了OpenGL当中通过纹理渲染图片的方法,但是,渲染出来的图片明显被拉伸,本文介绍下如何对抗这种拉伸。
二、防止被拉伸的方法:
1、什么是拉伸:
如上图所示,我本来要笑得很开心,就像左边图片一样,结果笑抽了,像右边一样了。这里面有个视口的概念,视口是指在屏幕上用于显示图形的矩形区域。
2、如何避免:
我们发现上图被拉伸是因为默认情况下视口大小就是屏幕大小,所以图片宽高比和屏幕宽高比不一样肯定就会被拉伸了。所以,我们将视口的宽高和照片保持一致是不是就可以正常显示了?这样的确可以解决问题,但是会存在一种情况,就是,如果照片很小,就像下图一样了:
是不是非常奇怪?因此,我们一般的做法就是:根据宽高比,选择宽顶到屏幕两头,或者长顶到屏幕两头!
具体调整方法:视口调整
或者投影调整
,下面详细分析:
三、视口调整:
注意,下图中视口的红线其实和照片一样大,为了让大家看清,留了一点空白。
最终结果就如上图所示。步骤如下:
-
计算视口的宽高比:
以图片宽高比和窗口宽高比为基础来决定视口的大小。首先计算宽高比:
float textureWidth = 300.0f; float textureHeight = 200.0f; float windowWidth = 640.0f; float windowHeight = 480.0f; float textureAspectRatio = (float) textureWidth / textureHeight; float windowAspectRatio = (float) windowWidth / windowHeight;
-
决定视口的大小:
基于比较这两个宽高比,设置适合的视口:
-
情况 1:窗口宽高比大于图片宽高比
在这种情况下,窗口是较宽的,使用图片的高度为基准:
if (windowAspectRatio > textureAspectRatio) { // 使用窗口高度作为基准 float adjustedWidth = textureAspectRatio * windowHeight; // 调整后的宽度 int viewportX = (windowWidth - adjustedWidth) / 2; // 左侧留白 glViewport(viewportX, 0, adjustedWidth, windowHeight); // 设置视口 } else { // 情况2 }
-
情况 2:窗口宽高比小于图片宽高比
在这种情况下,窗口是较窄的,使用图片的宽度为基准:
else { // 使用窗口宽度作为基准 float adjustedHeight = (1.0f / textureAspectRatio) * windowWidth; // 调整后的高度 int viewportY = (windowHeight - adjustedHeight) / 2; // 上侧留白 glViewport(0, viewportY, windowWidth, adjustedHeight); // 设置视口 }
-
在Android中我们有了图片的起始位置(左上角坐标)和宽高就可以调用下面函数完成视口调整:
glViewport(X, Y, vWidth, vHeight)
四、投影调整:
1、定义:
我们之前说了,在渲染管线的投影变换
阶段有两种选择:正交变换
和透视变换
,其中透视变换
一般用于显示3D效果,物体距离观察者越远,尺寸越小,造成深度感,一般适用于电影、游戏等场景;而正交变换
一般用于显示2D图片,无论距离如何,物体的尺寸保持不变,因此我们选择正交变换
;
-
透视投影:
-
正交投影:
2、正交投影特点:
- 无失真:物体的各个部分间的相对关系保持不变。
- 平行线:在投影过程中,平行线在投影后仍然平行。
- 均匀缩放:物体在距离视点的远近时大小不会变化。
3、投影平面:
通常选择Z=0平面作为投影平面(Z轴等于0,也就是说三维空间中变成二维的XY平面)。
4、正交投影的过程:
可以看出,先平移到坐标系的远点和物体模型的几何中心重合,再缩放成单位立方体,最后将3D模型投影到2D屏幕上。也就是平移和缩放两步。
之前章节介绍过,投影过程中需要三个矩阵,我们一般叫做MVP,MVP 是指 Model-View-Projection(模型-视图-投影)矩阵,是一种常用的矩阵变换组合,用于将三维场景中的对象坐标转换为最终在二维屏幕上呈现的坐标。
- Model Matrix(模型矩阵):用于将对象从局部坐标系转换到世界坐标系(即将对象放置到世界空间中的合适位置)。
- View Matrix(视图矩阵):描述了观察者的位置和方向,将世界坐标系中的对象转换到观察者的视角坐标系。视图矩阵实际上是观察者逆向移动的矩阵。
- Projection Matrix(投影矩阵):用于将观察坐标系中的对象投影到屏幕坐标系,定义了视锥体的几何形状,包括透视投影和正交投影。
一般情况下M我们不设置,或者设置为单位矩阵即可。V和P是需要我们计算出来的。
5、视图矩阵(V):
要构建相机的视图矩阵,以确定观察者的位置和方向。这个方法通常与 OpenGL ES 中的相机视图转换相关联。
主要通过 Matrix.setLookAtM()
方法完成:
-
方法签名:
public static void setLookAtM(float[] rm, int rmOffset, float eyeX, float eyeY, float eyeZ, float centerX, float centerY, float centerZ, float upX, float upY, float upZ)
-
参数:
rm
:目标矩阵,即要设置视图矩阵的目标数组。rmOffset
:目标矩阵的偏移量。eyeX, eyeY, eyeZ
:摄像机看向哪儿。centerX, centerY, centerZ
:摄像机观察的目标位置的坐标。upX, upY, upZ
:摄像机的朝向向量。
-
功能:
setLookAtM()
方法的作用是根据摄像机的位置、观察目标的位置和朝向向量,设置视图矩阵。这个视图矩阵描述了一个摄像机从指定位置观看指定目标的视角。 -
使用示例:下面是一个简单的示例代码,演示如何使用
Matrix.setLookAtM()
方法设置视图矩阵:float[] viewMatrix = new float[16]; Matrix.setLookAtM(viewMatrix, 0, eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ);
6、投影矩阵(P):
正交投影矩阵用于定义如何将三维坐标转换为二维坐标。设定参数包括视口的左、右、下、上、近、远平面值。通用格式如下:
- L (Left): 左侧边界
- R (Right): 右侧边界
- B (Bottom): 下侧边界
- T (Top): 上侧边界
- N (Near): 近剪裁平面
- F (Far): 远剪裁平面
上面的矩阵如此负责,我们平时怎么记住,或者怎么构建呢?在Android中提供了如下方法,我们调用一下就会生成正交投影矩阵:
Matrix.othroM(mvpM, offset, l, r, b, t, n, f)
mvpM
:目标数组,用于存储生成的正交投影矩阵。offset
:目标数组中的偏移量,指定从哪个位置开始存储矩阵数据,一般设置为0。l, r, b, t
:定义了正交投影矩阵的左、右、底和顶面边界。n
:近裁剪面(near clipping plane)的距离,即视锥体近端的距离。f
:远裁剪面(far clipping plane)的距离,即视锥体远端的距离。
由于图片是二维的,所以n和f不会改变,我们将它们设置为-1和1即可。
对于宽高来说,则需要根据实际情况来设定。如果上下留白,则左右设置为-1, 1;如果左右留白,则上下设置为-1,1。
所以,我们又像视口变换一样,来到一张二维图片了,根据,宽高比分类讨论。
矩阵宽高计算算法:
- 当图片宽高比大于窗口宽高比:
- 这意味着图片更“宽”相对于窗口。例如,窗口是竖直的,而图片是横向的。
- 计算模型的高度(tb)时,用图片的宽高比除以窗口的宽高比:
tb = (WI/HI) / (WW/HW)
。 - 设置正交投影矩阵的上边界 (
t
) 和下边界 (b
) 为-tb
和tb
。这确保了在Y轴方向上,图片能够适当缩放以不失真。
- 当图片宽高比小于或等于窗口宽高比:
- 这意味着图片更“高”相对于窗口。例如,窗口是横向的,而图片是竖直的。
- 计算模型的宽度(lr):
lr = (WW/HW) / (WI/HI)
; - 设置正交投影矩阵的左边界 (
l
) 和右边界 (r
) 为-lr
和lr
。这确保了在X轴方向上,图片能够适当缩放以不失真。
这样我们就得到了正确的正交变换矩阵,之后用这个正交变换矩阵就可以将图片正确的渲染到屏幕上。
7、MVP生成:
Matrix.multiplyMM()
是 Android 提供的用于矩阵相乘的方法,用于将两个 4x4 的浮点数矩阵相乘,并将结果存储在一个目标矩阵中。这个方法通常用于进行矩阵变换,比如将模型矩阵、视图矩阵和投影矩阵相乘,以生成 MVP(Model-View-Projection)矩阵。
以下是关于 Matrix.multiplyMM()
方法的一些重要信息:
-
方法签名:
public static void multiplyMM(float[] result, int resultOffset, float[] lhs, int lhsOffset, float[] rhs, int rhsOffset)
-
参数:
result
:存储结果的目标矩阵数组。resultOffset
:目标矩阵的偏移量。lhs
:左矩阵数组(第一个矩阵)。lhsOffset
:左矩阵的偏移量。rhs
:右矩阵数组(第二个矩阵)。rhsOffset
:右矩阵的偏移量。
-
功能:
multiplyMM()
方法将两个 4x4 的浮点数矩阵相乘,即左矩阵和右矩阵相乘,结果存储在目标矩阵中。这种矩阵相乘通常用于进行复合变换,例如将模型矩阵、视图矩阵和投影矩阵相乘。 -
使用示例:下面是一个简单的示例代码,演示如何使用
Matrix.multiplyMM()
方法进行矩阵相乘:float[] resultMatrix = new float[16]; Matrix.multiplyMM(resultMatrix, 0, modelMatrix, 0, viewMatrix, 0); Matrix.multiplyMM(resultMatrix, 0, resultMatrix, 0, projectionMatrix, 0);
8、编码示例:
private void calculateProjection(int windowWidth, int windowHeight) {
float imageWidth = textureBitmap.getWidth();
float imageHeight = textureBitmap.getHeight();
float imageAspectRatio = imageWidth / imageHeight;
float windowAspectRatio = (float) windowWidth / (float) windowHeight;
// 正交投影参数
float left, right, bottom, top;
if (imageAspectRatio > windowAspectRatio) {
// 图片相对窗口更宽
float tb = (imageWidth / imageHeight) / (windowWidth / (float) windowHeight);
top = tb;
bottom = -tb;
left = -1; // 可根据实际需求调整
right = 1; // 可根据实际需求调整
} else {
// 图片相对窗口更高或相等
float lr = (windowWidth / (float) windowHeight) / (imageWidth / imageHeight);
left = -lr;
right = lr;
top = 1; // 可根据实际需求调整
bottom = -1; // 可根据实际需求调整
}
// 更新正交投影矩阵
float[] projectionMatrix = new float[16];
Matrix.orthoM(projectionMatrix, 0, left, right, bottom, top);
square.setProjectionMatrix(projectionMatrix); // 这个square是你自定义的类来处理绘制
}
关键点总结:
- 计算正交投影矩阵:
- 在
calculateProjection
方法中,根据图片和窗口的宽高比计算并更新投影矩阵。
- 在
- 根据条件设置矩阵的边界:
- 当图片宽高比大于窗口宽高比时,调整模型的高度。
- 当图片宽高比小于或等于窗口宽高比时,调整模型的宽度。
- 设置和渲染图像:
- 图片加载、绘制等逻辑将在
Square
类中进行。
- 图片加载、绘制等逻辑将在
五、编码实战:
我们需要对两种方式都进行编码验证。
1、视口变换:
-
定义变量:
文件路径:
com/example/glsurfaceviewdemo/GLRenderTest.java
private int mSurfaceWidth = 0; // 窗口宽 private int mSurfaceHeight = 0; // 窗口高 private int mViewportWidth = 0; // 视口宽 private int mViewportHeight = 0; // 视口高 private int mViewportX = 0; // 视口起始横坐标 private int mViewportY = 0; // 视口起始纵坐标 private Bitmap mBitmap;
注意,我顺便将图片操作挪到
GLRenderTest
当中了,之前是在com/example/glsurfaceviewdemo/TextureRender.java
当中,通过构造函数传递给TextureRender
即可。 -
图片加载函数挪位置:
public TextureRender(Context context, Bitmap bitmap) { mContext = context; mBitmap = bitmap; initialize(); }
对应
GLRenderTest
也需要修改:public GLRenderTest(Context context) { this.mContext = context; mBitmap = loadImage(); } // 加载图片 private Bitmap loadImage() { BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false; return BitmapFactory.decodeResource(mContext.getResources(), R.drawable.android_logo, options); } public void onDestroy() { // mTriangle.release(); mBitmap.recycle(); // 回收bitmap mTextureRender.release(); }
-
添加视口变换函数:
文件路径:
com/example/glsurfaceviewdemo/GLRenderTest.java
// 视口变换:通过调整视口大小,保证图片不被拉伸 private void calculateViewport() { // 获取图片的宽高比 float imageRatio = (float) mBitmap.getWidth() / mBitmap.getHeight(); Log.e("Bitmap", "Bitmap111 width: " + mBitmap.getWidth() + ", height: " + mBitmap.getHeight()); // 获取surface(窗口)的宽高比 float surfaceRatio = (float) mSurfaceWidth / mSurfaceHeight; if (imageRatio > surfaceRatio) { // 图片宽高比大于窗口的宽高比,按照宽度填满 mViewportWidth = mSurfaceWidth; mViewportHeight = (int) (mSurfaceWidth / imageRatio); } else { // 图片宽高比小于等于窗口的宽高比,按照高度填满 mViewportWidth = (int) (mSurfaceHeight * imageRatio); mViewportHeight = mSurfaceHeight; } // 计算视口的中心位置 mViewportX = (mSurfaceWidth - mViewportWidth) / 2; mViewportY = (mSurfaceHeight - mViewportHeight) / 2; }
-
调用视口变换:
@Override public void onSurfaceChanged(GL10 gl, int width, int height) { mSurfaceWidth = width; mSurfaceHeight = height; calculateViewport(); // 计算视口参数 GLES30.glViewport(mViewportX, mViewportY, mViewportWidth, mViewportHeight); // 设置视口 // GLES30.glViewport(0, 0, width, height); // 视口变换时候不能从屏幕左上角开始 }
-
完整代码:
文件路径:
com/example/glsurfaceviewdemo/GLRenderTest.java
package com.example.glsurfaceviewdemo; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLES30; import android.opengl.GLSurfaceView; import android.opengl.Matrix; import android.util.Log; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; public class GLRenderTest implements GLSurfaceView.Renderer { private Triangle mTriangle; private TextureRender mTextureRender; private Context mContext; private int mSurfaceWidth = 0; // 窗口宽 private int mSurfaceHeight = 0; // 窗口高 private int mViewportWidth = 0; // 视口宽 private int mViewportHeight = 0; // 视口高 private int mViewportX = 0; // 视口起始横坐标 private int mViewportY = 0; // 视口起始纵坐标 private Bitmap mBitmap; public GLRenderTest(Context context) { this.mContext = context; mBitmap = loadImage(); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); // mTriangle = new Triangle(mContext); mTextureRender = new TextureRender(mContext, mBitmap); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { mSurfaceWidth = width; mSurfaceHeight = height; calculateViewport(); // 计算视口参数 GLES30.glViewport(mViewportX, mViewportY, mViewportWidth, mViewportHeight); // 设置视口 // GLES30.glViewport(0, 0, width, height); // 视口变换时候不能从屏幕左上角开始 } @Override public void onDrawFrame(GL10 gl){ GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); // mTriangle.draw(); mTextureRender.draw(); } public void onDestroy() { // mTriangle.release(); mBitmap.recycle(); // 回收bitmap mTextureRender.release(); } // 加载图片 private Bitmap loadImage() { BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false; return BitmapFactory.decodeResource(mContext.getResources(), R.drawable.android_logo, options); } // 视口变换:通过调整视口大小,保证图片不被拉伸 private void calculateViewport() { // 获取图片的宽高比 float imageRatio = (float) mBitmap.getWidth() / mBitmap.getHeight(); // 获取surface(窗口)的宽高比 float surfaceRatio = (float) mSurfaceWidth / mSurfaceHeight; if (imageRatio > surfaceRatio) { // 图片宽高比大于窗口的宽高比,按照宽度填满 mViewportWidth = mSurfaceWidth; mViewportHeight = (int) (mSurfaceWidth / imageRatio); } else { // 图片宽高比小于等于窗口的宽高比,按照高度填满 mViewportWidth = (int) (mSurfaceHeight * imageRatio); mViewportHeight = mSurfaceHeight; } // 计算视口的中心位置 mViewportX = (mSurfaceWidth - mViewportWidth) / 2; mViewportY = (mSurfaceHeight - mViewportHeight) / 2; } }
文件路径:
com/example/glsurfaceviewdemo/TextureRender.java
package com.example.glsurfaceviewdemo; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLES30; import android.opengl.GLUtils; import android.opengl.Matrix; import android.util.Log; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; public class TextureRender { private Context mContext; private float[] mCoordData = { // 顶点坐标 纹理坐标 -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, // 左上角 -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, // 左下角 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右上角 1.0f, -1.0f, 0.0f, 1.0f, 1.0f // 右下角 }; private int mTextureId = -1; private FloatBuffer mCoordBuffer; private int mProgram = -1; private int mVboId; private float[] mTranslateMatrix = new float[16]; private int mPositionHandle = -1; // 顶点位置属性的操作句柄 private int mTexCoordHandle = -1; // 纹理坐标属性的操作句柄 private int mTMatrixHandle = -1; // 变换矩阵操作句柄,用于实现顶点的变换 private int mSamplerHandle = -1; // 纹理采样器操作句柄,相当于一个指向某个纹理单元的指针 Bitmap mBitmap; public TextureRender(Context context, Bitmap bitmap) { mContext = context; mBitmap = bitmap; initialize(); } private void initialize() { mTextureId = uploadTexture(); // 上传纹理到GPU initVertexBuffer(); // 初始化坐标数据 initShaders(mContext); // 加载并编译着色器 initVbo(); // 初始化 VBO initMatrix(); // 初始化变换矩阵 initHandles(); // 获取GPU和Shader的一些操作接口 } // 初始化变换矩阵 private void initMatrix() { Matrix.setIdentityM(mTranslateMatrix, 0); // 设置为单位矩阵 } // 获取GPU和Shader的一些操作接口 private void initHandles() { // 获取顶点坐标操作接口的句柄 mPositionHandle = GLES30.glGetAttribLocation(mProgram, "aPosition"); validateAttributeLocation(mPositionHandle, "aPosition"); GLES30.glEnableVertexAttribArray(mPositionHandle); // 启用位置属性数组 // 获取纹理坐标操作接口的句柄 mTexCoordHandle = GLES30.glGetAttribLocation(mProgram, "aTexCoord"); validateAttributeLocation(mTexCoordHandle, "aTexCoord"); GLES30.glEnableVertexAttribArray(mTexCoordHandle); // 启用纹理坐标属性数组 // 获取变换矩阵操作接口的句柄 mTMatrixHandle = GLES30.glGetUniformLocation(mProgram, "uTMatrix"); // 用于获取Shader当中纹理采样器的操作接口的句柄 mSamplerHandle = GLES30.glGetUniformLocation(mProgram, "uSampler"); } // 用于验证属性句柄的有效性 private void validateAttributeLocation(int handle, String attributeName) { if (handle == -1) { Log.e("TextureRender", "Could not find attribute " + attributeName); } } // 绘制纹理 public void draw() { // 激活着色器程序 GLES30.glUseProgram(mProgram); // 绑定VBO GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId); // 设置顶点坐标和纹理坐标给OpenGL setupVertexAttribPointer(); // 上传变换矩阵 GLES30.glUniformMatrix4fv(mTMatrixHandle, 1, false, mTranslateMatrix, 0); // 绑定纹理并设置采样器 bindTexture(); // 绘制矩形 GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4); // 解绑当前的 VBO,避免后续操作意外影响到这个缓冲 GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0); // 检查 OpenGL 错误 checkOpenGLError(); } // 设置顶点坐标和纹理坐标 private void setupVertexAttribPointer() { // 指定位置属性的布局(设置顶点位置数据) // 3表示:每个顶点有3个浮点数(x, y, z),步长stride为 5*Float.BYTES,偏移量是0 GLES30.glVertexAttribPointer(mPositionHandle, 3, GLES30.GL_FLOAT, false, 5 * Float.BYTES, 0); // 指定纹理坐标属性的布局(设置纹理坐标数据) // 2表示:每个纹理有 2 个浮点数(u, v),步长是同样是 5*Float.BYTES,偏移量是3*Float.BYTES(前三个是顶点坐标) GLES30.glVertexAttribPointer(mTexCoordHandle, 2, GLES30.GL_FLOAT, false, 5 * Float.BYTES, 3 * Float.BYTES); } // 绑定纹理 private void bindTexture() { // 激活纹理单元 0 GLES30.glActiveTexture(GLES30.GL_TEXTURE0); // 绑定纹理 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId); // 上传纹理单元索引到Shader中的 uSampler 变量 GLES30.glUniform1i(mSamplerHandle, 0); } // 检查OpengGL的错误 private void checkOpenGLError() { int error = GLES30.glGetError(); if (error != GLES30.GL_NO_ERROR) { Log.e("OpenGL", "OpenGL Error: " + error); } } // 加载并编译着色器 private void initShaders(Context context) { String vertexShaderCode = ShaderController.loadShaderCodeFromFile("texture_vertex_shader.glsl", context); String fragmentShaderCode = ShaderController.loadShaderCodeFromFile("texture_fragment_shader.glsl", context); mProgram = ShaderController.createGLProgram(vertexShaderCode, fragmentShaderCode); if (mProgram == 0) { Log.e("TextureRender", "Failed to create OpenGL program."); } } // 初始化坐标数据 private void initVertexBuffer() { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(mCoordData.length * 4); byteBuffer.order(ByteOrder.nativeOrder()); mCoordBuffer = byteBuffer.asFloatBuffer(); mCoordBuffer.put(mCoordData); mCoordBuffer.position(0); } // 初始化 VBO private void initVbo() { int[] vbos = new int[1]; GLES30.glGenBuffers(1, vbos, 0); mVboId = vbos[0]; GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId); mCoordBuffer.position(0); GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, mCoordBuffer.capacity() * 4, mCoordBuffer, GLES30.GL_STATIC_DRAW); } // 上传纹理到GPU private int uploadTexture() { int[] textureIds = new int[1]; GLES30.glGenTextures(1, textureIds, 0); // 创建纹理 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]); // 绑定纹理 GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR); // 设置缩小策略 GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR); // 设置放大策略 GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, mBitmap, 0); // 纹理上传到GPU GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0); // 解绑纹理,避免后续误操作 return textureIds[0]; } public void release() { GLES30.glDeleteBuffers(1, new int[]{mVboId}, 0); // 删除 VBO GLES30.glDeleteProgram(mProgram); // 删除 shader program } }
-
运行结果:
正常了✌!
2、正交投影:
-
定义变量:
文件路径:
com/example/glsurfaceviewdemo/GLRenderTest.java
private float[] mProjectionMatrix = new float[16]; // 投影矩阵 private float[] mViewMatrix = new float[16]; // 视图矩阵 private float[] mMVPMatrix = new float[16]; // mvp矩阵
-
计算视图矩阵和投影矩阵:
文件路径:
com/example/glsurfaceviewdemo/GLRenderTest.java
// 正交投影变换 private void calculateViewport2(int width, int height) { float imageAspectRatio = (float) mBitmap.getWidth() / (float) mBitmap.getHeight(); float surfaceAspectRatio = (float) width / (float) height; if (imageAspectRatio > surfaceAspectRatio) { // 图片宽高比大于屏幕,按照宽度填满计算高度 float tb = imageAspectRatio / surfaceAspectRatio; // 计算投影矩阵 Matrix.orthoM(mProjectionMatrix, 0, -1.0f, 1.0f, -tb, tb, -1.0f, 1.0f); } else { // 图片宽高比小于等于屏幕,按照高度填满计算宽度 float tb = surfaceAspectRatio / imageAspectRatio; // 计算投影矩阵 Matrix.orthoM(mProjectionMatrix, 0, -tb, tb, -1.0f, 1.0f, -1.0f, 1.0f); } // 计算视图矩阵 Matrix.setLookAtM(mViewMatrix, 0, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f); // 计算mvp矩阵 Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0); }
-
用MVP矩阵替换原来的单位矩阵:
文件路径:
com/example/glsurfaceviewdemo/TextureRender.java
private float[] mMVPMatrix; // mvp矩阵 // 接收mvp矩阵 public void setCustomMVPMatrix(float[] mvpMatrix) { if (mvpMatrix.length == 16) { // 确保传入的数组长度为 16 mMVPMatrix = new float[16]; System.arraycopy(mvpMatrix, 0, mMVPMatrix, 0, 16); } else { Log.e("TextureRender", "mvp Matrix length invalid!"); } }
-
获取GPU上的MVP矩阵操作接口:
文件路径:
com/example/glsurfaceviewdemo/TextureRender.java
private void initHandles() { // ... 省略非关键代码 // 获取变换矩阵操作接口的句柄 mMVPMatrixHandle = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix"); // ... 省略非关键代码 }
同时修改点顶点着色器里面glsl代码:
文件路径:
texture_vertex_shader.glsl
#version 300 es // 指定 GLSL 版本 precision mediump float; // 定义浮点数精度 // 统一变量 uniform mat4 uMVPMatrix; // 变换矩阵 // 属性变量 in vec4 aPosition; // 顶点位置 in vec2 aTexCoord; // 纹理坐标 // 输出变量 out vec2 vTexCoord; // 传递给片段着色器的纹理坐标 void main() { // 应用变换矩阵并设置顶点位置 gl_Position = uMVPMatrix * aPosition; // 将纹理坐标传递给片段着色器 vTexCoord = aTexCoord; }
这样,我们iu通过draw方法传递给了
TextureRender
,再通过mMVPMatrixHandle
传递给了Shader,在Shader当中,通过uMVPMatrix
变量接收这个矩阵。最后,在Shader的main函数中通过这个MVP矩阵乘以每个顶点数据,就对图片中的每个顶点完成了变换;
-
完整代码:
文件路径:
com/example/glsurfaceviewdemo/GLRenderTest.java
package com.example.glsurfaceviewdemo; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLES30; import android.opengl.GLSurfaceView; import android.opengl.Matrix; import android.util.Log; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; public class GLRenderTest implements GLSurfaceView.Renderer { private Triangle mTriangle; private TextureRender mTextureRender; private Context mContext; private int mSurfaceWidth = 0; // 窗口宽 private int mSurfaceHeight = 0; // 窗口高 private int mViewportWidth = 0; // 视口宽 private int mViewportHeight = 0; // 视口高 private int mViewportX = 0; // 视口起始横坐标 private int mViewportY = 0; // 视口起始纵坐标 private Bitmap mBitmap; private float[] mProjectionMatrix = new float[16]; // 投影矩阵 private float[] mViewMatrix = new float[16]; // 视图矩阵 private float[] mMVPMatrix = new float[16]; // mvp矩阵 public GLRenderTest(Context context) { this.mContext = context; mBitmap = loadImage(); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); // mTriangle = new Triangle(mContext); mTextureRender = new TextureRender(mContext, mBitmap); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { mSurfaceWidth = width; mSurfaceHeight = height; // calculateViewport(); // 计算视口参数 calculateViewport2(width, height); // 计算正交投影参数 mTextureRender.setCustomMVPMatrix(mMVPMatrix); // 设置mvp矩阵给渲染器 // GLES30.glViewport(mViewportX, mViewportY, mViewportWidth, mViewportHeight); // 视口调整 GLES30.glViewport(0, 0, width, height); // 正交矩阵从屏幕左上角开始即可 } @Override public void onDrawFrame(GL10 gl){ GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); // mTriangle.draw(); mTextureRender.draw(); } public void onDestroy() { // mTriangle.release(); mBitmap.recycle(); // 回收bitmap mTextureRender.release(); } // 加载图片 private Bitmap loadImage() { BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false; return BitmapFactory.decodeResource(mContext.getResources(), R.drawable.android_logo, options); } // 视口变换:通过调整视口大小,保证图片不被拉伸 private void calculateViewport() { // 获取图片的宽高比 float imageRatio = (float) mBitmap.getWidth() / mBitmap.getHeight(); // 获取surface(窗口)的宽高比 float surfaceRatio = (float) mSurfaceWidth / mSurfaceHeight; if (imageRatio > surfaceRatio) { // 图片宽高比大于窗口的宽高比,按照宽度填满 mViewportWidth = mSurfaceWidth; mViewportHeight = (int) (mSurfaceWidth / imageRatio); } else { // 图片宽高比小于等于窗口的宽高比,按照高度填满 mViewportWidth = (int) (mSurfaceHeight * imageRatio); mViewportHeight = mSurfaceHeight; } // 计算视口的中心位置 mViewportX = (mSurfaceWidth - mViewportWidth) / 2; mViewportY = (mSurfaceHeight - mViewportHeight) / 2; } // 正交投影变换 private void calculateViewport2(int width, int height) { float imageAspectRatio = (float) mBitmap.getWidth() / (float) mBitmap.getHeight(); float surfaceAspectRatio = (float) width / (float) height; if (imageAspectRatio > surfaceAspectRatio) { // 图片宽高比大于屏幕,按照宽度填满计算高度 float tb = imageAspectRatio / surfaceAspectRatio; // 计算投影矩阵 Matrix.orthoM(mProjectionMatrix, 0, -1.0f, 1.0f, -tb, tb, -1.0f, 1.0f); } else { // 图片宽高比小于等于屏幕,按照高度填满计算宽度 float tb = surfaceAspectRatio / imageAspectRatio; // 计算投影矩阵 Matrix.orthoM(mProjectionMatrix, 0, -tb, tb, -1.0f, 1.0f, -1.0f, 1.0f); } // 计算视图矩阵 Matrix.setLookAtM(mViewMatrix, 0, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f); // 计算mvp矩阵 Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0); } }
文件路径:
com/example/glsurfaceviewdemo/TextureRender.java
package com.example.glsurfaceviewdemo; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLES30; import android.opengl.GLUtils; import android.opengl.Matrix; import android.util.Log; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; public class TextureRender { private Context mContext; private float[] mCoordData = { // 顶点坐标 纹理坐标 -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, // 左上角 -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, // 左下角 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右上角 1.0f, -1.0f, 0.0f, 1.0f, 1.0f // 右下角 }; private int mTextureId = -1; private FloatBuffer mCoordBuffer; private int mProgram = -1; private int mVboId; private int mPositionHandle = -1; // 顶点位置属性的操作句柄 private int mTexCoordHandle = -1; // 纹理坐标属性的操作句柄 private int mMVPMatrixHandle = -1; // 变换矩阵操作句柄,用于实现顶点的变换 private int mSamplerHandle = -1; // 纹理采样器操作句柄,相当于一个指向某个纹理单元的指针 Bitmap mBitmap; private float[] mMVPMatrix; // mvp矩阵 public TextureRender(Context context, Bitmap bitmap) { mContext = context; mBitmap = bitmap; initialize(); } private void initialize() { mTextureId = uploadTexture(); // 上传纹理到GPU initVertexBuffer(); // 初始化坐标数据 initShaders(mContext); // 加载并编译着色器 initVbo(); // 初始化 VBO initHandles(); // 获取GPU和Shader的一些操作接口 } // 获取GPU和Shader的一些操作接口 private void initHandles() { // 获取顶点坐标操作接口的句柄 mPositionHandle = GLES30.glGetAttribLocation(mProgram, "aPosition"); validateAttributeLocation(mPositionHandle, "aPosition"); GLES30.glEnableVertexAttribArray(mPositionHandle); // 启用位置属性数组 // 获取纹理坐标操作接口的句柄 mTexCoordHandle = GLES30.glGetAttribLocation(mProgram, "aTexCoord"); validateAttributeLocation(mTexCoordHandle, "aTexCoord"); GLES30.glEnableVertexAttribArray(mTexCoordHandle); // 启用纹理坐标属性数组 // 获取变换矩阵操作接口的句柄 mMVPMatrixHandle = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix"); // 用于获取Shader当中纹理采样器的操作接口的句柄 mSamplerHandle = GLES30.glGetUniformLocation(mProgram, "uSampler"); } // 用于验证属性句柄的有效性 private void validateAttributeLocation(int handle, String attributeName) { if (handle == -1) { Log.e("TextureRender", "Could not find attribute " + attributeName); } } // 绘制纹理 public void draw() { // 激活着色器程序 GLES30.glUseProgram(mProgram); // 绑定VBO GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId); // 设置顶点坐标和纹理坐标给OpenGL setupVertexAttribPointer(); // 上传变换矩阵 GLES30.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0); // 绑定纹理并设置采样器 bindTexture(); // 绘制矩形 GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4); // 解绑当前的 VBO,避免后续操作意外影响到这个缓冲 GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0); // 检查 OpenGL 错误 checkOpenGLError(); } // 接收mvp矩阵 public void setCustomMVPMatrix(float[] mvpMatrix) { if (mvpMatrix.length == 16) { // 确保传入的数组长度为 16 mMVPMatrix = new float[16]; System.arraycopy(mvpMatrix, 0, mMVPMatrix, 0, 16); } else { Log.e("TextureRender", "mvp Matrix length invalid!"); } } // 设置顶点坐标和纹理坐标 private void setupVertexAttribPointer() { // 指定位置属性的布局(设置顶点位置数据) // 3表示:每个顶点有3个浮点数(x, y, z),步长stride为 5*Float.BYTES,偏移量是0 GLES30.glVertexAttribPointer(mPositionHandle, 3, GLES30.GL_FLOAT, false, 5 * Float.BYTES, 0); // 指定纹理坐标属性的布局(设置纹理坐标数据) // 2表示:每个纹理有 2 个浮点数(u, v),步长是同样是 5*Float.BYTES,偏移量是3*Float.BYTES(前三个是顶点坐标) GLES30.glVertexAttribPointer(mTexCoordHandle, 2, GLES30.GL_FLOAT, false, 5 * Float.BYTES, 3 * Float.BYTES); } // 绑定纹理 private void bindTexture() { // 激活纹理单元 0 GLES30.glActiveTexture(GLES30.GL_TEXTURE0); // 绑定纹理 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId); // 上传纹理单元索引到Shader中的 uSampler 变量 GLES30.glUniform1i(mSamplerHandle, 0); } // 检查OpengGL的错误 private void checkOpenGLError() { int error = GLES30.glGetError(); if (error != GLES30.GL_NO_ERROR) { Log.e("OpenGL", "OpenGL Error: " + error); } } // 加载并编译着色器 private void initShaders(Context context) { String vertexShaderCode = ShaderController.loadShaderCodeFromFile("texture_vertex_shader.glsl", context); String fragmentShaderCode = ShaderController.loadShaderCodeFromFile("texture_fragment_shader.glsl", context); mProgram = ShaderController.createGLProgram(vertexShaderCode, fragmentShaderCode); if (mProgram == 0) { Log.e("TextureRender", "Failed to create OpenGL program."); } } // 初始化坐标数据 private void initVertexBuffer() { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(mCoordData.length * 4); byteBuffer.order(ByteOrder.nativeOrder()); mCoordBuffer = byteBuffer.asFloatBuffer(); mCoordBuffer.put(mCoordData); mCoordBuffer.position(0); } // 初始化 VBO private void initVbo() { int[] vbos = new int[1]; GLES30.glGenBuffers(1, vbos, 0); mVboId = vbos[0]; GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId); mCoordBuffer.position(0); GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, mCoordBuffer.capacity() * 4, mCoordBuffer, GLES30.GL_STATIC_DRAW); } // 上传纹理到GPU private int uploadTexture() { int[] textureIds = new int[1]; GLES30.glGenTextures(1, textureIds, 0); // 创建纹理 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]); // 绑定纹理 GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR); // 设置缩小策略 GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR); // 设置放大策略 GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, mBitmap, 0); // 纹理上传到GPU GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0); // 解绑纹理,避免后续误操作 return textureIds[0]; } public void release() { GLES30.glDeleteBuffers(1, new int[]{mVboId}, 0); // 删除 VBO GLES30.glDeleteProgram(mProgram); // 删除 shader program } }
文件路径:
texture_vertex_shader.glsl
#version 300 es // 指定 GLSL 版本 precision mediump float; // 定义浮点数精度 // 统一变量 uniform mat4 uMVPMatrix; // 变换矩阵 // 属性变量 in vec4 aPosition; // 顶点位置 in vec2 aTexCoord; // 纹理坐标 // 输出变量 out vec2 vTexCoord; // 传递给片段着色器的纹理坐标 void main() { // 应用变换矩阵并设置顶点位置 gl_Position = uMVPMatrix * aPosition; // 将纹理坐标传递给片段着色器 vTexCoord = aTexCoord; }
-
运行结果:
也成功了!
六、总结:
本文主要介绍了两种防止拉伸的方法,一个是通过修改视口来防止拉伸,第二个是通过设置投影来完成,设置投影主要是设置MVP矩阵。