opengl es3.0学习篇八:纹理

时间:2024-04-04 11:31:59

OpenGL ESMIP

开发十年,就只剩下这套架构体系了! >>>   opengl es3.0学习篇八:纹理

学习内容来源and参考

opengl es 3.0编程指南

https://www.jianshu.com/p/4d8d35288a0f

3D图形渲染最基本的操作之一是对一个表面进行纹理,纹理可以表现只从网格的几何形状中无法得到的附加细节。在opengl es3.0中的纹理有多种形式:2D纹理,2D纹理数组,3D纹理以及立方图纹理。

2D纹理

2D纹理是一个图像数据的二维数组。一个纹理单独数据元素称作“纹素”(Texel)。图像中的每个纹素根据基本格式和数据类型指定。如果用2D纹理渲染时,纹理坐标用作图像中的索引。2D纹理的纹理坐标用一对2D坐标(s,t)或者(u,v)来表示,这些坐标用于查找一个纹理贴图的规范化坐标。

纹理坐标如下所示:

opengl es3.0学习篇八:纹理

纹理图像的左下坐标由(0.0,0.0)决定,右上角坐标由(1.0,1.0)指定。在[0.0,1.0]之外的坐标是允许的,在区间之外的纹理读取行为由纹理包装模式决定。

立方图纹理

立方图是由6个单独2D纹理面组成的纹理。对于立方图纹理贴图,一般使用环境贴图特效,即在物体上的倒影通过使用一个表示环境的立方图渲染。。通常,生成环境贴图所用的立方图通过在场景*放置一个摄像机,从6个轴方向(+x,-x,+y,-y,+z,-z)捕捉场景图像并将结果保存在立方体的每个面上。

3D纹理

3D纹理可以看做2D纹理多个切片的一个数组,用一个三元坐标(s,t,r)访问,r坐标选择3D纹理需要采样的切片,(s,t)用来读取每个切片中的2D贴图。

2D纹理数组

2D纹理数组与3D纹理数组相似,但是用途不同,一般用来存储2D图像的动画。数组的每个切片标识纹理动画的一帧。坐标使用与3D纹理相同,都是使用(s,t,r)来表示,r坐标选择2D数组中需要采样的切片,(s,t)用来选取切片。

纹理对象和纹理加载

纹理对象是一个容器对象,保存所需要渲染的纹理数据,如图像数据,过滤模式以及包装模式。纹理对象使用一个无符号整数表示,该整数位纹理对象的句柄,生成纹理对象的函数如下:

void glGenTextures(GLsizei n,GLuint *textures);//native 实现

 // C function void glGenTextures ( GLsizei n, GLuint *textures )  java层代码
public static native void glGenTextures(
        int n,
        java.nio.IntBuffer textures
    );

Java代码中n表示生成纹理对象数量,textures为一个保存n个纹理对象id的无符号整数数组。创建时,glGenTextures方法生成的纹理对象是一个空的容器,用于加载纹理数据和参数。纹理对象在不使用时候可以通过调用glDeleteTextures(...)方法来删除。

一旦使用glGenTextures(...)来生成纹理对象id,应用程序就必须通过glBindTexture()绑定纹理对象进行操作。一旦绑定到一个特定的目标,纹理对象会一直绑定在此目标中直到删掉为止。在绑定完成后,需要去加载图像了,用于加载立方图纹理和2D的基本函数是glTexImage2D()。在opengl es3.0中可以使用多种代替方法指定2D纹理,包括不可变纹理(glTexStorage2D)以及glTexSubImage2D的结合。为了得到最佳性能,推荐使用不可变纹理。在Android中,系统帮我们封装了GLUtils来方便我们使用:

public static void texImage2D(int target, int level, Bitmap bitmap,
            int border);
//target为将纹理对象绑定到GL_TEXTURE_2D,GL_TEXTURE_3D,GL_TEXTURE_2D_ARRAY,GL_TEXTURE_CUBE_MAP的标识
//level为指定要加载mip的级别,第一个级别为0,后续的递增
//bitmap为需要加载的图像
//border在opengl es中忽略,传0即可

上述方法使用如下:


private void generateTexture(Bitmap bitmap) {
        int[] size = new int[1];
        GLES30.glGenTextures(size.length, size, 0);
        if (size[0] == 0) {
            Log.w(TAG, "创建纹理失败!");
            return;
        }
        int target = size[0];
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, target);
        GLUtils.texImage2D(target, 0, bitmap, 0);
		GLES30.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        GLES30.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
    } 

上述的glTexParameteri(..)方法将缩小和放大过滤模式设置为GL_NEAREST。这个对于纹理贴图来说是必须的,因为我们还没有为纹理加载完整的mip贴图链,因此,必须选择非mip贴图缩小过滤器。其他的模式还有GL_LINEAR,提供双线性非mip贴图过滤。

纹理过滤和mip贴图

纹理坐标用于生成一个2d索引,用来从纹理贴图中读取,当缩小和放大的过滤器设置为GL_NEAREST的时候,一个纹素将在提供的纹理坐标位置上读取,这个称为点采样或者最近采样。but,使用该方法采样可能会造成严重的视觉伪像,因为三角形在屏幕空间中变得较小,在不同像素间的差值,纹理坐标可能有很大的跳跃,从而造成从一个大的纹理涂重取得少量样本,造成锯齿伪像,并且可能造成巨大的性能损失。

