百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 博客教程 > 正文

音视频开发之旅(12) OpenGL ES之纹理

connygpt 2024-11-18 10:42 16 浏览

目录

  1. 纹理相关的基本概念
  2. 纹理绘制的流程以及关键方法
  3. 实践(纹理加载、二分屏、三分屏、八分屏、镜像、纹理和颜色混合)
  4. 遇到的问题
  5. 收获

一、基本概念

纹理
纹理(Texture)是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;把它像贴纸一样贴在什么东西上面,让那个东西看起来像我们贴纸所要表现的东西那样。从而使图形更加真实

纹理坐标

OpenGL中纹理坐标系是以纹理左下角为坐标原点的,而图片中像素的存储顺序是从左上到右下的,因此我们需要对我们的坐标系进行一次Y轴的“翻转”。

图片坐标系的(0,0)在图片左上角,纹理坐标的(0,0)在纹理左下角

纹理映射

为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。

纹理单元

纹理单元是能够被着色器采样的纹理对象的引用, 纹理通过调用glBindTexture函数绑定到指定的纹理单元。没有明确指定使用哪个纹理单元时纹理被默认绑定到GL_TEXTURE0_

glActiveTexture:激活纹理单元

为什么sampler2D变量是个uniform,我们却不用glUniform给它赋值。使用glUniform1i?
使用glUniform1i,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理

纹理环绕方式

GL_REPEAT: 默认方案,重复纹理图片。
GL_MIRRORED_REPEAT:类似于默认方案,不过每次重复的时候进行镜像重复。
GL_CLAMP_TP_EDGE:将坐标限制在0到1之间。超出的坐标会重复绘制边缘的像素,变成一种扩展边缘的图案。(通常很难看)
GL_CLAMP_TO_BORDER:超出的坐标将会被绘制成用户指定的边界颜色。

纹理过滤

GL_NEAREST:最近点过滤:
纹理坐标最靠近哪个纹素,就用哪个纹素。这是OpenGL默认的过滤方式,速度最快,但是效果比较差。
GL_LINEAR:(双)线性过滤:
纹理坐标位置附近的几个纹素值进行某种插值计算之后的结果。这是应用最广泛的一种方式,效果一般,速度较快。

多级渐进纹理(多级渐远纹理)
mipmaps,就是一系列的纹理图片,每一张纹理图的大小都是前一张的1/4,直到剩最后一个像素为止

它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。

GL_NEAREST_MIPMAP_NEAREST:采用最近的mipmap图,在纹理采样的时候使用最近点过滤采样。
GL_LINEAR_MIPMAP_NEAREST:采用最近的mipmap图,纹理采样的时候使用线性过滤采样。
GL_NEAREST_MIPMAP_LINEAR:采用两张mipmap图的线性插值纹理图,纹理采样的时候采用最近点过滤采样。
GL_LINEAR_MIPMAP_LINEAR:采用两张mipmap图的线性插值纹理图,纹理采样的时候采用线性过滤采样。
生成mimap对应方法如下

二、纹理绘制流程和关键方法

 final int[] textureObjectIds = new int[1];
//初始化纹理
        glGenTextures(1, textureObjectIds, 0);

        if (textureObjectIds[0] == 0) {
            return 0;
        } 

        //获取纹理图片
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;

        final Bitmap bitmap = BitmapFactory.decodeResource(
            context.getResources(), resourceId, options);

        if (bitmap == null) {
            glDeleteTextures(1, textureObjectIds, 0);
            return 0;
        } 
        // 绑定纹理 2D纹理和纹理id
        glBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);

        //设置纹理环绕方式为 GL_REPEAT
   glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
        
      //设置纹理过滤 缩小和放大的filter
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        // 加载纹理图片
        texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);


        //生成多级渐变纹理
        glGenerateMipmap(GL_TEXTURE_2D);

        //回收bitmap
        bitmap.recycle();

        // 解绑纹理
        glBindTexture(GL_TEXTURE_2D, 0);

重要方法

glActiveTexture
glGenTextures
glBindTexture
glTexParameteri
glTexImage2D
glGenerateMipmap
glUniform1i

三、实践 :加载纹理 (纹理加载、二分屏、三分屏、八分屏、镜像、纹理和颜色混合)

