public static int loadTexture(Context context, int resourceId) {}
首先,创建一个纹理对象,与普通OpenGL对象生成模式一样。生成成功之后,申明纹理调用应该应用于这个纹理对象。其次,加载位图数据,OpenGL读入位图数据并复制到前面绑定的纹理对象。
final int[] textureObjectIds = new int[1];
glGenTextures(1, textureObjectIds, 0);
if (textureObjectIds[0] == 0) {
if (LoggerConfig.ON) {
Log.w(TAG, "Could not generate a new OpenGL texture object.");
return 0;
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
// Read in the resource
final Bitmap bitmap = BitmapFactory.decodeResource(
context.getResources(), resourceId, options);
if (bitmap == null) {
if (LoggerConfig.ON) {
Log.w(TAG, "Resource ID " + resourceId + " could not be decoded.");
glDeleteTextures(1, textureObjectIds, 0);
return 0;
// Bind to the texture in OpenGL
glBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);
这两段代码需要说明的并不多,其中options.inScaled = false表明OpenGL读入图像的非压缩形式的原始数据。OpenGL读入位图数据需要注意一点:纹理过滤。OpenGL纹理过滤模式如下表:(--内容来自原书)
三线性过滤(使用MIP贴图级别之间插值的双线性过滤)
至于每种过滤具体的解释及实现,请自行Google吧。这里对于缩小情况,采用了GL_LINEAR_MIPMAP_LINEAR,对于放大情况,采用了GL_LINEAR。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
加载纹理的最后一步就是将bitmap复制到当前绑定的纹理对象:
texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
绑定之后,仍然需要做一些后续操作,比如回收bitmap对象(bitmap内存占用大户),生成MIP贴图,接触纹理绑定,最后返回纹理对象ID。
glGenerateMipmap(GL_TEXTURE_2D);
// Recycle the bitmap, since its data has been loaded into OpenGL.
bitmap.recycle();
// Unbind from the texture.
glBindTexture(GL_TEXTURE_2D, 0);
return textureObjectIds[0];
二、纹理着色器
在继续采用GLSL编写着色器程序之前,先说明下之前遗漏的一个问题:
OpenGL着色语言(OpenGL Shading Language)是用来在OpenGL中着色编程的语言,也即开发人员写的短小的自定义程序,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程型。比如:视图转换、投影转换等。
GLSL(GL Shading Language)的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment(片断着色器),有时还会有Geometry Shader(几何着色器)。负责运行顶点着色的是顶点着色器。它可以得到当前OpenGL 中的状态,GLSL内置变量进行传递。GLSL其使用C语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。
这段内容来自百度百科,有一点需要重视:采用GLSL编写的程序是在GPU中执行的,意味着着色器程序并不占用CPU时间,这启发我们在某些耗时的渲染程序(摄像头实时滤镜)中可以采用GLSL实现,或许比NDK方式实现数据处理更为高效。后续笔者会在这方面实践,这里先说明纹理着色器程序。同样,为了支持纹理,需对顶点着色器和片段着色器进行更改。
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec2 a_TextureCoordinates;
varying vec2 v_TextureCoordinates;
void main()
v_TextureCoordinates = a_TextureCoordinates;
gl_Position = u_Matrix * a_Position;
precision mediump float;
uniform sampler2D u_TextureUnit;
varying vec2 v_TextureCoordinates;
void main()
gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates);
上述顶点着色器中,变量a_TextureCoordinates的类型为vec2,因为纹理坐标的两个分量:S坐标和T坐标。片段着色器中,sampler2D类型的u_TextureUnit表示接收二维纹理数据的数组。
三、更新顶点数据类结构
首先将不同类型的顶点数据分配到不同的类中,每个类代表一个物理对象的类型。在类的构造器中初始化VertexArray对象,VertexArray的实现与前述文章中描述的一致,采用FloatBuffer在本地代码中存储顶点矩阵数据,并创建通用方法将着色器的属性与顶点数据关联。
private final FloatBuffer floatBuffer;
public VertexArray(float[] vertexData) {
floatBuffer = ByteBuffer
.allocateDirect(vertexData.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
public void setVertexAttribPointer(int dataOffset, int attributeLocation,
int componentCount, int stride) {
floatBuffer.position(dataOffset);
glVertexAttribPointer(attributeLocation, componentCount, GL_FLOAT,
false, stride, floatBuffer);
glEnableVertexAttribArray(attributeLocation);
floatBuffer.position(0);
public Table() {
vertexArray = new VertexArray(VERTEX_DATA);
构造器中传入的参数VERTEX_DATA就是顶点数据。
private static final float[] VERTEX_DATA = {
// Order of coordinates: X, Y, S, T
// Triangle Fan
0f, 0f, 0.5f, 0.5f,
-0.5f, -0.8f, 0f, 0.9f,
0.5f, -0.8f, 1f, 0.9f,
0.5f, 0.8f, 1f, 0.1f,
-0.5f, 0.8f, 0f, 0.1f,
-0.5f, -0.8f, 0f, 0.9f };
在该组数据中,x=0,y=0对应纹理S=0.5,T=0.5,x=-0.5,y=-0.8对应纹理S=0,T=0.9,之所以有这种对应关系,看下前面讲到的OpenGL纹理坐标与计算机图像坐标的对比就清楚啦。至于纹理部分的数据使用了0.1和0.9作为T坐标,是为了避免把纹理压扁,而对纹理进行了裁剪,截取了0.1到0.9的部分。
初始化vertexArray之后,通过其setVertexAttribPointer()方法将顶点数据绑定到着色器程序上。
public void bindData(TextureShaderProgram textureProgram) {
vertexArray.setVertexAttribPointer(
textureProgram.getPositionAttributeLocation(),
POSITION_COMPONENT_COUNT,
STRIDE);
vertexArray.setVertexAttribPointer(
POSITION_COMPONENT_COUNT,
textureProgram.getTextureCoordinatesAttributeLocation(),
TEXTURE_COORDINATES_COMPONENT_COUNT,
STRIDE);
这个方法为每个顶点调用了setVertexAttribPointer(),并从着色器程序获取每个属性的位置。通过getPositionAttributeLocation()把位置数据绑定到被引用的着色器属性上,并通过getTextureCoordinatesAttributeLocation()把纹理坐标数据绑定到被引用的着色器属性。
完成上述绑定以后,绘制只需要调用glDrawArrays()实现。
public void draw() {
glDrawArrays(GL_TRIANGLE_FAN, 0, 6);
四、着色器程序类
随着纹理的使用,着色器程序变得更多,因此需要为着色器程序添加管理类。根据着色器分类,这里分别创建纹理着色器类和颜色着色器类,且抽象它们的共同点,形成基类ShaderProgram,TextureShaderProgram和ColorShaderProgram分别继承于此实现。ShaderProgram主要的功能就是根据Android上下文Context和着色器资源ID读入着色器程序,其构造器参数列表如下:
protected ShaderProgram(Context context, int vertexShaderResourceId,
int fragmentShaderResourceId) {
读入着色器程序的实现应该在ShaderHelper类中,其步骤与之前所述相似,包括编译、链接等步骤。
public static int buildProgram(String vertexShaderSource,
String fragmentShaderSource) {
int program;
// Compile the shaders.
int vertexShader = compileVertexShader(vertexShaderSource);
int fragmentShader = compileFragmentShader(fragmentShaderSource);
// Link them into a shader program.
program = linkProgram(vertexShader, fragmentShader);
if (LoggerConfig.ON) {
validateProgram(program);
return program;
compileVertexShader(编译)和linkProgram(链接)的实现在之前的笔记中已详细描述过。ShaderProgram的构造器调用上述buildProgram()方法即可。
program = ShaderHelper.buildProgram(
TextResourceReader.readTextFileFromResource(
context, vertexShaderResourceId),
TextResourceReader.readTextFileFromResource(
context, fragmentShaderResourceId));
得到着色器程序之后,定义OpenGL后续的渲染使用该程序。
public void useProgram() {
// Set the current OpenGL shader program to this program.
glUseProgram(program);
着色器程序类TextureShaderProgram和ColorShaderProgram在构造器中调用父类的构造函数,并读入纹理着色器中uniform和属性的位置。
public TextureShaderProgram(Context context) {
super(context, R.raw.texture_vertex_shader,
R.raw.texture_fragment_shader);
// Retrieve uniform locations for the shader program.
uMatrixLocation = glGetUniformLocation(program, U_MATRIX);
uTextureUnitLocation = glGetUniformLocation(program, U_TEXTURE_UNIT);
// Retrieve attribute locations for the shader program.
aPositionLocation = glGetAttribLocation(program, A_POSITION);
aTextureCoordinatesLocation =
glGetAttribLocation(program, A_TEXTURE_COORDINATES);
接下来,传递矩阵给uniform,这在之前的笔记中描述过了。
// Pass the matrix into the shader program.
glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
纹理的传递相对于矩阵的传递要复杂一些,因为纹理并不直接传递,而是采用纹理单元(Texture Unit)来保存,因为一个GPU只能同时绘制数量有限的纹理,使用这些纹理单元表示正在被绘制的活动的纹理。
// Set the active texture unit to texture unit 0.
glActiveTexture(GL_TEXTURE0);
// Bind the texture to this unit.
glBindTexture(GL_TEXTURE_2D, textureId);
// Tell the texture uniform sampler to use this texture in the shader by
// telling it to read from texture unit 0.
glUniform1i(uTextureUnitLocation, 0);
glActiveTexture(GL_TEXTURE0)表示把活动的纹理单元设置为纹理单元0,调用glBindTexture将textureId指向的纹理绑定到纹理单元0,最后,调用glUniform1i把选定的纹理单元传递给片段着色器中的u_TextureUnit(sampler2D)。
颜色着色器类与纹理着色器类的实现基本类似,同样在构造器中获取uniform和属性的位置,不过设置uniform值只需传递矩阵即可。
public void setUniforms(float[] matrix) {
// Pass the matrix into the shader program.
glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
五、纹理绘制
通过前面的准备,顶点数据,着色器程序已经放到了不同的类中,因此,在渲染类中可以通过前面的实现进行纹理绘制了。AirHockeyRenderer类更新后的成员变量和构造函数如下:
private final Context context;
private final float[] projectionMatrix = new float[16];
private final float[] modelMatrix = new float[16];
private Table table;
private Mallet mallet;
private TextureShaderProgram textureProgram;
private ColorShaderProgram colorProgram;
private int texture;
public AirHockeyRenderer(Context context) {
this.context = context;
初始化变量主要包括清理屏幕、初始化顶点数组和着色器程序,加载纹理等。
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
table = new Table();
mallet = new Mallet();
textureProgram = new TextureShaderProgram(context);
colorProgram = new ColorShaderProgram(context);
texture = TextureHelper.loadTexture(context, R.drawable.air_hockey_surface);
最后,在onDrawFrame()中绘制物体,绘制的方法就是通过调用前面着色器类和物体类(顶点数据)的方法来实现的。
@Override
public void onDrawFrame(GL10 glUnused) {
// Clear the rendering surface.
glClear(GL_COLOR_BUFFER_BIT);
// Draw the table.
textureProgram.useProgram();
textureProgram.setUniforms(projectionMatrix, texture);
table.bindData(textureProgram);
table.draw();
// Draw the mallets.
colorProgram.useProgram();
colorProgram.setUniforms(projectionMatrix);
mallet.bindData(colorProgram);
mallet.draw();
总结一下,这篇笔记涉及到一下内容:
1)加载纹理并显示到物体上;
2)重新组织程序,管理多个着色器和顶点数据之间的切换;
3)调整纹理以适应它们将要被绘制的形状,既可以调整纹理坐标,也可以通过拉伸或压扁纹理本身来实现;
4)纹理不能直接传递,需要被绑定到纹理单元,然后将纹理单元传递给着色器;
相信未来,热爱生命