zoukankan      html  css  js  c++  java
  • OpenGL ES学习笔记(三)——纹理

    首先申明下,本文为笔者学习《OpenGL ES应用开发实践指南(Android卷)》的笔记,涉及的代码均出自原书,如有需要,请到原书指定源码地址下载。

          《OpenGL ES学习笔记(二)——平滑着色、自适应宽高及三维图像生成》中阐述的平滑着色、自适应宽高是为了实现在移动端模拟真实场景采用的方法,并且通过w分量增加了三维视角,在具体实现上采用了正交投影、透视投影的理论。本文将在此基础上,构建更加精美的三维场景。三维效果本质上是点、直线和三角形的组合,纹理是将图像或者照片覆盖到物体表面,形成精美的细节。在实现上具体分为两步:1)将纹理图片加载进OpenGL;2)OpenGL将其显示到物体表面。(有点像把大象装进冰箱分几步~~~)不过,在实现过程中,涉及到着色器程序的管理,涉及到不同的纹理过滤模式,涉及到顶点数据新的类结构等问题,下面将一一对其阐述:

    1. 纹理加载
    2. 纹理着色器
    3. 更新顶点数据类结构
    4. 着色器程序类
    5. 纹理绘制


    一、纹理加载

          将纹理覆盖到物体表面,最终是通对齐坐标来实现的。而OpenGL中二维纹理的坐标与计算机图像的坐标并不一致,因此,首先对比下两者的不同。

    clip_image001

          可见,两者的差别在于绕横轴翻转180度。另外,OpenGL ES支持的纹理不必是正方形,但每个维度都必须是2的幂。

          加载纹理图片的方法参数列表应该包括Android上下文(Context)和资源ID,返回值应该是OpenGL纹理的ID,因此,该方法申明如下:

    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纹理过滤模式如下表:(--内容来自原书)

    GL_NEAREST

    最近邻过滤

    GL_NEAREST_MIPMAP_NEAREST

    使用MIP贴图的最近邻过滤

    GL_NEAREST_MIPMAP_LINEAR

    使用MIP贴图级别之间插值的最近邻过滤

    GL_LINEAR

    双线性过滤

    GL_LINEAR_MIPMAP_NEAREST

    使用MIP贴图的双线性过滤

    GL_LINEAR_MIPMAP_LINEAR

    三线性过滤(使用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(
            0, 
            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)纹理不能直接传递,需要被绑定到纹理单元,然后将纹理单元传递给着色器;

  • 相关阅读:
    关于Python3中venv虚拟环境
    Python爬虫番外篇之关于登录
    站在圈外看待小米公司发展史
    Python爬虫从入门到放弃(十九)之 Scrapy爬取所有知乎用户信息(下)
    Python爬虫从入门到放弃(十八)之 Scrapy爬取所有知乎用户信息(上)
    Python爬虫番外篇之Cookie和Session
    Python爬虫从入门到放弃(十七)之 Scrapy框架中Download Middleware用法
    Python爬虫从入门到放弃(十六)之 Scrapy框架中Item Pipeline用法
    openstack的网络、子网、端口的关系
    openstack之安全组管理
  • 原文地址:https://www.cnblogs.com/younghao/p/5141290.html
Copyright © 2011-2022 走看看