我们通常需要使用一张JPG和PNG等格式的图片文件作为模型的纹理,而OpenGL中并没有提供相关API用于将这些图片文件转换成我们所需要的数组。在java层我们可以通过如下方法加载bitmap到纹理,我们用广州塔灯光节的一张图片作为纹理


final BitmapFactory.Options options = new BitmapFactory.Options();
      options.inScaled = false;

      // Read in the resource
      final Bitmap bitmap = BitmapFactory.decodeResource(
          context.getResources(), resourceId, options);

texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);

1. 首先来写顶点着色器和片元着色器glsl程序

//texture_vertex_shader.glsl

//顶点坐标
attribute vec4 a_Position;

//纹理坐标
attribute vec2 a_TextureCoordinates;

varying vec2 v_TextureCoordinates;


void main()                    
{                            
    v_TextureCoordinates = a_TextureCoordinates;

    gl_Position = a_Position;
}          
//texture_fragment_shader.glsl


precision mediump float; 

//纹理单元                          
uniform sampler2D u_TextureUnit;                
//纹理坐标          
varying vec2 v_TextureCoordinates;

  
void main()                         
{      
//通过texture2D方法,传入纹理单元和纹理坐标获取颜色                         
    gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates) ;
}

2. 然后,写纹理程序

//在Render的onSurfaceCreated中创建着色器程序
    
public class TextureShaderProgram extends ShaderProgram {

    private final int uTextureUnitLocation;
    
    // Attribute locations
    private final int aPositionLocation;
    private final int aTextureCoordinatesLocation;

    public TextureShaderProgram(Context context) {
         String vertexCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "texture_vertex_shader.glsl");
        String fragmentCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "texture_fragment_shader.glsl");
        //创建着色器程序
        programId = ShaderHelper.loadProgram(vertexCode, fragmentCode);

//纹理单元location
        uTextureUnitLocation = glGetUniformLocation(programId, U_TEXTURE_UNIT);
        
 //顶点坐标location
        aPositionLocation = glGetAttribLocation(programId, A_POSITION);

//纹理坐标location
        aTextureCoordinatesLocation = 
            glGetAttribLocation(program, A_TEXTURE_COORDINATES);


    }


    public int getPositionAttributeLocation() {
        return aPositionLocation;
    }

    public int getTextureCoordinatesAttributeLocation() {
        return aTextureCoordinatesLocation;
    }


}

3. 接着,生成顶点数据

public class GuangzhouTa {
    private static final int POSITION_COMPONENT_COUNT = 2;
    private static final int TEXTURE_COORDINATES_COMPONENT_COUNT = 2;
    private static final int STRIDE = (POSITION_COMPONENT_COUNT 
        + TEXTURE_COORDINATES_COMPONENT_COUNT) * BYTES_PER_FLOAT;
    

    private final VertexArray vertexArray;
    
    public GuangzhouTa(float[] vertexData) {
        vertexArray = new VertexArray(vertexData);
    }
    
//把顶点数据和顶点着色器的location绑定赋值
    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);
    }
    
    public void draw() {                                
        glDrawArrays(GL_TRIANGLE_FAN, 0, 6);
    }
}

4. 再 加载纹理获取到纹理id

public static int loadTexture(Context context, int resourceId) {
        final int[] textureObjectIds = new int[1];
//初始化纹理
        glGenTextures(1, textureObjectIds, 0);

        if (textureObjectIds[0] == 0) {
            return 0;
        } 

        //获取纹理图片
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;

        final Bitmap bitmap = BitmapFactory.decodeResource(
            context.getResources(), resourceId, options);

        if (bitmap == null) {
            glDeleteTextures(1, textureObjectIds, 0);
            return 0;
        } 
        // 绑定纹理 2D纹理和纹理id
        glBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);

        //设置纹理环绕方式为 GL_REPEAT
   glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
        
      //设置纹理过滤 缩小和放大的filter
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        // 加载纹理图片
        texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);


        //生成多级渐变纹理
        glGenerateMipmap(GL_TEXTURE_2D);

        //回收bitmap
        bitmap.recycle();

        // 解绑纹理
        glBindTexture(GL_TEXTURE_2D, 0);

        //范围纹理id
        return textureObjectIds[0];
    }

