学习内容来源and参考
opengl es 3.0编程指南
3D图形渲染最基本的操作之一是对一个表面进行纹理,纹理可以表现只从网格的几何形状中无法得到的附加细节。在opengl es3.0中的纹理有多种形式:2D纹理,2D纹理数组,3D纹理以及立方图纹理。
2D纹理
2D纹理是一个图像数据的二维数组。一个纹理单独数据元素称作“纹素”(Texel)。图像中的每个纹素根据基本格式和数据类型指定。如果用2D纹理渲染时,纹理坐标用作图像中的索引。2D纹理的纹理坐标用一对2D坐标(s,t)或者(u,v)来表示,这些坐标用于查找一个纹理贴图的规范化坐标。
纹理坐标如下所示:
纹理图像的左下坐标由(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为采用的过滤模式
过滤模式的采样过程如图所示:
GL_LINEAR
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中有三种模式可供使用:
- GL_REPEAT:对纹理的默认行为,重复纹理图像。
- GL_MIRRORED_REPEAT:和GL_REPEAT一样,但每次重复图片是镜像放置的。
- 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);
...
绘制出来图像如下所示:
附上次绘制立方体的纹理贴图效果:
代码地址:https://github.com/JerryChan123/LearnOEL/tree/gl30 提交信息为[add the texture for cube]
© 著作权归作者所有