一.理解纹理
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类型,它指定是一个二维纹理数据的数组。被插值的纹理坐标和纹理数据被传递给着色器函数texture(),它会读入纹理中那个特定坐标处的颜色值,然后把结果赋值给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() } }
最后,运行程序,看看纹理是否绘制在球桌上了。
标签:val,component,纹理,细节,0f,增加,GL,着色器 From: https://www.cnblogs.com/luqman/p/18006810