在之前的一系列博客中, 介绍了OpenGLES相关的着色器Shader, 顶点及图形绘制。
那么, 接下来将进入图片纹理Texture的部分. 这里, 我们首先使用OpenGLES的方式绘制一张图片到屏幕上.
Shader脚本
Vertex Shader如下:
attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsOut;
void main(void)
{
gl_Position = Position;
TextureCoordsOut = TextureCoords;
}
其中, Position依然是顶点坐标, 即将要绘制到OpenGLES坐标系上的位置. TextureCoords是纹理坐标, 即将要把图片纹理的哪一个像素点绘制出来.
Fragment Shader如下:
precision mediump float;
uniform sampler2D Texture;
varying vec2 TextureCoordsOut;
void main(void)
{
vec4 mask = texture2D(Texture, TextureCoordsOut);
gl_FragColor = vec4(mask.rgb, 1.0);
}
TextureCoordsOut是接收Vertex Shader中的TextureCoords变量的值. 参数Texture是图片纹理的来源, 使用texture2D(Texture, TextureCoordsOut);可获取对应纹理坐标点上的颜色. 将该颜色传递给gl_FragColor即可绘制出来图片在该纹理坐标点上的颜色.
关联Shader参数
_glProgram = [ShaderOperations compileShaders:@"DemoDrawImageTextureVertex" shaderFragment:@"DemoDrawImageTextureFragment"];
glUseProgram(_glProgram);
// 需要三个参数, 跟Shader中的一一对应。
// Position: 将颜色放置在CAEAGLLayer上的哪个位置
// Texture: 图像的纹理
// TextureCoords: 图像的纹理坐标,即图像纹理的哪一块颜色
_positionSlot = glGetAttribLocation(_glProgram, "Position");
_textureSlot = glGetUniformLocation(_glProgram, "Texture");
_textureCoordsSlot = glGetAttribLocation(_glProgram, "TextureCoords");
此处与之前的demo基本一致.
获取图片纹理
获取图片纹理的方式通常比较固定, 涉及到一系列的CoreGraphics方法调用, 封装如下:
/**
* 加载image, 使用CoreGraphics将位图以RGBA格式存放. 将UIImage图像数据转化成OpenGL ES接受的数据.
* 然后在GPU中将图像纹理传递给GL_TEXTURE_2D。
* @return 返回的是纹理对象,该纹理对象暂时未跟GL_TEXTURE_2D绑定(要调用bind)。
* 即GL_TEXTURE_2D中的图像数据都可从纹理对象中取出。
*/
- (GLuint)setupTexture:(UIImage *)image {
CGImageRef cgImageRef = [image CGImage];
GLuint width = (GLuint)CGImageGetWidth(cgImageRef);
GLuint height = (GLuint)CGImageGetHeight(cgImageRef);
CGRect rect = CGRectMake(0, 0, width, height);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
void *imageData = malloc(width * height * 4);
CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0f, -1.0f);
CGColorSpaceRelease(colorSpace);
CGContextClearRect(context, rect);
CGContextDrawImage(context, rect, cgImageRef);
glEnable(GL_TEXTURE_2D);
/**
* GL_TEXTURE_2D表示操作2D纹理
* 创建纹理对象,
* 绑定纹理对象,
*/
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
/**
* 纹理过滤函数
* 图象从纹理图象空间映射到帧缓冲图象空间(映射需要重新构造纹理图像,这样就会造成应用到多边形上的图像失真),
* 这时就可用glTexParmeteri()函数来确定如何把纹理象素映射成像素.
* 如何把图像从纹理图像空间映射到帧缓冲图像空间(即如何把纹理像素映射成像素)
*/
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // S方向上的贴图模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // T方向上的贴图模式
// 线性过滤:使用距离当前渲染像素中心最近的4个纹理像素加权平均值
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
/**
* 将图像数据传递给到GL_TEXTURE_2D中, 因其于textureID纹理对象已经绑定,所以即传递给了textureID纹理对象中。
* glTexImage2d会将图像数据从CPU内存通过PCIE上传到GPU内存。
* 不使用PBO时它是一个阻塞CPU的函数,数据量大会卡。
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
// 结束后要做清理
glBindTexture(GL_TEXTURE_2D, 0); //解绑
CGContextRelease(context);
free(imageData);
return textureID;
}
该方法接收一个UIImage对象, 将其图像数据传递至一个纹理对象中, 最后返回该纹理对象. 则以后取出该图像数据时, 直接根据纹理对象取出即可. 其中的glTexImage2D方法用于将图像数据传递至GL_TEXTURE_2D中, 而如果已经提前将GL_TEXTURE_2D与一个纹理对象绑定好了, 则该方法可将图像数据传递至纹理对象中(这里是textureID).
混合模式
关于混合模式, 是一个非常大的话题. 这里只介绍glBlendFunc的使用. glBlendFunc的参数1作用于源数据, 参数2作用于目标数据. 混合作用的结果颜色是 源颜色源因子 + 目标颜色目标因子. 例如, 我们这里的demo是直接将一张图片贴在OpenGL画布上, 因此图片取GL_ONE, 而目标数据即OpenGL画布取GL_ZERO.
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
而另外一种非常常见的混合模式是:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
混合后的结果颜色是 source * source_alpha + destination * (1 - source_alpha). 这种混合模式就是将两种颜色混合叠加的效果. 只有RGBA模式下才使用blend模式. 其他常见的混合模式有:
glBlendFunc(GL_ONE, GL_ZERO) 完全使用源颜色, 完全不使用目标颜色. 和不使用blend的时候一致.
glBlendFunc(GL_ZERO, GL_ONE) 完全不使用源颜色, 完全使用目标颜色. 即原图没有变化.
glBlendFunc(GL_ONE, GL_ONE) 两种颜色的直接相加.
纹理的渲染
有了纹理对象且存储了图像数据后, 就可以拿来进行纹理渲染了. 使用时, 直接再次绑定GL_TEXTURE_2D即可, 表示接下来读取GL_TEXTURE_2D时要使用的是_textureID纹理对象中的数据:
// 第一行和第三行不是严格必须的,默认使用GL_TEXTURE0作为当前激活的纹理单元
glActiveTexture(GL_TEXTURE5); // 指定纹理单元GL_TEXTURE5
glBindTexture(GL_TEXTURE_2D, _textureID); // 绑定,即可从_textureID中取出图像数据。
glUniform1i(_textureSlot, 5); // 与纹理单元的序号对应
纹理数据可以跟纹理坐标一一对应, 在OpenGL坐标中呈现出来.
GLfloat texCoords[] = {
0, 0,//左下
1, 0,//右下
0, 1,//左上
1, 1,//右上
};
glVertexAttribPointer(_textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, 0, texCoords);
glEnableVertexAttribArray(_textureCoordsSlot);
纹理坐标也同样存储于一个数组中, 使用glVertexAttribPointer方法将其传递给对应的插槽_textureCoordsSlot, 继而传递给Shader中的TextureCoords变量. 注意, 纹理坐标的坐标系与OpenGL不同, 左下角是原点.
有了纹理数据和纹理坐标, 接下来只需要我们已经很熟悉的顶点绘制方式将OpenGL画布绘制出来即可. 这里, 顶点坐标与纹理坐标的位置要一一对应, 即将图像的某个点绘制到OpenGL画布的对应点. 如果我们只是普通的渲染图片, 则将左下, 右下, 左上, 右上这四个点一一对应起来就好了.
GLfloat vertices[] = {
-1, -1, 0, //左下
1, -1, 0, //右下
-1, 1, 0, //左上
1, 1, 0 }; //右上
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glEnableVertexAttribArray(_positionSlot);
// 一旦纹理数据准备好,两个坐标系的顶点位置一一对应好。
// 就直接绘制顶点即可, 具体的绘制方式就与纹理坐标和纹理数据没有关系了。
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindTexture(GL_TEXTURE_2D, 0); // 使用完之后解绑GL_TEXTURE_2D
[_eaglContext presentRenderbuffer:GL_RENDERBUFFER];
这样, 使用OpenGLES来渲染一张图片的过程就结束了.
实际上, 准备好了纹理数据, 并且将纹理坐标传递给了Shader之后, 就不再需要对其进行操作了. 因此, 至于要不要使用VBO来优化顶点绘制的效率, 就是另外一回事了.
使用索引和VBO来绘制
那么, 既然如此, 再来回顾一下索引数组和VBO的使用吧.
const GLfloat vertices[] = {
-1, -1, 0, //左下
1, -1, 0, //右下
-1, 1, 0, //左上
1, 1, 0 }; //右上
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(_positionSlot);
const GLubyte indices[] = {
0,1,2,
1,2,3
};
GLuint indexBuffer;
glGenBuffers(1, &indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(indices[0]), GL_UNSIGNED_BYTE, 0);
Demo
请参考: Demo