OpenGL ES 纹理(7)
简述
通过前面几章的学习,我们已经可以绘制渲染我们想要的逻辑图形了,但是如果我们想要渲染一张本地图片,这就需要纹理了。
纹理其实是一个可以用于采样的数据集,比较典型的就是图片了,我们知道我们的片段着色器会对每一个像素都执行一次来计算,该像素应该渲染什么颜色,纹理就是一个数据集,比如想要渲染一个图片,我们就是用图片的所有像素信息作为总数据集,然后片段着色器计算的时候就根据像素坐标去图片纹理数据集中取出对应的像素。
这一节我们就通过纹理来渲染一张图片。
接口介绍
- glGenTextures 申请分配纹理
public static native void glGenTextures(int n, int[] textures, int offset);
第一个参数为需要分配纹理个数,第二个申请的纹理句柄(是一个出参),第三个是偏移 - glTexParameteri 配置纹理参数
public static native void glTexParameteri(int target, int pname, int param);
第一个参数是目标,一般使用GL_TEXTURE_2D,第二和第三个参数分别是需要配置的参数的名称和值。
下面我们列举几个常用的参数:- GL_TEXTURE_MAG_FILTER 当显示区域大于原来纹理时,如何放大图像
- GL_NEAREST 会使用靠近的像素作为扩增的像素(计算量小但是效果差)
- GL_LINEAR 求附近像素的加权平均值
- GL_TEXTURE_MIN_FILTER 当显示区域小于原来纹理时,如何缩小图像,GL_NEAREST, GL_LINEAR值和GL_TEXTURE_MAG_FILTER类似。
- GL_TEXTURE_WRAP_T / GL_TEXTURE_WRAP_S
- 纹理的ST坐标系,对应XY坐标,纹理的坐标范围是0-1,这个参数就是控制如果超出这个坐标范围的表现。GL_CLAMP会使用边缘拉伸,GL_REPEAT会重复使用图像填充
- GL_TEXTURE_MAG_FILTER 当显示区域大于原来纹理时,如何放大图像
- GLUtils.texImage2D 加载纹理
用于使用位图来填充纹理 - glGenerateMipmap 开启多级细节。我们看越远的东西就会越小,这时候可能不需要加载完整的图像,可以降低图像细节提高性能。
纹理坐标系
我们之前提过,OpenGL ES的坐标系是(-1,-1)->(1,1)
在OpenGL ES中,纹理的坐标系是(0,0)-> (1,1)的,且纹理的起始坐标(0,0)是在左上角。
纹理渲染图片
原图
顶点数据
顶点数据和绘制正方形时候类似,依旧是4个顶点,然后索引缓冲区中6个点,以两个三角形拼接成一个正方形。
其中每个顶点有5个数据,前三个作为OpenGL的坐标,后两个作为纹理坐标,这里坐标对应关系是OpenGLES坐标系(-0.5,-0.5)对应纹理坐标系(1, 1),即OpenGL ES左下角对应图片的右下角,最终效果是图片顺时针旋转来90度。
private float[] vertexArray = new float[] {
-0.5f, -0.5f, 0.0f, 1, 1f,
0.5f, -0.5f, 0.0f, 1f, 0f,
-0.5f, 0.5f, 0.0f, 0f, 1f,
0.5f, 0.5f, 0.0f, 0f, 0f,
};
private short[] indexArray = new short[] {
0,1,2,
1,2,3
};
着色器
顶点着色器有两个属性,一个vPosition为OpenGL ES坐标,另一个vCoordinate为纹理坐标。纹理坐标会通过varying变量outCoordinate透传给片段着色器。
片段着色器中有一个统一变量vTexture,类型为sampler2D,它相当于纹理的句柄,我门通过texture2D方法,传入纹理句柄和当前纹理坐标即可获取当前坐标的颜色。
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"attribute vec2 vCoordinate;" +
"varying vec2 outCoordinate;" +
"void main() {" +
" gl_Position = vPosition;" +
" outCoordinate = vCoordinate;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"varying vec2 outCoordinate;" +
"uniform sampler2D vTexture;" +
"void main() {" +
" gl_FragColor = texture2D(vTexture,outCoordinate);" +
"}";
顶点数据填充并加载纹理
顶点填充数据逻辑和孩子i去哪都一样,这里额外调用来一个loadTexture(getContext(), R.drawable.test),R.drawable.test是我们的需要显示的图片资源,我们来看看loadTexture里面做了什么。
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 清除颜色
GLES30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// 创建顶点缓冲区
int[] idBuffer = new int[2];
GLES30.glGenBuffers(2, idBuffer, 0);
vertexBufferId = idBuffer[0];
elementBufferId = idBuffer[1];
// 顶点缓冲区数据填充
FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexBuffer.put(vertexArray);
vertexBuffer.position(0);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
GLES30.glBufferData(
GLES30.GL_ARRAY_BUFFER,
vertexArray.length * 4,
vertexBuffer,
GLES30.GL_STATIC_DRAW
);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
ShortBuffer indexBuffer = ByteBuffer.allocateDirect(indexArray.length * 4).order(ByteOrder.nativeOrder()).asShortBuffer();
indexBuffer.put(indexArray);
indexBuffer.position(0);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId);
GLES30.glBufferData(
GLES30.GL_ELEMENT_ARRAY_BUFFER,
indexArray.length * 4,
indexBuffer,
GLES30.GL_STATIC_DRAW
);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);
loadTexture(getContext(), R.drawable.test);
// shader
shaderProgramId = initShaderProgram(vertexShaderCode, fragmentShaderCode);
}
loadTexture
这里调用的方法我们在之前都已经介绍过了,这个过程其实和创建顶点缓冲区很想,先申请一个纹理,申请纹理会返回一个纹理句柄,然后绑定纹理,配置参数,配置纹理对应的位图,再解绑纹理,后续就可以通过这个纹理句柄来使用这个纹理了。
public int loadTexture(final Context context, final int resourceId) {
GLES30.glGenTextures(1, textureHandle, 0);
if (textureHandle[0] != 0) {
// 绑定纹理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureHandle[0]);
// 设置纹理参数
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR_MIPMAP_LINEAR);
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
// 加载纹理
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId);
GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0);
// 生成多级细节
GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D);
// 释放Bitmap资源
bitmap.recycle();
}
// 解绑纹理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
return textureHandle[0];
}
顶点布局以及渲染
渲染的流程和之前类似,处理配置顶点布局以外,这里主要新增的逻辑就是需要通过glBindTexture绑定纹理,还有将纹理句柄通过统一变量传给片段着色器。
public void onDrawFrame(GL10 gl) {
// 清除屏幕
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
// 使能着色器程序
GLES30.glUseProgram(shaderProgramId);
// 绑定纹理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureHandle[0]);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
int positionLocation = GLES30.glGetAttribLocation(shaderProgramId, "vPosition");
GLES30.glEnableVertexAttribArray(positionLocation);
GLES30.glVertexAttribPointer(positionLocation, 3, GLES30.GL_FLOAT, false, 5 * 4, 0);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
int coordinateLocation = GLES30.glGetAttribLocation(shaderProgramId, "vCoordinate");
GLES30.glEnableVertexAttribArray(coordinateLocation);
GLES30.glVertexAttribPointer(coordinateLocation, 2, GLES30.GL_FLOAT, false, 5 * 4, 3 * 4);
// 配置纹理句柄到统一变量中去
int vTextureLocation = GLES30.glGetUniformLocation(shaderProgramId, "vTexture");
GLES30.glUniform1f(vTextureLocation, textureHandle[0]);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId);
// 调用DrawCall绘制三角形
GLES30.glDrawElements(GLES30.GL_TRIANGLES, 6, GLES30.GL_UNSIGNED_SHORT, 0);
// 清除配置
GLES30.glDisableVertexAttribArray(positionLocation);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
GLES30.glUseProgram(0);
}
效果
如预期一样,这里相比原图顺时针旋转了90度。
小结
本节通过演示如何使用纹理加载渲染一个图片来介绍了纹理的使用方式,除此之外还通过纹理坐标系和OpenGL ES坐标系的映射关系将图片做了一个旋转,其实我们还有一个方式专门来做图像的变换,就是变化矩阵,我们下一节会专门来介绍这个。