前言
通过前面 2D 图像绘制的学习, 让我们对 GL 渲染图像有了一定的了解
作为非游戏开发者, 我们很少会自己绘制图形, 更多的是去绘制一张 2D 的图片, 通过纹理便可以实现这一功能
纹理的定义如下
纹理可以理解为 2D 的贴纸, 我们可以通过这个贴纸, 将纹理图像映射到 OpenGL 的图形上
接下来探索一下纹理相关的知识点, 最终提供一个 fitCenter 和 centerCrop 的实现
一. 纹理坐标系统
纹理坐标系统, 即针对于纹理画布建立的坐标系统, 其样式如下
纹理的坐标系为 TSR 坐标系, 2D 纹理只用到 TS, 其中 1 描述加载的纹理的初始大小, 与 GL 坐标系一样, 是一个归一化的值
二. 纹理的环绕
纹理原始的范围通常是[0, 1], 当我们选取纹理的区域超过了原始范围, 则会触发纹理的环绕效果
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
一) 视觉效果
二) 操作 API
// 设置 S 为 GL_REPEAT
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
// 设置 T 为 GLREPEAT
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
三. 纹理的过滤
将纹理映射到 GL 图形上时, 可能会对纹理进行缩放, 这个过程称之为纹理过滤(类似于图片的采样操作), GL 提供了纹理过滤的选项, 主要有 GL_LINEAR 和 GL_NEAREST
一) GL_LINEAR(线性过滤)
GL_LINEAR(也叫线性过滤,(Bi)linear Filtering) 它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。
一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。
下图中你可以看到返回的颜色是邻近像素的混合色:
二) GL_NEAREST(邻近过滤)
GL_NEAREST 是 OpenGL 默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。
下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
三) 操作 API
// 设置缩小过滤
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
// 设置放大过滤
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
四. 纹理绘制实战
这里以绘制 Bitamp 为例
一) 生成纹理
public class TextureRenderer implements GLSurfaceView.Renderer {
/**
* 创建纹理
*/
private int createTextureFromRes(int resId) {
// 生成绑定纹理
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
int textureId = textures[0];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
// 设置环绕方向
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
// 设置纹理过滤方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
// 将 Bitmap 生成 2D 纹理
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), resId);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
// 解绑
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
return textureId;
}
}
二) 定义坐标
public class TextureRenderer implements GLSurfaceView.Renderer {
/**
* 矩形顶点坐标(定义在 GL 世界坐标系中)
*/
private float[] mRectCoords = new float[]{
// 矩形顶点坐标
-1f, 1f, // 左上
-1f, -1f, // 左下
1f, 1f, // 右上
1f, -1f, // 右下
};
private FloatBuffer mRectCoordsBuffer;
/**
* 取纹理区域的坐标(原始纹理的范围为 [0, 1], 我们可以指定取纹理的哪些部分)
* <p>
* 因为要贴到上面定义的矩形上, 因此其位置要与矩形的顶点一一对应
* <p>
* 图片的 Y 轴通常是向下的, 我在取我们所需部分时, 需要上下颠倒一下映射, 这样可以保证纹理映射到矩形上时正常的
*/
private float[] mTextureCoords = new float[]{
0f, 0f, // 纹理左下
0f, 1f, // 纹理左上
1f, 0f, // 纹理右下
1f, 1f // 纹理右上
};
private FloatBuffer mTextureCoordsBuffer;
public TextureRenderer(Context context) {
this.mContext = context;
// 初始化顶点坐标
mRectCoordsBuffer = ByteBuffer.allocateDirect(mRectCoords.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(mRectCoords);
mRectCoordsBuffer.position(0);
// 初始化纹理顶点坐标
mTextureCoordsBuffer = ByteBuffer.allocateDirect(mTextureCoords.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(mTextureCoords);
mTextureCoordsBuffer.position(0);
}
}
这里选取的纹理坐标有两个点需要注意
- 纹理坐标一般情况下需要保证与矩形的顶点坐标的顺序一致, 否则会导致映射偏差
- 我们这里纹理与 GL 图形映射时, 对纹理坐标进行了上下颠倒的处理
- 这是因为一般图片的坐标 Y 轴是向下的, 因此在其读入内存生成纹理时, 在纹理坐标系上图形就已经上下颠倒了, 我们在选取的纹理顶点与 GL 图形进行映射时再颠倒一次就能够保证显示效果的正常了
三) 定义着色器
1. 顶点着色器
// 定义一个属性,图形顶点坐标
attribute vec4 aShapeCoords;
// 定义一个属性,纹理顶点坐标
attribute vec2 aTextureCoords;
// 顶点裁剪矩阵
uniform mat4 uMatrix;
// varying 可用于相互传值
varying vec2 vTextureCoords;
void main() {
// 暂存纹理的坐标, 到片元着色器中使用
vTextureCoords = aTextureCoords;
// gl_Position 为内置变量, 根据 u_Matrix 计算出裁剪坐标系的位置
gl_Position = aShapeCoords * uMatrix;
}
2. 片元着色器
// 着色器纹理扩展类型
#extension GL_OES_EGL_image_external : require
// 设置精度,中等精度
precision mediump float;
// varying 可用于相互传值
varying vec2 vTextureCoords;
// 2D 纹理 ,uniform 用于 application 向 gl 传值
uniform sampler2D uTexture;
void main() {
gl_FragColor = texture2D(uTexture, vTextureCoords);//进行纹理采样,拿到当前颜色
}
三) 定义变换矩阵
通过上面的 GL 矩形是坐标可知, 它是填充整个 View 的, 若是纹理直接映射到 GL 矩形上, 可能会出现比例失调的问题, 也就是 ImageView 中的 fitXY 的 ScaleType
因此我们需要使用投影矩阵, 来矫正视觉观感, 下面给出 fitCenter 和 centerCrop 的投影实现
1. fitCenter 投影
可以看到 fitCenter 的投影面是大于 GL 矩形的, 以保证 GL 矩形能够以正常的比例全部显示在投影平面内, 因为矩形的大小与纹理是对应的, 因此
public class TextureRenderer implements GLSurfaceView.Renderer {
/**
* 定义投影矩阵
* <p>
* 这里省略了视图矩阵, 我们初始坐标系即观察者坐标系
*/
private final float[] mProjectionMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
......
// 2. 实现 fitCenter
// 获取比例
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeResource(mContext.getResources(), mResId, opts);
// 计算 Bitmap 的宽高比
float aspectBitmap = opts.outWidth / (float) opts.outHeight;
// 计算当前画布 Surface 的宽高比
float aspectSurface = width / (float) height;
// fitCenter
fitCenter(aspectSurface, aspectBitmap);
}
private void fitCenter(float aspectPlane, float aspectTexture) {
float left, top, right, bottom;
// 1. 纹理比例 > 投影平面比例
if (aspectTexture > aspectPlane) {
left = -1;
right = 1;
top = 1 / aspectPlane * aspectTexture;
bottom = -top;
}
// 2. 纹理比例 < 投影平面比例
else {
left = -aspectPlane / aspectTexture;
right = -left;
top = 1;
bottom = -1;
}
Matrix.orthoM(
mProjectionMatrix, 0,
left, right, bottom, top,
1, -1
);
}
}
2. centerCrop
centerCrop 与 fitCenter 不同, 它的投影平面小于纹理, 以保证 GL 矩形铺满整个平面
public class TextureRenderer implements GLSurfaceView.Renderer {
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
......
// 获取比例
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeResource(mContext.getResources(), mResId, opts);
// 计算 Bitmap 的宽高比
float aspectBitmap = opts.outWidth / (float) opts.outHeight;
// 计算当前画布 Surface 的宽高比
float aspectSurface = width / (float) height;
// centerCrop
centerCrop(aspectSurface, aspectBitmap);
}
private void centerCrop(float aspectPlane, float aspectTexture) {
float left, top, right, bottom;
// 1. 纹理比例 > 投影平面比例
if (aspectTexture > aspectPlane) {
left = -aspectPlane / aspectTexture;
right = -left;
top = 1;
bottom = -1;
}
// 2. 纹理比例 < 投影平面比例
else {
left = -1;
right = 1;
top = 1 / aspectPlane * aspectTexture;
bottom = -top;
}
Matrix.orthoM(
mProjectionMatrix, 0,
left, right, bottom, top,
1, -1
);
}
}
四) 纹理的绘制
纹理的绘制流程就比较简单了
public class TextureRenderer implements GLSurfaceView.Renderer {
@Override
public void onDrawFrame(GL10 gl) {
// 清屏并绘制白色
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(1f, 1f, 1f, 1f);
// 绘制纹理
// 激活 Program
GLES20.glUseProgram(mProgram);
// 写入顶点坐标数据
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);
GLES20.glEnableVertexAttribArray(aShapeCoords);
GLES20.glVertexAttribPointer(aShapeCoords, 2, GLES20.GL_FLOAT, false, 8, 0);
// 写入纹理坐标数据
GLES20.glEnableVertexAttribArray(aTextureCoords);
GLES20.glVertexAttribPointer(aTextureCoords, 2, GLES20.GL_FLOAT, false, 8,
mRectCoords.length * 4);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
// 写入裁剪矩阵数据
GLES20.glUniformMatrix4fv(uMatrix, 1, false, mProjectionMatrix, 0);
// 激活纹理
GLES20.glUniform1i(uTexture, 0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
// 执行绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
// 解绑纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
}
}
绘制展示
fitCenter
centerCrop
总结
从纹理的实战中可以看到, 纹理的绘制比起图形的绘制更有趣, 其中需要掌握
- 纹理的坐标
- TSR
- 环绕方式
- GL_REPEAT: 对纹理的默认行为。重复纹理图像。
- GL_MIRRORED_REPEAT: 和GL_REPEAT一样,但每次重复图片是镜像放置的。
- GL_CLAMP_TO_EDGE: 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
- GL_CLAMP_TO_BORDER: 超出的坐标为用户指定的边缘颜色。
- 过滤方式
- GL_LINEAR(线性过滤)
- GL_NEAREST(邻近过滤)
- 纹理的绘制
- 图像读入纹理坐标
- 从纹理坐标选取所需的区域
- 将选取的区域映射到 GL 图形
通过手写了两个非常常用的 fitCenter 和 centerCrop 效果的实现, 对坐标系统有了更加深刻的理解, 这对后面自定义相机的实战有着非常大的帮助