一、前言
前面介绍的都是绘制一个简单的有规律的图形,比如三角形、正方形等等。但是对于那些比较复杂的图像,这种方式就不行了,比如显示一张图片、相机预览,这些数据是无规格的,它只与图像的的内容有关系。为此引出了纹理(2D纹理),即一张图片。通过顶点坐标固定住显示的区域,和纹理坐标进行映射以此固定纹理的位置。最后通过采样,采样某一点的颜色,并显示。
纹理坐标,标明该从纹理图像的对应部分采样颜色,因为是2D纹理,所以只存在于X和Y轴坐标,取值范围在0 ~ 1。纹理坐标需要和顶点坐标进行对应,只有图片的顶点对应上,这个图片的位置才能固定(如图片的旋转)。
二、纹理过滤
当我们的纹理显示到设备上,每一个点并不能完完整整的对应上每一个像素点的,有可能多有可能少,这样就需要二次处理。如屏幕的像素是1920 * 1280,图片的宽和高分别是1920和1280,这样才能完全的对应上,但图片的宽和高是1280 * 720,那就会导致设备上的像素有的不能对应上。为此需要为那些没有分配到颜色的像素点想想办法,通过算法的方式给它们显示个合理的颜色。比如通过相邻的像素点计算个折中的像素。OpenGLES也提供了已下的算法。
过滤方式 | 描述 | +为取样的点 |
---|---|---|
GL_NEAREST | OpenGL默认的纹理过滤方式,OpenGL会选择中心点最接近纹理坐标的那个像素 |
|
GL_LINEAR | 基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色 |
|
从上面的可以看到,GL_LINEAR线性插值的方式的消耗大点但效果要比GL_NEAREST 好。
因为存在着图片放大和缩小两种可能,所以可以分别的者两种可能做不同的算法:
- GL_TEXTURE_MAG_FILTER : 放大模式
- GL_TEXTURE_MIN_FILTER :缩小模式
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
三、纹理环绕
原因:
纹理坐标超出(0,1)范围时,不能满屏显示,需要指定空余地方该怎么显示。
OpenGLES提供了已下四种方式:
环绕方式 | 描述 |
---|---|
GL_REPEAT | 纹理的默认行为,重复纹理图像。 |
GL_MIRRORED_REPEAT | 镜像重复图像 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_MIRRORED_REPEAT | 超出的坐标为用户指定的边缘颜色。 |
//纹理也有坐标系,称UV坐标,或者ST坐标
// S轴的拉伸方式为重复,决定采样值的坐标超出图片范围时的采样方式
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
// T轴的拉伸方式为重复
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
- GL_TEXTURE_WRAP_S : 纹理坐标系S轴
- GL_TEXTURE_WRAP_T : 纹理坐标系T轴
四、显示图片纹理
这里采用的是顶点法绘制纹理,所以不需要顶点索引坐标数组。
1)、顶点坐标、纹理坐标和颜色
// 原始的矩形区域的顶点坐标
private static final float CUBE[] = {
-1.0f, -1.0f,0.0f, // v1
1.0f, -1.0f, 0.0f, // v2
-1.0f, 1.0f,0.0f, // v3
1.0f, 1.0f, 0.0f, // v4
};
// 纹理坐标,每个坐标的纹理采样对应上面顶点坐标。
// 纹理为0 ~ 2,会有四分屏
private static final float TEXTURE_NO_ROTATION[] = {
0.0f, 1.0f, // v1
1.0f, 1.0f, // v2
0.0f, 0.0f, // v3
1.0f, 0.0f, // v4
};
//设置颜色
private static final float COLORS[] = {
0.0f, 1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f
};
2)、顶点和片段着色器
首先我们将颜色和纹理显示在同一个区域。所以导致GLSL代码稍微多点。
顶点着色器:
- 顶点坐标
- 纹理坐标
- 颜色输入(varying )
- 矩阵变换
片段着色器:
- sampler2D 纹理采样器
- mix RGBA颜色和纹理混合
// 顶点shader
private static final String VERTEX_SHADER = "" +
"precision mediump float;\n"+
"attribute vec4 position;\n" + // 顶点着色器的顶点坐标,由外部程序传入
"attribute vec2 inputTextureCoordinate;\n" + // 传入的纹理坐标
"attribute vec4 aColor;\n" +
"varying vec4 mColor;\n" + // 传入的纹理坐标
"uniform mat4 transform;" + // 变换矩阵
"varying vec2 textureCoordinate;\n" +
" \n" +
"void main()\n" +
"{\n" +
" gl_Position = transform*position;\n" +
" mColor = aColor;\n" +
" textureCoordinate = inputTextureCoordinate;\n" + // 最终顶点位置
"}";
// 光栅化后产生了多少个片段,就会插值计算出多少个varying变量,同时渲染管线就会调用多少次片段着色器
private static final String FRAGMENT_SHADER =
"precision mediump float;\n"+
"varying vec2 textureCoordinate;\n" + // 最终顶点位置,上面顶点着色器的varying变量会传递到这里
"uniform sampler2D vTexture;\n" + // 外部传入的图片纹理 即代表整张图片的数据
"varying vec4 mColor;\n" + // 传入的纹理坐标
"void main()\n" +
"{" +
"gl_FragColor =mix ( texture2D(vTexture,vec2(1.0-textureCoordinate.x,textureCoordinate.y)) , mColor,0.2);"+ // 增加1.0 - ,为了使图像反转
"}";
3)、创建纹理
int[] textures = new int[1];
// 加载纹理
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
//纹理也有坐标系,称UV坐标,或者ST坐标
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
// GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,0,GLES20.GL_RGBA,mBitmap.getWidth(),mBitmap.getHeight(),0,GLES20.GL_RGBA,GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glUniform1i(mGLUniformTexture, 1); // 设置第一层纹理
- 1、GLES20.glGenTextures(1, textures, 0);创建一个大小为1的纹理
- 2、GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
绑定纹理,因为是图片所以纹理类型是GL_TEXTURE_2D - 3、纹理过滤和设置环绕方式
- 4、通过glTexImage2D,设置纹理的格式和大小,这里可以不用先传入数据
- 5、设置纹理层glUniform1i
一个着色器程序中,它会有很多歌纹理,所以需要对应好着色器中的纹理层,这样后面就可以通过这个纹理层设置数据,默认的会对应第0层纹理
4)、传入替换纹理数据
- 1、glActiveTexture激活上面设置的纹理层(上面设置的第1层,所以这里也对应第一层)
- 2、glBindTexture绑定一个GL_TEXTURE_2D纹理
- 3、替换纹理数据texSubImage2D,这里替换的是Bitmap
经过了以上了这几步,纹理已经有了数据,现在可以直接绘制了
GLES20.glActiveTexture(GLES20.GL_TEXTURE0+1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mGLTextureId);
GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0,0,0,mBitmap);
5)、绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
采用顶点法,绘制的是两个三角形,组合成一个长方形。
该绘制的结果与取样有关系,现在看下片段着色器中的代码:
gl_FragColor =mix ( texture2D(vTexture,vec2(1.0-textureCoordinate.x,textureCoordinate.y)) , mColor,0.3);
其中mix是混合两个颜色的意思,后面参数的0.3就代表取样比是多少(vTexture占0.7,mColor占0.3),其中texture2D是采样,采样的位置我们是1.0-textureCoordinate.x,这样就会达到了反转的作用,会使纹理坐标和顶点坐标映射时候出现图像反转左右。
五、代码
代码整合在github
下面是全部代码:
public class ImageRender implements GLSurfaceView.Renderer {
private final static String TAG = ImageRender.class.getSimpleName();
private int mPositionHandle; // 顶点
private int mColorHandle; // 颜色
private int mTextureHandle; // 纹理
private int mMvpMatrixHandle; // 变换矩阵
private int mProgram; // Program
private int mGLUniformTexture; // 图片纹理
private int mGLTextureId = -1; // 纹理ID
private final Bitmap mBitmap;
private FloatBuffer mCubeBuffer;
private FloatBuffer mTextureBuffer;
private FloatBuffer mColorBuffer;
private FloatBuffer mMatrixBuffer;
// 原始的矩形区域的顶点坐标,因为后面使用了顶点法绘制顶点,所以不用定义绘制顶点的索引。无论窗口的大小为多少,在OpenGL二维坐标系中都是为下面表示的矩形区域
private static final float CUBE[] = { // 窗口中心为OpenGL二维坐标系的原点(0,0)
-1.0f, -1.0f, 0.0f, // v1
1.0f, -1.0f, 0.0f, // v2
-1.0f, 1.0f, 0.0f, // v3
1.0f, 1.0f, 0.0f, // v4
};
// 纹理也有坐标系,称UV坐标,或者ST坐标。UV坐标定义为左上角(0,0),右下角(1,1),一张图片无论大小为多少,在UV坐标系中都是图片左上角为(0,0),右下角(1,1)
// 纹理坐标,每个坐标的纹理采样对应上面顶点坐标。
// 纹理为0 ~ 2,会有四分屏
private static final float TEXTURE_NO_ROTATION[] = {
0.0f, 1.0f, // v1
1.0f, 1.0f, // v2
0.0f, 0.0f, // v3
1.0f, 0.0f, // v4
};
//设置颜色
private static final float COLORS[] = {
0.0f, 1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f
};
private float[] mViewMatrix = new float[16];
private float[] mProjectMatrix = new float[16];
private float[] mMVPMatrix = new float[16];
// 顶点shader
private static final String VERTEX_SHADER = "" +
"precision mediump float;\n" +
"attribute vec4 position;\n" + // 顶点着色器的顶点坐标,由外部程序传入
"attribute vec2 inputTextureCoordinate;\n" + // 传入的纹理坐标
"attribute vec4 aColor;\n" +
"varying vec4 mColor;\n" + // 传入的纹理坐标
"uniform mat4 transform;" + // 变换矩阵
"varying vec2 textureCoordinate;\n" +
" \n" +
"void main()\n" +
"{\n" +
" gl_Position = transform*position;\n" +
" mColor = aColor;\n" +
" textureCoordinate = inputTextureCoordinate;\n" + // 最终顶点位置
"}";
// 光栅化后产生了多少个片段,就会插值计算出多少个varying变量,同时渲染管线就会调用多少次片段着色器
private static final String FRAGMENT_SHADER =
"precision mediump float;\n" +
"varying vec2 textureCoordinate;\n" + // 最终顶点位置,上面顶点着色器的varying变量会传递到这里
"uniform sampler2D vTexture;\n" + // 外部传入的图片纹理 即代表整张图片的数据
"varying vec4 mColor;\n" + // 传入的纹理坐标
"void main()\n" +
"{" +
"gl_FragColor =mix ( texture2D(vTexture,vec2(1.0-textureCoordinate.x,textureCoordinate.y)) , mColor,0.2);" + // 增加1.0 - ,为了使图像反转
"}";
public ImageRender(Bitmap bitmap) {
this.mBitmap = bitmap;
}
// 加载Handle
private void makeHandle() {
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "position");
mColorHandle = GLES20.glGetAttribLocation(mProgram, "aColor");
mTextureHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
mMvpMatrixHandle = GLES20.glGetUniformLocation(mProgram, "transform");
mGLUniformTexture = GLES20.glGetUniformLocation(mProgram, "vTexture");
Log.e(TAG, "makeHandle: mPositionHandle=" + mPositionHandle + " mColorHandle=" + mColorHandle + " mTextureHandle="
+ mTextureHandle + " mMvpMatrixHandle=" + mMvpMatrixHandle + " mGLUniformTexture=" + mGLUniformTexture);
}
// 加载Buffer
private void loadBuffer() {
mCubeBuffer = initBuffer(CUBE);
mTextureBuffer = initBuffer(TEXTURE_NO_ROTATION);
mColorBuffer = initBuffer(COLORS);
mMatrixBuffer = initBuffer(mMVPMatrix);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 创建program
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
//创建一个空的OpenGLES程序
mProgram = GLES20.glCreateProgram();
Log.e(TAG, "onSurfaceCreated: vertexShader=" + vertexShader + " fragmentShader=" + fragmentShader + " mProgram=" + mProgram);
//将顶点着色器加入到程序
GLES20.glAttachShader(mProgram, vertexShader);
//将片元着色器加入到程序中
GLES20.glAttachShader(mProgram, fragmentShader);
//连接到着色器程序
GLES20.glLinkProgram(mProgram);
// 获取program的链接情况
int[] link = new int[1];
GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, link, 0);
if (link[0] <= 0) {
Log.e("Load Program", "Linking Failed" + GLES20.glGetProgramInfoLog(mProgram));
}
GLES20.glUseProgram(mProgram);
makeHandle();
int[] textures = new int[1];
// 加载纹理
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
//纹理也有坐标系,称UV坐标,或者ST坐标
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT); // S轴的拉伸方式为重复,决定采样值的坐标超出图片范围时的采样方式
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT); // T轴的拉伸方式为重复
// GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, mBitmap.getWidth(), mBitmap.getHeight(),
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
// GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER,GLES20.GL_COLOR_ATTACHMENT0,
// GLES20.GL_TEXTURE_2D,textures[0],0);
if (GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER) != GLES20.GL_FRAMEBUFFER_COMPLETE) {
Log.e(TAG, "glCheckFramebufferStatus error");
} else {
mGLTextureId = textures[0];
GLES20.glUniform1i(mGLUniformTexture, 1); // 设置第一层纹理
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height); // 设置窗口大小
Log.e(TAG, "onSurfaceChanged: width=" + width + " height=" + height);
int w = mBitmap.getWidth();
int h = mBitmap.getHeight();
float sWH = w / (float) h;
float sWidthHeight = width / (float) height;
if (width > height) {
if (sWH > sWidthHeight) {
Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight * sWH, sWidthHeight * sWH, -1, 1, 3, 7);
} else {
Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight / sWH, sWidthHeight / sWH, -1, 1, 3, 7);
}
} else {
if (sWH > sWidthHeight) {
Matrix.orthoM(mProjectMatrix, 0, -1, 1, -1 / sWidthHeight * sWH, 1 / sWidthHeight * sWH, 3, 7);
} else {
Matrix.orthoM(mProjectMatrix, 0, -1, 1, -sWH / sWidthHeight, sWH / sWidthHeight, 3, 7);
}
}
//设置相机位置
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
//计算变换矩阵
Matrix.multiplyMM(mMVPMatrix, 0, mProjectMatrix, 0, mViewMatrix, 0);
loadBuffer();
}
@Override
public void onDrawFrame(GL10 gl) {
// 顶点
GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false, 12, mCubeBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);
// 顶点着色器的纹理坐标
GLES20.glVertexAttribPointer(mTextureHandle, 2, GLES20.GL_FLOAT, false, 8, mTextureBuffer);
GLES20.glEnableVertexAttribArray(mTextureHandle);
// 传入的图片纹理
if (mGLTextureId != -1) {
Log.e(TAG, "onDrawFrame: mGLTextureId=" + mGLTextureId);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + 1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mGLTextureId);
GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, mBitmap);
}
// 变换矩阵
GLES20.glUniformMatrix4fv(mMvpMatrixHandle, 1, false, mMatrixBuffer);
//获取片元着色器的vColor成员的句柄
//设置绘制三角形的颜色
GLES20.glEnableVertexAttribArray(mColorHandle);
GLES20.glVertexAttribPointer(mColorHandle, 4,
GLES20.GL_FLOAT, false,
4, mColorBuffer);
// 绘制顶点 ,方式有顶点法和索引法
// GLES20.GL_TRIANGLE_STRIP即每相邻三个顶点组成一个三角形,为一系列相接三角形构成
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); // 顶点法,按照传入渲染管线的顶点顺序及采用的绘制方式将顶点组成图元进行绘制
GLES20.glDisableVertexAttribArray(mPositionHandle);
GLES20.glDisableVertexAttribArray(mTextureHandle);
GLES20.glDisableVertexAttribArray(mColorHandle);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
}
/**
* 加载shader
*
* @param type 片元、顶点
* @param shaderCode Code
* @return int
*/
private int loadShader(int type, String shaderCode) {
//根据type创建顶点着色器或者片元着色器
int shader = GLES20.glCreateShader(type);
//将资源加入到着色器中,并编译
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
// 初始化buffer
private static FloatBuffer initBuffer(float[] buffers) {
// 先初始化buffer,数组的长度*4,因为一个float占4个字节
ByteBuffer mbb = ByteBuffer.allocateDirect(buffers.length * 4);
// 数组排列用nativeOrder
mbb.order(ByteOrder.nativeOrder());
FloatBuffer floatBuffer = mbb.asFloatBuffer();
floatBuffer.put(buffers);
floatBuffer.flip();
return floatBuffer;
}
}