OpenGL ES学习之纹理贴图
基本原理
启用纹理映射功能后,如果想把一副纹理应用到相应的几何图元,就必须告知渲染系统如何进行纹理的映射。告知的方式就是为图元汇总的顶点指定恰当的纹理坐标,纹理坐标用浮点数来表示。范围一般是0.0到1.0:
左侧是一副纹理图,其位于纹理坐标当中。纹理坐标系原点在左上侧,向右为S轴,向下为T轴,两个周的取值范围都是0.0到1.0.也就是说不论实际纹理图的尺寸如何,其横向,纵向坐标最大值都是1.
右侧是一个三角形图元,其三个顶点A,B,C都指定了纹理坐标,3组纹理坐标正好在右侧的纹理图中确定了需要映射的三角形纹理区域。
所以:纹理映射的基本思想:首先为图元中的每个顶点指定恰当的纹理坐标,然后通过纹理坐标在纹理图中可以确定选中的纹理区域,最后将选中纹理区域中的内容根据纹理坐标映射到指定的图元上。
我们知道,最终用户看到的是显示在屏幕上的像素,而像素是有片元产生的。因此,进行纹理映射的过程实际上就是为右侧三角形图元中的每个片元着色,用于着色的颜色需要从左侧的纹理图中提取,具体过程:
- 首先图元中的每个顶点都需要在顶点着色器中通过易变变量将纹理坐标传入片元着色器。
- 经过顶点着色器后渲染管线的固定功能部分会根据情况进行插值计算,产生对应到每个片元的用于记录纹理坐标的易变变量
- 最后每个片元在片元着色器中根据其接收到的记录纹理坐标的易变变量值到纹理图中提取出对应位置的颜色,提取颜色的过程一般称之为纹理采样。
首先看一个小例子,效果图如下:
public class TriangleTexture {
private String mVertexShader=
"attribute vec3 aPosition;" + //顶点位置
"attribute vec2 aTexCoord;" + //顶点纹理位置
"varying vec2 vTextureCoord;" + //用于传递给片元着色器的易变变量
"void main(){" +
"gl_Position = vec4(aPosition,1);" +
"vTextureCoord = aTexCoord;" +
"}";
private String mFragmentShader =
"precision mediump float;" +
"varying vec2 vTextureCoord;" +
"uniform sampler2D sTexture;" + //纹理采样器,代表一副纹理
"void main(){" +
"gl_FragColor = texture2D(sTexture,vTextureCoord);" +//进行纹理采样
"}";
private FloatBuffer mVertexBuffer;//顶点坐标数据缓冲
private FloatBuffer mTexCoordBuffer;//顶点纹理坐标数据缓冲
private int mvCount = 0;//顶点数量
private int maPositionHandle;
private int maTexCoordHandle;
private int mProgram;
public TriangleTexture(){
initVertext();
initShder();
}
private void initVertext(){
mvCount = 3;
mvCount = 3;
float vertices[] = new float[]{
0,1,0,-1,0,0,1,0,0
};//顶点位置
ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
vbb.order(ByteOrder.nativeOrder());//设置字节顺序为本地操作系统顺序
mVertexBuffer = vbb.asFloatBuffer();
mVertexBuffer.put(vertices);//在缓冲区内写入数据
mVertexBuffer.position(0);//设置缓冲区的起始位置
float[] colors = new float[]{
0.5f,0,
0,1,
1,1
};//顶点颜色数组
ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length*4);
cbb.order(ByteOrder.nativeOrder());
mTexCoordBuffer = cbb.asFloatBuffer();
mTexCoordBuffer.put(colors);
mTexCoordBuffer.position(0);
}
private void initShder(){
mProgram = shaderUtil.createProgram(mVertexShader,mFragmentShader);
maPositionHandle = GLES30.glGetAttribLocation(mProgram,"aPosition");
maTexCoordHandle = GLES30.glGetAttribLocation(mProgram,"aTexCoord");
}
public void drawSelf(int textId){
GLES30.glUseProgram(mProgram);
GLES30.glVertexAttribPointer(maPositionHandle,3,
GLES30.GL_FLOAT,false,3*4,mVertexBuffer);
GLES30.glVertexAttribPointer(maTexCoordHandle,2,
GLES30.GL_FLOAT,false,2*4,mTexCoordBuffer);
GLES30.glEnableVertexAttribArray(maPositionHandle);
GLES30.glEnableVertexAttribArray(maTexCoordHandle);
GLES30.glActiveTexture(GLES30.GL_TEXTURE0); //设置使用的纹理编号
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,textId); //绑定指定的纹理id
GLES30.glDrawArrays(GLES30.GL_TRIANGLES,0,mvCount);
}
}
上面的代码基本和之前的代码差不多,但是有一点改变的是,顶点的颜色,不再是直接用RGBA来表示了,而是用了纹理,将纹理坐标传入了渲染管线。
public class TextureView extends GLSurfaceView {
private TextureRender mRender;
public TextureView(Context context) {
this(context,null);
}
public TextureView(Context context, AttributeSet attrs) {
super(context, attrs);
setEGLContextClientVersion(3);
mRender = new TextureRender();
setRenderer(mRender);
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
private class TextureRender implements Renderer{
private TriangleTexture mTriangle;
private int mTextureId;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mTriangle = new TriangleTexture();
GLES30.glClearColor(1,1,1,1);
initTexture();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES30.glViewport(0,0,width,height);
}
@Override
public void onDrawFrame(GL10 gl) {
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
mTriangle.drawSelf(mTextureId);
}
private void initTexture(){
int textures[] = new int[1]; //生成纹理id
GLES30.glGenTextures( //创建纹理对象
1, //产生纹理id的数量
textures, //纹理id的数组
0 //偏移量
);
mTextureId = textures[0];
//绑定纹理id,将对象绑定到环境的纹理单元
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,mTextureId);
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_MIN_FILTER,GLES30.GL_NEAREST);//设置MIN 采样方式
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_MAG_FILTER,GLES30.GL_LINEAR);//设置MAG采样方式
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_WRAP_S,GLES30.GL_CLAMP_TO_EDGE);//设置S轴拉伸方式
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_WRAP_T,GLES30.GL_CLAMP_TO_EDGE);//设置T轴拉伸方式
//加载图片
Bitmap bitmap = BitmapFactory.decodeResource(
TextureView.this.getResources(), R.mipmap.brick);
GLUtils.texImage2D( //实际加载纹理进显存
GLES30.GL_TEXTURE_2D, //纹理类型
0, //纹理的层次,0表示基本图像层,可以理解为直接贴图
bitmap, //纹理图像
0 //纹理边框尺寸
);
bitmap.recycle();
}
}
}
纹理拉伸
在上面例子中,在initTexture方法中对纹理进行的S轴和T轴的拉伸方式的设置,我们具体来看看:
重复拉伸方式
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_WRAP_S,GLES30.GL_REPEAT);
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_WRAP_T,GLES30.GL_REPEAT);
当顶点纹理坐标大于1时则实际起作用的纹理坐标为纹理坐标的小树部分,也就是若纹理坐标为3.3,则其作用的是0.3:
原先的显示为:
当我把S,T的拉伸该为重复拉伸,将纹理坐标改成:
float[] colors = new float[]{
4,0,
4,4,
0,4,
0,0
};
这样显示的效果:
我们可以看到,纹理的横纵都重复了4个。
截取拉伸方式
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_WRAP_S,GLES30.GL_CLAMP_TO_EDGE);//设置S轴拉伸方式
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_WRAP_T,GLES30.GL_CLAMP_TO_EDGE);//设置T轴拉伸方式
截取方式中当纹理坐标的值大于1时都看做1,因此会产生边缘被拉伸的效果:
float[] colors = new float[]{
4,0,
4,4,
0,4,
0,0
};
纹理采样
纹理采样就是根据片元的纹理坐标到纹理图中提取对应位置颜色的过程。
但是由于被渲染图元中的片元数量与其对应纹理区域中像素的数量并不一定相同,也就是说图元中的片元与纹理图中的像素并不总是一一对应的。
例如,将较小的纹理图映射到较大的图元或将较大的纹理图映射到较小的图元时,通过纹理坐标在纹理图中不一定能找到与之完全对应的像素,这个时候就必须采取一些策略:
最近点采样
最近点采样的原理就是根据片元的纹理坐标在纹理图中的位置距离那个像素近就选择哪个像素的颜色值作为该片元的颜色采样值。最近点采样计算很简单,但存在的问题就是将较小的纹理图映射到较大的图元上容易产生明显的锯齿,同时将较大的纹理缩小的一定的程度后也就不好用了。加载纹理时设置最近点采样的方式
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_NEAREST);
线性纹理采样
线性纹理采样的结果并不仅来自纹理图中的一个像素,在进行纹理采样时会考虑到该片元对应的纹理坐标周围的几个像素,然后根据周围几个像素的比例进行加权得到最终的采样结果。由于线性采样对多个像素进行了加权平均,仅此将较小的图元纹理映射到较大的图元时,不会有锯齿现象,而是平滑过渡。加载纹理时设置线性纹理采样的方式
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
因此一般来说对于缩小映射采用最近点采样,对于放大映射来说采用线性采样。
MIN与MAG采样
当一副尺寸较大的纹理图映射到一个显示到屏幕上尺寸比其小的图元时,系统将采用GLES20.GL_TEXTURE_MIN_FILTER
的采样方式。
当一副尺寸较小的纹理图映射到一个显示到屏幕上尺寸比其大的图元时,系统将采用GLES20.GL_TEXTURE_MAG_FILTER
的采样方式。
一般在代码中都会分别进行设置
mipmap纹理技术
待定。。。。。
项目地址: https://github.com/vivianluomin/PracticeEveryDay/tree/master/LearnOpenGL