一.理解纹理
OpenGL中的纹理可以用来表示照片,图像。每个二维的纹理都由许多小的纹理元素组成,他们是小块的数据,类似于我们前面讨论的片段和像素。要使用纹理,最直接的方式是从图像文件加载数据。我们现在要加载下面这副图像作为空气曲棍球桌子的表面纹理:
我们将其存储在drawable文件夹中即可。每个纹理都有坐标空间,其范围是从一个拐角(0,0)到另一个拐角(1,1),我们想要把一个纹理应用到一个或多个三角形时,我们要为每个顶点指定一个纹理坐标,以便让OpenGL知道用纹理的哪个部分画到每个三角形上。按照惯例,一个二维的纹理一个维度称作S,另一个维度称作T。
二.把纹理加载进OpenGL中
我们的第一个任务是将一副图像文件的数据加载到一个OpenGL的纹理中,我们将创建一个新的类TextureHelper,并在其中完成加载纹理的工作。在进行这个工作之前,我们先来了解一下纹理过滤,当纹理大小被放大或缩小时,我们要使用纹理过滤明确说明会发生什么。当我们在渲染表面绘制一个纹理时,那个纹理的纹理元素可能无法精确的映射到OpenGL生成的片段上,此时会出现两种情况,放大和缩小。当我们将几个纹理元素挤到一个片段时,缩小就发生了;当我们把一个纹理元素扩大到几个片段上时,放大就发生了。针对每种情况,我们都需要配置纹理过滤器。我们会通过glTexParameteri()函数设置纹理过滤模式,下面是OpenGL支持的纹理过滤模式:
并且放大和缩小两种情况下所允许的纹理过滤模式有所不同,如下所示:
下面,是加载纹理的代码:
class TextureHelper {
companion object {
val TAG="TextureHelper"
fun loadTexture(context: Context, id:Int):Int{//加载由id指定的图像,并生成纹理对象返回
//生成纹理对象
val textureObjectIds= IntArray(1)
glGenTextures(1,textureObjectIds,0)
if(textureObjectIds[0]==0){//返回0表示创建纹理对象失败
Log.i(TAG,"could not generate texture object")
return 0
}
val option= BitmapFactory.Options()
option.inScaled=false//保留原始图像,取消缩放
//OpenGL不能直接使用压缩的jpg,png图像,要解码为它能理解的位图数据
val bitmap=BitmapFactory.decodeResource(context.resources,id,option)
if(bitmap==null){
Log.i(TAG,"decode failed.")
glDeleteTextures(1,textureObjectIds,0)
return 0
}
//告诉OpenGL后面的纹理调用应该应用于这个纹理对象
glBindTexture(GL_TEXTURE_2D,textureObjectIds[0])
//设置纹理过滤
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR)//处理图片缩小的情况
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR)//处理图片放大的情况
//加载位图数据到opengl,并复制到当前绑定的纹理对象
GLUtils.texImage2D(GL_TEXTURE_2D,0,bitmap,0)
//使用完后,回收位图数据
bitmap.recycle()
glGenerateMipmap(GL_TEXTURE_2D)//生成各种级别的贴图
glBindTexture(GL_TEXTURE_2D,0)//解除绑定当前的纹理对象
return textureObjectIds[0]//返回纹理对象id
}
}
}
三.创建新的着色器集合
在把纹理绘制到屏幕之前,我们需要创建一套新的着色器,他们可以接收纹理,并且把它们应用到要绘制的片段上。这些新的着色器和我们之前使用的着色器非常类似,只是为了支持纹理做了轻微的改动。
1.创建新的顶点着色器:texture_vertex_shader.glsl
#version 300 es
layout(location=0) uniform mat4 u_Matrix;
layout(location=0) in vec4 a_Position;
layout(location=1) in vec2 a_TextureCoordinates;
out vec2 v_TextureCoordinates;
void main() {
v_TextureCoordinates=a_TextureCoordinates;
gl_Position=u_Matrix*a_Position;
}
我们用uniform定义了一个向量a_TextureCoordinates,用于接收纹理坐标,由于纹理是二维的,所以这里我们也定义成了二维的,然后将其传递给片段着色器。
2.创建新的片段着色器:texture_fragment_shader.glsl
#version 300 es
precision mediump float;
layout(location=1) uniform sampler2D u_TextureUnit;
in vec2 v_TextureCoordinates;
out vec4 fragColor;
void main() {
fragColor=texture(u_TextureUnit,v_TextureCoordinates);
}
为了把纹理绘制到一个物体上,OpenGL会为每个片段都调用片段着色器,并且每个片段都接收v_TextureCoordinates的纹理坐标。片段着色器也通过u_TextureUnit变量接收实际的纹理数据,u_TextureUnit被定义为一个sampler2D类型,它指定是一个二维纹理数据的数组。被插值的纹理坐标和纹理数据被传递给着色器函数texture2D(),它会读入纹理中那个特定坐标处的颜色值,然后把结果赋值给fragColor,以便设置片段的颜色。
四.为顶点数据创建新的类结构
首先,我们要把顶点数组分离到不同的类中,每个类代表一个物理对象的类型。我们为桌子创建一个新类,并为木槌创建另一个类。为了避免重复,我们会创建一个单独的类用于封装实际的顶点数组,新的类结构如下图所示:
Table用于存储桌子的顶点数据,Mallet用于存储木槌的顶点数据,VertexArray用于存储实际的FloatBuffer数据,并且Table和Mallet都持有一个VertexArray实例。
我们先从VertexArray开始,新建一个VertexArray类,并加入以下代码:
class VertexArray(vertexData:FloatArray) {
private var floatBuffer: FloatBuffer
init {
floatBuffer= ByteBuffer
.allocateDirect(vertexData.size*4)//一个浮点数占4个字节
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData)
}
fun setVertexAttribPointer(dataOffset:Int,attributeLocation:Int,componentCount:Int,stride:Int){//关联属性和顶点数据的数组
floatBuffer.position(dataOffset)
glVertexAttribPointer(attributeLocation,componentCount,GL_FLOAT,false,stride,floatBuffer)
glEnableVertexAttribArray(attributeLocation)
floatBuffer.position(0)
}
}
创建一个Table类,这个类会存储桌子的位置数据,我们还会加入纹理坐标,并把这个纹理应用于桌子。代码如下所示:
class Table {
private var vertexArray:VertexArray
companion object{
val position_component_count=2//记录顶点的位置由两个分量表示
val texture_coordinates_component_count=2//记录纹理坐标用两个分量表示
val stride=(position_component_count+ texture_coordinates_component_count)*4//两个点的跨距
val vertex_data= floatArrayOf(
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
)
}
init {
vertexArray= VertexArray(vertex_data)
}
fun bindData(){//为位置属性和纹理坐标属性绑定数据
vertexArray.setVertexAttribPointer(0,0, position_component_count, stride)
vertexArray.setVertexAttribPointer(position_component_count,1, texture_coordinates_component_count, stride)
}
fun draw(){
glDrawArrays(GL_TRIANGLE_FAN,0,6)
}
}
这个vertex_data数组中包含了空气曲棍球桌子的顶点数据,我们定义了x,y的位置以及S和T纹理坐标。我们需要注意的是S轴的方向是向右为正的,范围是从0到1,T轴是向下为正的,范围也是从0到1。我们还使用了0.1和0.9作为T的坐标,为什么?因为桌子是1个单位宽,1.6个单位高,而纹理图像是512x1024,因此如果宽对应一个单位,那么高就对应两个单位,如果我们使用[0,1]范围的T值的话,即整幅图像的高,那么这副图像的高就会被压缩。我们选择纹理图像[0.1,0.9]范围的高,对图像进行了裁剪,取图像的中间部分,这时,宽高比正好是1:1.6,纹理图像就不会被压缩了。
创建一个Mallet类,用于管理木槌数据。代码如下:
class Mallet() {
private var vertexArray:VertexArray
companion object{
val position_component_count=2//记录顶点的位置由两个分量表示
val color_component_count=3//记录顶点颜色用三个分量表示
val stride=(position_component_count+ color_component_count)*4//两个点的跨距
val vertex_data= floatArrayOf(
0f,-0.4f,0f,0f,1f,
0f,0.4f,1f,0f,0f,
)
}
init{
vertexArray= VertexArray(vertex_data)
}
fun bindData(){
vertexArray.setVertexAttribPointer(0,0, position_component_count,stride)
vertexArray.setVertexAttribPointer(position_component_count,1, color_component_count, stride)
}
fun draw(){
glDrawArrays(GL_POINTS,0,2)
}
}
接下来,我们会为纹理着色器程序创建一个类,为颜色着色器程序创建另一个类,我们会用纹理着色器绘制桌子,并用颜色着色器绘制木槌。我们也会创建一个基类作为他们的公共函数,我们不需要画中间那条线,因为那是纹理的一部分,类的继承结构如下:
我们先给ShaderHelper类中加入一个函数用于编译着色器并链接成OpenGL程序,代码如下:
fun buildProgram(vertexShaderSource:String,fragmentShaderSource:String):Int{
var program=0
val vertexShader=compileVertexShader(vertexShaderSource)
val fragmentShader=compileFragmentShader(fragmentShaderSource)
program= linkProgram(vertexShader,fragmentShader)
return program
}
现在我们来创建ShaderProgram类,代码如下:
open class ShaderProgram(context: Context, vertexShaderSourceId:Int, fragmentShaderSourceId:Int) {
var program=0
init{
program=ShaderHelper.buildProgram(
TextResourceReader.readTextFileFromResource(context,vertexShaderSourceId),
TextResourceReader.readTextFileFromResource(context,fragmentShaderSourceId)
)
}
fun useProgram(){
glUseProgram(program)
}
}
加入纹理着色器程序TextureShaderProgram类:
class TextureShaderProgram(context: Context):ShaderProgram(context,R.raw.texture_vertex_shader,R.raw.texture_fragment_shader) {
fun setUniforms(matrix:FloatArray,textureId:Int){//给uniform变量传递数据
glUniformMatrix4fv(0,1,false,matrix,0)//传递投影矩阵
//在opengl里使用纹理进行绘制时,不需要直接传递纹理给着色器,我们使用纹理单元texture unit保存那个纹理,然后将纹理单元传递给着色器
glActiveTexture(GL_TEXTURE0)//激活纹理单元0
glBindTexture(GL_TEXTURE_2D,textureId)//绑定纹理
glUniform1i(1,0)
}
}
加入颜色着色器程序ColorShaderProgram类:
class ColorShaderProgram(context: Context):ShaderProgram(context,R.raw.simple_vertex_shader,R.raw.simple_fragment_shader) {
fun setUniforms(matrix:FloatArray){
glUniformMatrix4fv(0,1,false,matrix,0)
}
}
现在,我们已经把顶点数据和着色器程序放在不同的类了,现在就可以更新渲染器类,使用纹理进行绘制了。打开MyRenderer类,删掉所有代码,只保留onSurfaceChanged()函数,修改后的代码如下所示:
class MyRenderer(val context: Context):Renderer {
private val projectionMatrix:FloatArray=FloatArray(16)//存储投影矩阵
private val modelMatrix:FloatArray=FloatArray(16)//存储模型矩阵
private var table:Table?=null
private var mallet:Mallet?=null
private var textureShaderProgram:TextureShaderProgram?=null
private var colorShaderProgram:ColorShaderProgram?=null
private var texture=0
override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
glClearColor(0.0F,0.0F,0.0F,0.0F)//设置清除所使用的颜色,参数分别代表红绿蓝和透明度
table= Table()
mallet= Mallet()
textureShaderProgram= TextureShaderProgram(context)
colorShaderProgram= ColorShaderProgram(context)
texture=TextureHelper.loadTexture(context,R.drawable.air_hockey_surface)
}
override fun onSurfaceChanged(p0: GL10?, width: Int, height: Int) {
glViewport(0,0,width,height)//是一个用于设置视口的函数,视口定义了在屏幕上渲染图形的区域。这个函数通常用于在渲染过程中指定绘图区域的大小和位置,前两个参数x,y表示视口左下角在屏幕的位置
Matrix.perspectiveM(projectionMatrix,0,45f,width.toFloat()/height.toFloat(),1f,10f)
//生成模型矩阵
Matrix.setIdentityM(modelMatrix,0)//设置为单位矩阵
Matrix.translateM(modelMatrix,0,0f,0f,-3.5f)//将z值平移到可见范围内
Matrix.rotateM(modelMatrix,0,-60f,1f,0f,0f)//绕x轴旋转-60度
val temp:FloatArray=FloatArray(16)//存储矩阵相乘的结果
Matrix.multiplyMM(temp,0,projectionMatrix,0,modelMatrix,0)
System.arraycopy(temp,0,projectionMatrix,0,temp.size)//将temp复制到projectionMatrix
}
override fun onDrawFrame(p0: GL10?) {
glClear(GL_COLOR_BUFFER_BIT)//清除帧缓冲区内容,和glClearColor一起使用
//绘制桌子
textureShaderProgram?.useProgram()
textureShaderProgram?.setUniforms(projectionMatrix,texture)
table?.bindData()
table?.draw()
//绘制木槌
colorShaderProgram?.useProgram()
colorShaderProgram?.setUniforms(projectionMatrix)
mallet?.bindData()
mallet?.draw()
}
}
最后,运行程序,看看纹理是否绘制在球桌上了。