上述的解决办法可以通过mip贴图来解决伪像的问题。其思路是构建一个mip贴图链,开始于原来指定的图像,后续的每个图像在每个维度上是前一个图像的一般,一直持续到最后到达链底部的1x1纹理。mip贴图级别可以变成生成(上述的level参数),一个mip级别中的每个像素通常根据上一级别中相同位置的4个像素的平均值计算。

纹理渲染时会发生两种过滤情况:缩小和放大。

  • 缩小的情况发生在屏幕投影的多边形小于纹理尺寸的时候。
  • 放大的情况发生在屏幕投影的多边形大于纹理尺寸的时候。

对于放大而言,mip贴图是不起作用的,因为我们总是从最大的可用级别进行采样。对于缩小来说,可以使用不同的采样方式。 过滤模式使用glTexParameteri(...)进行设置:

   public static native void glTexParameteri(
        int target,
        int pname,
        int param
    );
//纹理对象,GL_TEXTURE_2D,GL_TEXTURE_3D,GL_TEXTURE_2D_ARRAY,GL_TEXTURE_CUBE_MAP
//pname一般指定为GL_TEXTURE_MIN_FILTER(缩小),GL_TEXTURE_MAG_FILTER(放大)
//param为采用的过滤模式

过滤模式的采样过程如图所示:

opengl es3.0学习篇八:纹理

GL_LINEAR

opengl es3.0学习篇八:纹理

GL_NEAREST

  • GL_LINEAR:从最靠近纹理坐标中的纹理中获得一个双线性样本
  • GL_NEAREST:从最靠近纹理坐标中的纹理中获得一个单点样本

更加详细介绍关于过滤模式的区别可以查看这篇博客

自动mip贴图生成

opengls es3.0中提供glGenerateMipmap()方法来自动生成mip贴图。


 public static native void glGenerateMipmap(
        int target
    );
//之前生成的纹理对象,GL_TEXTURE_2D,GL_TEXTURE_3D,GL_TEXTURE_2D_ARRAY,GL_TEXTURE_CUBE_MAP

纹理包装模式

纹理包装模式用于指定纹理坐标超出[0.0,1.0]方位内所发生的行为。使用glTexParameter[i|f]v来进行设置:


    public static native void glTexParameterfv(
        int target,
        int pname,
        java.nio.FloatBuffer params
    );

    // C function void glTexParameteri ( GLenum target, GLenum pname, GLint param )

    public static native void glTexParameteri(
        int target,
        int pname,
        int param
    );

这些模式可以为s,t,r坐标进行单独设置,GL_TEXTURE_WRAP_S设置s坐标的模式,GL_TEXTURE_WRAP_T设置t坐标的模式,GL_TEXTURE_WRAP_R设置r坐标的模式。

在opengl es中有三种模式可供使用:

  1. GL_REPEAT:对纹理的默认行为,重复纹理图像。
  2. GL_MIRRORED_REPEAT:和GL_REPEAT一样,但每次重复图片是镜像放置的。
  3. GL_CLAMP_TO_EDGE:纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。

代码demo

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

着色器中使用纹理

这里的demo不考虑图像拉伸的问题,旨在理清展示的逻辑

简单使用2D纹理展示一下效果,首先封装一个展示纹理的关键方法:

代码位于 https://github.com/JerryChan123/LearnOEL/tree/gl30 的[2D纹理普通贴图&&2D纹理立方体贴图]提交当中

   private Bitmap mBitmap;

    public int loadTexture(Context context, int resId) {
        int[] textureObjIds = new int[1];
        GLES30.glGenTextures(1, textureObjIds, 0);
        if (textureObjIds[0] == 0) {
            return 0;
        }
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;
        if (mBitmap == null) {
            mBitmap = BitmapFactory.decodeResource(context.getResources(), resId, options);
            if (mBitmap == null) {
                GLES30.glDeleteTextures(1, textureObjIds, 0);
                return 0;
            }
        }
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureObjIds[0]);//bind
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
        GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, mBitmap, 0);
        GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D);
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);//unbind
        return textureObjIds[0];
    }

在SurfaceView的onDraw()方法中进行绘制:

...
 int texCoord = GLES30.glGetAttribLocation(mProgram, "texCoord");
 GLES30.glEnableVertexAttribArray(texCoord);
 GLES30.glVertexAttribPointer(texCoord, 2, GLES30.GL_FLOAT, false, 0, textBuffer);
 int textureId = loadTexture(mContext, R.drawable.aa);
 GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId);
 int uTextureUnitLocation = GLES30.glGetUniformLocation(mProgram, "s_texture");
 GLES30.glUniform1i(uTextureUnitLocation, 0);
...

绘制出来图像如下所示:

opengl es3.0学习篇八:纹理

附上次绘制立方体的纹理贴图效果:

代码地址:https://github.com/JerryChan123/LearnOEL/tree/gl30 提交信息为[add the texture for cube]

opengl es3.0学习篇八:纹理

© 著作权归作者所有