最近研究了OpenGL的一些绘图方面的东西,这里给大家分享一下做的一个小demo,顺带自己复习下。
功能主要是在绘制好的图片上进行点击,将规定的范围内的色值改为灰色。就是一个画笔的功能实现
这一篇,默认大家对OpenGL还是有一定的了解的,一些基础的东西相对有所了解
#import "OpenGLView.h"
//系统目录
#import <QuartzCore/QuartzCore.h>
#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>
@interface OpenGLView () {
CAEAGLLayer *_eaglLayer;
EAGLContext *_context;
//参数索引
GLuint _colorRenderBuffer;
GLuint _positionSlot;
GLuint _texCoordSlot;
GLuint _imageTexture;
GLuint _vertexBuffer;
GLuint _indexBuffer;
GLuint _textureUniform;
//灰色块参数索引
GLuint _grayVertexBuffer;
GLuint _grayIndexBuffer;
GLuint _grayPositionSlot;
GLuint _grayTexCoordSlot;
GLuint _grayTransTexCoordSlot;
//混合图片
GLuint _transparentImageTexture;
GLuint _transparentTextureUniform;
}
@end
typedef struct {
float Position[3];
float TexCoord[2];
} Vertex;
typedef struct {
float grayPosition[3];
float grayTexCoord[2];
float transTexCoord[2];
} GrayVertex;
const Vertex Vertices[] = {
{{1, -1, 0}, {-0, -0}},
{{1, 1, 0}, {-0, -1}},
{{-1, 1, 0}, {-1, -1}},
{{-1, -1, 0},{-1, -0}}
};
const GLubyte Indices[] = {
0, 1, 2,
2, 3, 0
};
const GLubyte GrayIndices[] = {
0, 1, 2,
2, 3, 0
};
@implementation OpenGLView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.isRendGray = NO;
[self setupLayer];
[self setupContext];
[self setupRenderBuffer];
[self setupFrameBuffer];
_imageTexture = [self setupTexture:@"image.jpg"];
_transparentImageTexture = [self setupTexture2:@"magic.png"];
[self compileShaders];
[self setupVBOs];
[self render];
}
return self;
}
/**
* 想要显示OpenGL内容,你需要把它缺省的layer设置为一个特殊的Layer
*/
+ (Class)layerClass {
return [CAEAGLLayer class];
}
/**
* 设置layer为不透明状态,因为缺省的话,CALayer是透明的。而透明层对性能的负荷很大,特别是OpengGL的层。
*/
- (void)setupLayer {
_eaglLayer = (CAEAGLLayer *)self.layer;
_eaglLayer.opaque = YES;
// 设置描绘属性,在这里设置维持渲染内容以及颜色格式
// 为了保存层中用到的OpenGL ES的帧缓存类型的信息
//这段代码是告诉Core Animation要试图保留任何以前绘制的图像留作以后重用
//双缓存机制:单缓存机制是在渲染前必须要清空画面然后再进行渲染,这样在重绘的过程中会不断的闪烁。双缓存机制,是在后台先将需要绘制的画面绘制好然后再显示绘制出来
_eaglLayer.drawableProperties = @{
kEAGLDrawablePropertyRetainedBacking: [NSNumber numberWithBool:YES],
kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
};
}
/**
* EAGLContext管理所有通过OpenGL进行draw的信息,创建一个context并声明用的哪个版本
*/
- (void)setupContext {
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
_context = [[EAGLContext alloc] initWithAPI:api];
if (!_context) {
NSLog(@"Failed to initialize OpenGLES 2.0 context");
return;
}
if (![EAGLContext setCurrentContext:_context]) {
NSLog(@"Failed to set current OpenGL context");
return;
}
}
/**
* 创建渲染缓冲区render buffer
* render buffer 是OpenGL的一个对象,用于存放渲染过的图像
*/
- (void)setupRenderBuffer {
//调用函数来创建一个新的render buffer索引.这里返回一个唯一的integer来标记render buffer
glGenRenderbuffers(1, &_colorRenderBuffer);
//调用函数,告诉OpenGL,我们定义的buffer对象属于哪一种OpenGL对象
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer);
//为render buffer分配空间
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];
}
/**
* 创建一个帧缓冲区frame buffer
*/
- (void)setupFrameBuffer {
GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
//把前面创建的buffer render 依附在frame buffer的GL_COLOR_ATTACHMENT0的位置上
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER, _colorRenderBuffer);
}
- (void)compileShaders {
// 编译vertex shader 和 fragment shader
GLuint vertexShader = [self compileShader:@"SimpleVertex"
withType:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShader:@"SimpleFragment"
withType:GL_FRAGMENT_SHADER];
// 链接vertex 和 fragment成一个完整的Program
GLuint programHandle = glCreateProgram();
glAttachShader(programHandle, vertexShader);
glAttachShader(programHandle, fragmentShader);
glLinkProgram(programHandle);
// 输出错误信息
GLint linkSuccess;
glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(programHandle, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"%@", messageString);
return;
}
// 执行program
glUseProgram(programHandle);
// 获取指向vertex shader传入变量的指针。以后可以通过这个写指针来使用。调用glEnableVertexAttribArray来启用数据
_positionSlot = glGetAttribLocation(programHandle, "Position");
glEnableVertexAttribArray(_positionSlot);
_texCoordSlot = glGetAttribLocation(programHandle, "TexCoordIn");
glEnableVertexAttribArray(_texCoordSlot);
_textureUniform = glGetUniformLocation(programHandle, "Texture");
}
- (GLuint)compileShader:(NSString*)shaderName withType:(GLenum)shaderType {
//在NSBundle中查找某个文件
NSString *shaderPath = [[NSBundle mainBundle] pathForResource:shaderName
ofType:@"glsl"];
NSError *error;
NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath
encoding:NSUTF8StringEncoding error:&error];
if (!shaderString) {
NSLog(@"Error loading shader: %@", error.localizedDescription);
}
//创建一个代表shader的OpenGL对象。这时你必须告诉OpenGL,你想创建的是frament shader 还是 vertex shader.所以便有了这个参数:shaderTypeType
GLuint shaderHandle = glCreateShader(shaderType);
//让OpenGL获取到这个shader的源代码,把NSString转换成C-string
const char *shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = [shaderString length];
glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
//运行时编译shader
glCompileShader(shaderHandle);
// 输出失败信息
GLint compileSuccess;
glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
GLchar messages[256];
glGetShaderInfoLog(shaderHandle, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"%@", messageString);
}
return shaderHandle;
}
- (void)setupVBOs {
//创建索引
glGenBuffers(1, &_vertexBuffer);
//在glBufferData之前需要将要使用的缓冲区绑定
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
/**
* 把数据传到缓冲区
*
* @param target 与绑定缓冲区时使用的目标相同
* @param size 我们将要上传的数据大小,以字节为单位
* @param data 将要上传的数据本身
* @param usage 告诉OpenGL我们打算如何使用缓冲区
*/
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
glGenBuffers(1, &_indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
}
/**
* 获取图片里面的像素数据
*/
- (GLuint)setupTexture:(NSString *)imageName{
//初始化一个UIImage对象,然后获得它的CGImage属性
//图片规格有限制 只能用2次方的大小的图
CGImageRef spriteImage = [UIImage imageNamed:imageName].CGImage;
if (!spriteImage) {
NSLog(@"load image failed");
exit(1);
}
//获取image的宽度和高度然后手动分配空间 width*height*4个字节的数据空间
//空间*4的原因是,我们在调用方法来绘制图片数据时,我们要为red,green,blue和alpha通道,每个通道要准备一个字节
//每个通道准备一个字节的原因,因为要用CoreGraphics来建立绘图上下文。而CGBitmapContextCreate函数里面的第4个参数指定的就是每个通道要采用几位来表现,我们只用8位,所以是一个字节
NSInteger width = 512;
NSInteger height = 512;
GLubyte *spriteData = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width * 4, CGColorSpaceCreateDeviceRGB(), kCGImageAlphaPremultipliedLast);
//告诉Core Graphics在一个指定的矩形区域内来绘制这些图像
CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
//完成绘制要释放
CGContextRelease(spriteContext);
//把像素信息发送给OpenGL,首先调用glGenTextures来创建一个纹理对象,并且得到一个唯一的ID,由"name"保存着。然后,我们调用glBindTexture来把我们新建的纹理名字加载到当前的纹理单元中。
GLuint texName;
glGenTextures(1, &texName);
glBindTexture(GL_TEXTURE_2D, texName);
glUniform1i(_textureUniform, 0);
//接下来的步骤是,为我们的纹理设置纹理参数,使用glTexParameterf函数,。这里我们设置函数参数为GL_TEXTURE_MIN_FILTER(这个参数的意思是,当我们绘制远距离的对象的时候,我们会把纹理缩小)和GL_NEAREST(这个函数的意思是,当绘制顶点的时候,选择最临近的纹理像素)。
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
//最后一步,把像素中的数据发送给OpenGL,通过调用glTexImage2D.当你调用这个函数的时候,你需要指定像素格式。这里我们指定的是GL_RGBA和GL_UNSIGNED_BYTE.它的意思是,红绿蓝alpha道具都有,并且他们占用的空间是1个字节,也就是每个通道8位。
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)width, (int)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
//已经把图片数据传送给了OpenGL,所以可以把这个释放掉
free(spriteData);
return texName;
}
- (void)render {
glClearColor(150.0/255.0, 100.0/255.0, 55.0/255.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
// 设置UIView中渲染的部分
glViewport(0, 0, self.frame.size.width, self.frame.size.height);
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer);
/**
* 为vertex shader的两个输入参数配置两个合适的值。
*
* @param _positionSlot 声明这个属性的名称
* @param 3 定义这个属性由多少个值组成,定点是3个,颜色是4个
* @param GL_FLOAT 声明每一个值是什么类型
* @param GL_FALSE
* @param Vertex 描述每个vertex数据大小的方式,所以可以简单的传入
* sizeof(Vertex)
* 最后一个是数据结构的偏移量
* @return
*/
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE,
sizeof(Vertex), 0);
glVertexAttribPointer(_texCoordSlot, 2, GL_FLOAT, GL_FALSE,
sizeof(Vertex), (GLvoid *) (sizeof(float) * 3));
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _imageTexture);
glUniform1i(_textureUniform, 0);
//开启混合因子
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_DST_ALPHA);
// 在每个vertex上调用我们的vertex shader,以及每个像素调用fragment shader,最终画出我们的矩形
glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]),
GL_UNSIGNED_BYTE, 0);
[_context presentRenderbuffer:GL_RENDERBUFFER];//绘制到渲染缓冲区
}
#pragma mark - 渲染灰色块
- (void)renderGray:(NSArray *)pointArr{
@synchronized(self){
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self compileGrayShaders];
// 启动混合并设置混合因子
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
});
//色块边长
NSInteger graySideLength = 40;
for (int i = 0; i < pointArr.count; i++) {
CGPoint point = CGPointFromString([pointArr objectAtIndex:i]);
CGFloat x = point.x;
CGFloat y = point.y;
//点击位置
float clickX = 1 - (x - graySideLength / 2) / [UIScreen mainScreen].bounds.size.width;
float clickY = 1 - (y + graySideLength / 2) / [UIScreen mainScreen].bounds.size.height;
//边长在屏幕中横竖占比例
float lengthX = graySideLength / [UIScreen mainScreen].bounds.size.width;
float lengthY = graySideLength / [UIScreen mainScreen].bounds.size.height;
const GrayVertex GrayVertexs[] = {
{{1,-1,0} , {-clickX + lengthX,-clickY} , {-0,-0}},
{{1,1,0} , {-clickX + lengthX,-clickY - lengthY} , {-0,-1}},
{{-1,1,0} , {-clickX ,-clickY - lengthY} , {-1,-1}},
{{-1,-1,0}, {-clickX ,-clickY} , {-1,-0}}
};
glBindBuffer(GL_ARRAY_BUFFER, _grayVertexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _grayIndexBuffer);
//构建定点、元素数组VBO
glGenBuffers(1, &_grayVertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _grayVertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(GrayVertexs), GrayVertexs, GL_STATIC_DRAW);
glGenBuffers(1, &_grayIndexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _grayIndexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GrayIndices), GrayIndices, GL_STATIC_DRAW);
// 设置UIView中渲染的部分
// glViewport(0,
// [[UIScreen mainScreen]bounds].size.height - 200,
// (int)100,
// (int)100);
glViewport(x - graySideLength / 2,
[[UIScreen mainScreen]bounds].size.height - y - graySideLength / 2,
(int)graySideLength,
(int)graySideLength);
// NSLog(@"%f %f",x - graySideLength / 2,[[UIScreen mainScreen]bounds].size.height - y - graySideLength / 2);
glVertexAttribPointer(_grayPositionSlot, 3, GL_FLOAT, GL_FALSE,
sizeof(GrayVertex), 0);
glVertexAttribPointer(_grayTexCoordSlot, 2, GL_FLOAT, GL_FALSE,
sizeof(GrayVertex), (GLvoid *) (sizeof(float) * 3));
glVertexAttribPointer(_grayTransTexCoordSlot, 2, GL_FLOAT, GL_FALSE, sizeof(GrayVertex), (GLvoid *) (sizeof(float) * 5));
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _imageTexture);
glUniform1i(_textureUniform, 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, _transparentImageTexture);
glUniform1i(_transparentTextureUniform, 1);
glDrawElements(GL_TRIANGLES, sizeof(GrayIndices)/sizeof(GrayIndices[0]),GL_UNSIGNED_BYTE, 0);
}
[_context presentRenderbuffer:GL_RENDERBUFFER];//绘制到渲染缓冲区
}
}
//重新加载灰色块着色器
- (void)compileGrayShaders {
// 编译vertex shader 和 fragment shader
GLuint vertexShader = [self compileShader:@"GrayVertex"
withType:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShader:@"GrayFragment"
withType:GL_FRAGMENT_SHADER];
// 链接vertex 和 fragment成一个完整的Program
GLuint programHandle = glCreateProgram();
glAttachShader(programHandle, vertexShader);
glAttachShader(programHandle, fragmentShader);
glLinkProgram(programHandle);
// 输出错误信息
GLint linkSuccess;
glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(programHandle, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"%@", messageString);
return;
}
// 执行program
glUseProgram(programHandle);
// 获取指向vertex shader传入变量的指针。以后可以通过这个写指针来使用。调用glEnableVertexAttribArray来启用数据
_grayPositionSlot = glGetAttribLocation(programHandle, "grayPosition");
glEnableVertexAttribArray(_grayPositionSlot);
_grayTexCoordSlot = glGetAttribLocation(programHandle, "grayTexCoordIn");
glEnableVertexAttribArray(_grayTexCoordSlot);
_grayTransTexCoordSlot = glGetAttribLocation(programHandle, "grayTransTexCoordIn");
glEnableVertexAttribArray(_grayTransTexCoordSlot);
_textureUniform = glGetUniformLocation(programHandle, "grayTexture");
_transparentTextureUniform = glGetUniformLocation(programHandle, "grayTransparentTexture");
}
/**
* 获取图片里面的像素数据
*/
- (GLuint)setupTexture2:(NSString *)imageName{
//初始化一个UIImage对象,然后获得它的CGImage属性
//图片规格有限制 只能用2次方的大小的图
CGImageRef spriteImage = [UIImage imageNamed:imageName].CGImage;
if (!spriteImage) {
NSLog(@"load image failed");
exit(1);
}
//获取image的宽度和高度然后手动分配空间 width*height*4个字节的数据空间
//空间*4的原因是,我们在调用方法来绘制图片数据时,我们要为red,green,blue和alpha通道,每个通道要准备一个字节
//每个通道准备一个字节的原因,因为要用CoreGraphics来建立绘图上下文。而CGBitmapContextCreate函数里面的第4个参数指定的就是每个通道要采用几位来表现,我们只用8位,所以是一个字节
// NSInteger width = CGImageGetWidth(spriteImage);
// NSInteger height = CGImageGetHeight(spriteImage);
NSInteger width = 64;
NSInteger height = 64;
GLubyte *spriteData = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width * 4, CGColorSpaceCreateDeviceRGB(), kCGImageAlphaPremultipliedLast);
//告诉Core Graphics在一个指定的矩形区域内来绘制这些图像
CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
//完成绘制要释放
CGContextRelease(spriteContext);
//把像素信息发送给OpenGL,首先调用glGenTextures来创建一个纹理对象,并且得到一个唯一的ID,由"name"保存着。然后,我们调用glBindTexture来把我们新建的纹理名字加载到当前的纹理单元中。
GLuint texName2;
glGenTextures(2, &texName2);
glBindTexture(GL_TEXTURE_2D, texName2);
glUniform1i(_transparentTextureUniform, 1);
//接下来的步骤是,为我们的纹理设置纹理参数,使用glTexParameterf函数,。这里我们设置函数参数为GL_TEXTURE_MIN_FILTER(这个参数的意思是,当我们绘制远距离的对象的时候,我们会把纹理缩小)和GL_NEAREST(这个函数的意思是,当绘制顶点的时候,选择最临近的纹理像素)。
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
//最后一步,把像素中的数据发送给OpenGL,通过调用glTexImage2D.当你调用这个函数的时候,你需要指定像素格式。这里我们指定的是GL_RGBA和GL_UNSIGNED_BYTE.它的意思是,红绿蓝alpha道具都有,并且他们占用的空间是1个字节,也就是每个通道8位。
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)width, (int)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
//已经把图片数据传送给了OpenGL,所以可以把这个释放掉
free(spriteData);
return texName2;
}
代码里面注释都还挺详细的,这里主要讲几个点
1、一般来说在第二次绘制,将制定区域绘制为灰色的时候,绘制出来的是一个矩形,这样的话连续的绘制之后会出现非常明显的锯齿效果。如果要去掉这个锯齿效果的话只好是采用混合模式,使用一个内部是白色圆形,外部是黑色的图片进行混合,更好的情况是圆形的边缘有个透明度的渐变。这样绘制出来的效果就很自然了。
2、在渲染灰色块的时候,是从外部创建一个贝塞尔曲线,然后将点传进来进行绘制。如果是每个点都调用一次
- (void)renderGray:(NSArray *)pointArr
会产生严重的闪烁,所以处理的时候需要将每一条曲线的点进行集中绘制到屏幕上
for (int i = 0; i < pointArr.count; i++)
所以在上面的函数里面有个这样的循环
3、在这里面其实也实现了一个简单的放大镜的功能
// 设置UIView中渲染的部分
// glViewport(0,
// [[UIScreen mainScreen]bounds].size.height - 200,
// (int)100,
// (int)100);
glViewport(x - graySideLength / 2,
[[UIScreen mainScreen]bounds].size.height - y - graySideLength / 2,
(int)graySideLength,
(int)graySideLength);
在这里面把上一段注释打开,下一段给注释掉就是一个图片局部位置的放大镜了,其实原理很简单,就是获取手指点击位置范围的图片色值,然后在指定绘制位置进行绘制,然后把绘制范围放大。自然就成了放大镜啦 0.o
4、最后,如果代码里面有些概念基础不太懂的,大家可以先试着查查资料,这样以后也记得清楚些,也更有成就感一些啦。然后,如果还有上面疑惑的地方,还请提出来大家相互讨论下。
5、关于本文着色器脚本的编写,还挺简单的,一个是将原图绘制出来,一个是把色值规定成灰色然后绘制出来。挺简单的,稍微搜索下就能写了。就不特别贴出来了哈。