5. 最后在Render的onDrawFrame中进行绘制

    public class GuangZhouTaRenderer implements Renderer {
        private final Context context;
    
    
        private GuangzhouTa guangzhouta;
    
        private TextureShaderProgram textureProgram;
    
        private int textureId;
    
        public GuangZhouTaRenderer(Context context) {
            this.context = context;
        }
    
        @Override
        public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
            glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    
            guangzhouta = new GuangzhouTa(VertexDataUtils.VERTEX_DATA);
    
            textureProgram = new TextureShaderProgram(context);
    
            textureId = TextureHelper.loadTexture(context, R.drawable.guangzhou);
        }
    
        @Override
        public void onSurfaceChanged(GL10 glUnused, int width, int height) {
     
            glViewport(0, 0, width, height);
    
        }
    
        @Override
        public void onDrawFrame(GL10 glUnused) {
            
            glClear(GL_COLOR_BUFFER_BIT);
    
            textureProgram.useProgram();
            textureProgram.setUniforms(textureId);
            guangzhouta.bindData(textureProgram);
            guangzhouta.draw();
    
        }
    }
    
    
    public class TextureShaderProgram{
    ....
    public void setUniforms( int textureId) {
           //激活纹理单元0
            glActiveTexture(GL_TEXTURE0);
    
            // 绑定纹理id
            glBindTexture(GL_TEXTURE_2D, textureId);
    
           //使用纹理单元0
            glUniform1i(uTextureUnitLocation, 0);
        }
    ...
    }

我们上面使用的顶点数据矩阵是

    public static final float[] VERTEX_DATA = {
            // Order of coordinates: X, Y,  S, T
            // Triangle Fan
            0f,    0f,  0.5f, 0.5f,
            -1f, -1f,     0f, 1f,
            1f, -1f,   1f, 1f,
            1f,  1f,    1f, 0.0f,
            -1f,  1f,    0f, 0.0f,
            -1f, -1f,    0f, 1f };

效果如下


我们发现被拉伸了,为什么会被拉伸?
因为纹理原图是宽高比是1:1,但是手机屏幕的宽高比一般是9:16,在水平方向上9相当于1,在垂直方向上16/9就是图片被拉伸的倍数。那么该如何处理
?矩阵的数据纹理坐标的S和T的根据实际屏幕宽高比进行计算。

简单把矩阵在T坐标上放大一倍

    public static final float[] SPLIT_SCREEN_2_VERTEX_DATA = {
            // Order of coordinates: X, Y,  S, T
            // Triangle Fan
            0f,    0f,  0.5f, 1f,
            -1f, -1f,   0f, 2f,
            1f, -1f,     1f, 2f,
            1f,  1f,    1f, 0.0f,
            -1f,  1f,   0f, 0.0f,
            -1f, -1f,    0f, 2f };

效果如下(即2分屏的效果)


还记得我们 上面设置的纹理环绕方式为 GL_REPEAT,纹理坐标限制在0到1之间。超出的坐标会重复绘制

   glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);

镜像重复的效果

 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_MIRRORED_REPEAT);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_MIRRORED_REPEAT);

设置为边缘扩展效果如下(的确很难看)

       glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);

三分屏和八分屏也是类似,只需要修改矩阵即可
修改及效果如下

   public static final float[] SPLIT_SCREEN_3_VERTEX_DATA = {
        
            0f,    0f,  0.5f, 1.5f,
            -1f, -1f,   0f, 3f,
            1f, -1f,   1f, 3f,
            1f,  1f,    1f, 0.0f,
            -1f,  1f,    0f, 0.0f,
            -1f, -1f,    0f, 3f };
    public static final float[] SPLIT_SCREEN_8_VERTEX_DATA = {
            // Order of coordinates: X, Y, S, T
            // Triangle Fan
            0f,    0f,  1f, 2f,
            -1f, -1f,    0f, 4f,
            1f, -1f,   2f, 4f,
            1f,  1f,   2f, 0.0f,
            -1f,  1f,    0f, 0.0f,
            -1f, -1f,    0f, 4f };

纹理与颜色混合

上面的分屏、镜像等都是直接针对纹理图片改变顶点着色器的S和T坐标实现。如果想在上面的结果上再和颜色混合该如何做?先上结果


还是和上面一样的流程
首先修改着色器,顶点着色器添加color的attribute和varying,然后片元着色器生成gl_FragColor时,乘以颜色的向量_
然后在GLprograme中拿到color的loaction,顶点着色器的矩阵数据添加rgb值,

attribute vec4 a_Position;
attribute vec3 a_Color;
attribute vec2 a_TextureCoordinates;

varying vec2 v_TextureCoordinates;
varying vec3 v_Color;


void main()                    
{                            
    v_TextureCoordinates = a_TextureCoordinates;
    v_Color = a_Color;

    gl_Position = a_Position;
}  

precision mediump float; 
                        
uniform sampler2D u_TextureUnit;                                        
varying vec2 v_TextureCoordinates;
varying vec3 v_Color;
  
void main()                         
{                               
    gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates) * vec4(v_Color,1.0f);
}
    public static final float[] SPLIT_SCREEN_2_VERTEX_DATA = {
            // Order of coordinates: X, Y, R, G, B, S, T
            // Triangle Fan
            0f,    0f, 1.0f,0.0f,0.0f,  0.5f, 1f,
            -1f, -1f,   1.0f,1.0f,0.0f,  0f, 2f,
            1f, -1f,   0.0f,0.0f,1.0f,  1f, 2f,
            1f,  1f,   0.0f,0.0f,0.0f,  1f, 0.0f,
            -1f,  1f,   1.0f,0.0f,0.0f,  0f, 0.0f,
            -1f, -1f,  1.0f,1.0f,0.0f,   0f, 2f };
public class GuangzhouTa {
    private static final int POSITION_COMPONENT_COUNT = 2;
    private static final int COLOR_COMPONENT_COUNT = 3;
    private static final int TEXTURE_COORDINATES_COMPONENT_COUNT = 2;
    private static final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT
        + TEXTURE_COORDINATES_COMPONENT_COUNT) * BYTES_PER_FLOAT;
    

    private final VertexArray vertexArray;
    
    public GuangzhouTa(float[] vertexData) {
        vertexArray = new VertexArray(vertexData);
    }
    
    public void bindData(TextureShaderProgram textureProgram) {
        vertexArray.setVertexAttribPointer(
            0, 
            textureProgram.getPositionAttributeLocation(), 
            POSITION_COMPONENT_COUNT,
            STRIDE);

        vertexArray.setVertexAttribPointer(
                POSITION_COMPONENT_COUNT,
                textureProgram.getColorAttributeLocation(),
                COLOR_COMPONENT_COUNT,
                STRIDE);

        vertexArray.setVertexAttribPointer(
            POSITION_COMPONENT_COUNT+COLOR_COMPONENT_COUNT,
            textureProgram.getTextureCoordinatesAttributeLocation(),
            TEXTURE_COORDINATES_COMPONENT_COUNT, 
            STRIDE);
    }
    
    public void draw() {                                
        glDrawArrays(GL_TRIANGLE_FAN, 0, 6);
    }
}

四、资料

《OpenGL ES 3.0 编程指南》
《OpenGL编程指南》(红宝书)
《OpenGL ES应用开发实践指南》

[OpenGL入门第七课--纹理]
[Android OpenGL ES 2.0绘图:绘制纹理]
[从0开始的OpenGL学习(五)-纹理]
[OpenGL纹理详解(上)]
[OpenGL纹理详解(下)实践篇]
[OpenGL(十二) 纹理映射(贴图)]
[OpenGL纹理显示]
[纹理]

五、收获

  1. 了解纹理坐标、纹理单元、纹理环绕、纹理过滤、mipmap等概念
  2. 了解纹理加载流程以及重要API分析
  3. 通过实践加载纹理,熟悉概念和流程
  4. 纹理倒立等问题分析解决

感谢你的阅读

下一篇我们一起来学习实践相机预览时添加滤镜效果,欢迎关注公众号“音视频开发之旅”,一起学习成长。

欢迎交流

相关推荐

自学Python,写一个挨打的游戏代码来初识While循环

自学Python的第11天。旋转~跳跃~,我~闭着眼!学完循环,沐浴着while的光芒,闲来无事和同事一起扯皮,我说:“编程语言好神奇,一个小小的循环,竟然在生活中也可以找到原理和例子”,同事也...

