5

用纹理增加细节 - 故乡的樱花开了

 7 months ago
source link: https://www.cnblogs.com/luqman/p/18006810
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

用纹理增加细节

一.理解纹理

  OpenGL中的纹理可以用来表示照片,图像。每个二维的纹理都由许多小的纹理元素组成,他们是小块的数据,类似于我们前面讨论的片段和像素。要使用纹理,最直接的方式是从图像文件加载数据。我们现在要加载下面这副图像作为空气曲棍球桌子的表面纹理:

  

3219734-20240204185548002-1602132387.png

  我们将其存储在drawable文件夹中即可。每个纹理都有坐标空间,其范围是从一个拐角(0,0)到另一个拐角(1,1),我们想要把一个纹理应用到一个或多个三角形时,我们要为每个顶点指定一个纹理坐标,以便让OpenGL知道用纹理的哪个部分画到每个三角形上。按照惯例,一个二维的纹理一个维度称作S,另一个维度称作T。

二.把纹理加载进OpenGL中

  我们的第一个任务是将一副图像文件的数据加载到一个OpenGL的纹理中,我们将创建一个新的类TextureHelper,并在其中完成加载纹理的工作。在进行这个工作之前,我们先来了解一下纹理过滤,当纹理大小被放大或缩小时,我们要使用纹理过滤明确说明会发生什么。当我们在渲染表面绘制一个纹理时,那个纹理的纹理元素可能无法精确的映射到OpenGL生成的片段上,此时会出现两种情况,放大和缩小。当我们将几个纹理元素挤到一个片段时,缩小就发生了;当我们把一个纹理元素扩大到几个片段上时,放大就发生了。针对每种情况,我们都需要配置纹理过滤器。我们会通过glTexParameteri()函数设置纹理过滤模式,下面是OpenGL支持的纹理过滤模式:

3219734-20240204200419769-899467963.png

   并且放大和缩小两种情况下所允许的纹理过滤模式有所不同,如下所示:

3219734-20240204200624020-1667530310.png

   下面,是加载纹理的代码:

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,以便设置片段的颜色。

四.为顶点数据创建新的类结构

  首先,我们要把顶点数组分离到不同的类中,每个类代表一个物理对象的类型。我们为桌子创建一个新类,并为木槌创建另一个类。为了避免重复,我们会创建一个单独的类用于封装实际的顶点数组,新的类结构如下图所示:

3219734-20240204205515876-1314498624.png

  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)
    }
}

  接下来,我们会为纹理着色器程序创建一个类,为颜色着色器程序创建另一个类,我们会用纹理着色器绘制桌子,并用颜色着色器绘制木槌。我们也会创建一个基类作为他们的公共函数,我们不需要画中间那条线,因为那是纹理的一部分,类的继承结构如下:

3219734-20240204220714672-169350437.png

   我们先给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()

    }
}

  最后,运行程序,看看纹理是否绘制在球桌上了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK