OpenGL 学习系列---纹理

原文链接:https://glumes.com/post/opengl/opengl-tutorial-texture/

接下来探索纹理了。

纹理,简单的理解就是一副图像。而把一副图像映射到图形上的过程,叫做纹理映射

比如有如下图形和三角形,想要把图形中的一部分映射到三角形上。

image
image

结果就是这样的:

image

这就是纹理映射的一个小小例子。

基本原理

要注意到,OpenGL 绘制的物体是 3D 的,而纹理是 2D 的,那么纹理映射就是将 2D 的纹理映射到 3D 的物体上,可以想象成用一张纸裹着一个物体一样,不过要按照一定规律来。

OpenGL 中绘制的物体是有坐标系的,每个点都对应 x、y、z 坐标,而纹理也有着它的坐标,只要 3D 物体中的每个点都对应了 2D 纹理中的某个点,那么就可以把纹理映射到 3D 物体上去了。

纹理的坐标,叫做纹理坐标系。它的范围只有 [0,0][1,1]

image

它的坐标原点位于左下角,水平向右为 S 轴,竖直向上为 Y 轴。不论实际的纹理图片尺寸大小如何,横向、纵向坐标最大值都是 1 。

例如:实际图为 512 x 256 像素分辨率,则横向第 512 个像素对应纹理坐标为 1 ,纵向第 256 个像素对应纹理坐标为 1 。不过,纹理图最好是采用像素为 2 的 n 次方的纹理图。

纹理映射的基本思想就是:首先为图元中的每个顶点指定恰当的纹理坐标,然后通过纹理坐标在纹理图中可以确定选中的纹理区域,最后将选中纹理区域中的内容根据纹理坐标映射到指定的图元上。

纹理映射在 OpenGL 的渲染管线上的体现:在渲染管线中,先进行顶点着色器,绘制出物体的大致形状,之后会进行光栅化,将物体光栅化为许多片段组成,然后再进行片段着色器,将图形的每个片段进行着色。

那么就需要在 顶点着色器 中将纹理的坐标传入,在光栅化阶段,纹理坐标将根据 顶点着色器 对它的处理以及 片段和各顶点的位置关系 插值产生,然后才是将插值计算后的结果传入到片段着色器中。

着色器操作

相比直接绘制图形,使用纹理后,着色器也要改变了。

顶点着色器:

attribute vec4 a_Position;
attribute vec2 a_TextureCoordinates;
varying vec2 v_TextureCoordinates;
uniform mat4 u_ModelMatrix;
uniform mat4 u_ViewMatrix;
uniform mat4 u_ProjectionMatrix;
uniform mat4 u_Matrix;

void main() {
    v_TextureCoordinates = a_TextureCoordinates ;
    gl_Position = u_ProjectionMatrix * u_ViewMatrix * u_ModelMatrix * a_Position;
}

在顶点着色器中多了 v_TextureCoordinates 变量,它是 varying 类型,意思为可变类型,在光栅化处理时会对该变量进行处理,随后传入到片段着色器中。

片段着色器

precision mediump float;
uniform sampler2D u_TextureUnit;
varying vec2 v_TextureCoordinates;

void main(){
    // 未使用纹理的颜色赋值 : gl_FragColor = u_Color;
    gl_FragColor = texture2D(u_TextureUnit,v_TextureCoordinates);
}

v_TextureCoordinates1变量就是接受来自顶点着色器传的值,u_TextureUnit变量就是使用的采样器,类型是sampler2D

使用纹理后的片段着色器要使用 texture2D 函数给颜色赋值。

texture2D函数的作用就是采样,从纹理中采取像素赋值给 gl_FragColor变量,也就是最后的颜色。

上层代码

大致了解了着色器代码,接着就是上层的 Java 代码了。

和要创建一个 OpenGL ProgramId 类似,使用纹理也需要创建一个纹理 ID。

     /**
     * 返回加载图像后的 OpenGl 纹理的 ID
     * @param context
     * @param resourceId
     * @return
     */
    public static int loadTexture(Context context, int resourceId) {
        final int[] textureObjectIds = new int[1];
        glGenTextures(1, textureObjectIds, 0);
        if (textureObjectIds[0] == 0) {
            Timber.d("Could not generate a new OpenGL texture object.");
            return 0;
        }
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;
        final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);

        if (bitmap == null) {
            Timber.d("resource Id could not be decoded");
            glDeleteTextures(1, textureObjectIds, 0);
            return 0;
        }

        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,读入 Bitmap 定义的位图数据,并把它复制到当前绑定的纹理对象
        // 当前绑定的纹理对象就会被附加上纹理图像。
        texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);

        bitmap.recycle();
        
        // 为当前绑定的纹理自动生成所有需要的多级渐远纹理
        // 生成 MIP 贴图
        glGenerateMipmap(GL_TEXTURE_2D);

        // 解除与纹理的绑定,避免用其他的纹理方法意外地改变这个纹理
        glBindTexture(GL_TEXTURE_2D, 0);

        return textureObjectIds[0];
    }
  1. 首先使用 glGenTextures 创建纹理 ID。
  2. 如果创建失败,则使用 glDeleteTextures 删除并退出。
  3. 创建成功之后,使用 glBindTexture 函数将纹理 ID 和纹理目标绑定。
  4. 之后会设置纹理在缩小和放大情况下的过滤方式。
  5. 再使用 texImage2D 将纹理目标和 Bitmap 图片绑定。
  6. 使用 glGenerateMipmap 函数生成多级渐远纹理和 MIP 纹理贴图。
  7. 再使用 glBindTexture函数解除绑定。

glBindTexture 函数

这里要重点说一下 glBindTexture 函数。

它的作用是绑定纹理名到指定的当前活动纹理单元,当一个纹理绑定到一个目标时,目标纹理单元先前绑定的纹理对象将被自动断开。纹理目标默认绑定的是 0 ,所以要断开时,也再将纹理目标绑定到 0 就好了。

所以在代码的最后调用了 glBindTexture(GL_TEXTURE_2D, 0) 来解除绑定。

当一个纹理被绑定时,在绑定的目标上的 OpenGL 操作将作用到绑定的纹理上,并且,对绑定的目标的查询也将返回其上绑定的纹理的状态。

也就是说,这个纹理目标成为了被绑定到它上面的纹理的别名,而纹理名称为 0 则会引用到它的默认纹理。所以,当后续对纹理目标调用 glTexParameteri 函数设置过滤方式,其实也是对纹理设置的过滤方式。

绑定纹理中的值

创建并且设置了纹理着色器ID之后,就需要绑定并设置在着色器语言中的变量了。

        // 绑定着色器脚本中的变量
        uTextureUnitAttr = glGetUniformLocation(mProgram, U_TEXTURE_UNIT)
        mTextureId = TextureHelper.loadTexture(mContext,R.drawable.texture)
        // 激活纹理单元
        glActiveTexture(GL_TEXTURE0)
        // 绑定纹理目标
        glBindTexture(GL_TEXTURE_2D, mTextureId)
        // 给片段着色器中的采样器变量 sample2D 赋值
        glUniform1i(uTextureUnitAttr, 0)

在着色器脚本中定义了 uniform 类型的采样器变量 sampler2D,在上层的应用代码需要将它绑定并赋值。而 varying 类型的变量由顶点着色器传过去,不需要另外赋值了。

接下来要使用 glActiveTexture 函数激活纹理单元。在一个系统中,纹理单元的数据是有限的,在源码中从 GL_TEXTURE0 到 GL_TEXTURE31 共定义了三十二个纹理单元,但具体数量根据机型而定。

通过 GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS 常量可以查询到。

   var intBuffer:IntBuffer = IntBuffer.allocate(1)
   glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,intBuffer)
   LogUtil.d("max combined texture image units " + intBuffer[0])

激活了纹理单元,还需要再绑定纹理目标。

一个纹理单元包含了多个类型纹理目标,如:GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP 等等。

因为纹理单元是纹理的一个别名,所以对纹理单元所做的操作,都相当于对纹理操作的。把一些对纹理所做的操作提取到函数里,最后再加载纹理,并绑定到纹理目标上。

使用glUniform1i函数为采样器进行赋值为 0 ,这是和激活纹理单元相对应的。因为激活的纹理单元为 0 ,所以赋值也是为 0 。如果这里不一致,直接就看不到任何东西了。

实际效果

当绑定并设置好片段着色器中的值之后,接下来的流程就和绘制基本图形一样了。

image

具体的绘制操作都在片段着色器里面定义了,而在上层代码中就不用花费很多心思了,在顶点着色器不变的情况下,甚至可以只改变片段着色器的值来绘制不同的纹理效果。

总结 & 名词混淆点

在上面既是纹理单元又是纹理目标的很容易搞混,梳理一下概念:

形如 GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2 的就是纹理单元,一台机子上纹理单元数量是有限的,依具体机型而定。而 glActiveTexture 则是激活具体的纹理单元。

一个纹理单元又包含多个类型的纹理目标,有:GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP 等等。

通过 glGenTextures 函数生成的 int 类型的值就是纹理,通过 glBindTexture 函数将纹理目标和纹理绑定后,对纹理目标所进行的操作都反映到对纹理上。

纹理目标需要通过 texImage2D 函数附加上 Bitmap 位图。

参考

  1. http://blog.csdn.net/opengl_es/article/details/19852277
  2. http://blog.csdn.net/artisans/article/details/76695614

一起交流学习,答疑解惑,有问题,我们星球见~~~


图形/图像/音视频交流

具体代码详情,可以参考我的 Github 项目:

https://github.com/glumes/AndroidOpenGLTutorial

OpenGL 系列文章:

  1. OpenGL 系列---基础绘制流程

  2. OpenGL 学习系列---基本形状的绘制

  3. OpenGL 学习系列---坐标系统

  4. OpenGL 学习系列---投影矩阵

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容