常用的 Python 工具与资源,你知道几个?

最近几年你会发现,越来越多的人开始学习Python,工欲善其事必先利其器,今天纬软小编就跟大家分享一些常用的Python工具与资源,记得收藏哦!不然下次就找不到我了。1、PycharmPychar...

一张思维导图概括Python的基本语法, 一周的学习成果都在里面了

一周总结不知不觉已经自学Python一周的时间了,这一周,从认识Python到安装Python,再到基本语法和基本数据类型,对于小白的我来说无比艰辛的,充满坎坷。最主要的是每天学习时间有限。只...

三日速成python?打工人,小心钱包,别当韭菜

随着人工智能的热度越来越高,许多非计算机专业的同学们也都纷纷投入到学习编程的道路上来。而Python,作为一种相对比较容易上手的语言,也越来越受欢迎。网络上各类网课层出不穷,各式广告令人眼花缭乱。某些...

Python自动化软件测试怎么学?路线和方法都在这里了

Python自动化测试是指使用Python编程语言和相关工具,对软件系统进行自动化测试的过程。学习Python自动化测试需要掌握以下技术:Python编程语言:学习Python自动化测试需要先掌握Py...

Python从放弃到入门:公众号历史文章爬取为例谈快速学习技能

这篇文章不谈江流所专研的营销与运营,而聊一聊技能学习之路,聊一聊Python这门最简单的编程语言该如何学习,我完成的第一个Python项目,将任意公众号的所有历史文章导出成PDF电子书。或许我这个Py...

【黑客必会】python学习计划

阅读Python文档从Python官方网站上下载并阅读Python最新版本的文档(中文版),这是学习Python的最好方式。对于每个新概念和想法,请尝试运行一些代码片段,并检查生成的输出。这将帮助您更...

公布了!2025CDA考试安排

CDA数据分析师报考流程数据分析师是指在不同行业中专门从事行业数据搜集、整理、分析依据数据作出行业研究评估的专业人员CDA证书分为1-3级,中英文双证就业面广,含金量高!!?报考条件:满18...

一文搞懂全排列、组合、子集问题(经典回溯递归)

原创公众号:【bigsai】头条号:程序员bigsai前言Hello,大家好,我是bigsai,longtimenosee!在刷题和面试过程中,我们经常遇到一些排列组合类的问题,而全排列、组合...

「西法带你学算法」一次搞定前缀和

我花了几天时间,从力扣中精选了五道相同思想的题目,来帮助大家解套,如果觉得文章对你有用,记得点赞分享,让我看到你的认可,有动力继续做下去。467.环绕字符串中唯一的子字符串[1](中等)795.区...

平均数的5种方法,你用过几种方法?

平均数,看似很简单的东西,其实里面包含着很多学问。今天,分享5种经常会用到的平均数方法。1.算术平均法用到最多的莫过于算术平均法,考试平均分、平均工资等等,都是用到这个。=AVERAGE(B2:B11...

【干货收藏】如何最简单、通俗地理解决策树分类算法?

决策树(Decisiontree)是基于已知各种情况(特征取值)的基础上,通过构建树型决策结构来进行分析的一种方式,是常用的有监督的分类算法。决策树算法是机器学习中的一种经典算法,它通过一系列的规则...

面试必备:回溯算法详解

我们刷leetcode的时候,经常会遇到回溯算法类型题目。回溯算法是五大基本算法之一,一般大厂也喜欢问。今天跟大家一起来学习回溯算法的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~什么是回溯...

「机器学习」决策树——ID3、C4.5、CART(非常详细)

决策树是一个非常常见并且优秀的机器学习算法,它易于理解、可解释性强,其可作为分类算法,也可用于回归模型。本文将分三篇介绍决策树,第一篇介绍基本树(包括ID3、C4.5、CART),第二篇介绍Ran...

大话AI算法: 决策树

所谓的决策树算法,通俗的说就是建立一个树形的结构,通过这个结构去一层一层的筛选判断问题是否好坏的算法。比如判断一个西瓜是否好瓜,有20条西瓜的样本提供给你,让你根据这20条(通过机器学习)建立起...