持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
上一篇我们使用 EAGLContext 和 GLKBaseEffect 加载显示了一张图片。
今天,我们使用 GLSL 自定义着色器来 加载显示一张图片。
我们不采用GLKBaseEffect,使用编译链接自定义的着色器(shader)。用简单的glsl语言来实现顶点、片元着色器,并图形进行简单的变换。
大体思路上我们可以通过一下6步 来完成:
- 创建图层
- 创建上下文
- 清空缓存区
- 设置RenderBuffer
- 设置FrameBuffer
- 开始绘制
首先,我们自定义一个 SMView 继承自 UIView
自定义View
SMView 有一下属性
@interface SMView ()
// 在iOS和tvOS上绘制OpenGL ES 内容的涂层,继承自CALayer
@property (nonatomic, strong) CAEAGLLayer *myEagLayer;
@property (nonatomic, strong) EAGLContext *myContext;
@property (nonatomic, assign) GLuint myColorRenderBuffer;
@property (nonatomic, assign) GLuint myColorFrameBuffer;
@property (nonatomic, assign) GLuint myPrograme;
@end
实现我们的六步走
创建图层
首先, 创建特殊图层 (通过重写layerClass,将SMView返回的图层从CALayer替换成CAEAGLLayer);
接着, 设置描述属性,这里设置不维持渲染内容以及颜色格式为RGBA8;
kEAGLDrawablePropertyRetainedBacking 表示绘图表面显示后,是否保留其内容。 kEAGLDrawablePropertyColorFormat 可绘制表面的内部颜色缓存区格式,这个key对应的值是一个NSString指定特定颜色缓存区对象。默认是kEAGLColorFormatRGBA8;
kEAGLColorFormatRGBA8:32位RGBA的颜色,4*8=32位
kEAGLColorFormatRGB565:16位RGB的颜色,
kEAGLColorFormatSRGBA8:sRGB代表了标准的红、绿、蓝,即CRT显示器、LCD显示器、投影机、打印机以及其他设备中色彩再现所使用的三个基本色素。sRGB的色彩空间基于独立的色彩坐标,可以使色彩在不同的设备使用传输中对应于同一个色彩坐标体系,而不受这些设备各自具有的不同色彩坐标的影响。
#pragma mark -- 设置图层
- (void)setupLayer {
self.myEagLayer = (CAEAGLLayer *)self.layer;
// 设置scale
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat, nil];
}
+(Class)layerClass {
return [CAEAGLLayer class];
}
设置图形上下文
- 设置我们使用的OpenGL ES 渲染API的版本;
- 创建图形上下文
EAGLContext; - 将创建的图形上下文设置为当前的图形上下文;
#pragma mark -- 设置上下文
- (void)setupContext {
// 指定OpenGL ES 渲染API版本
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES3;
// 创建图形上下文
EAGLContext *context = [[EAGLContext alloc] initWithAPI:api];
// 判断是否创建成功
if (!context) {
NSLog(@"create context failed!");
return;
}
//设置图形上下文
if (![EAGLContext setCurrentContext:context]) {
NSLog(@"setcurrentcontext failed!");
return;
}
// 将局部context,变成全局的
self.myContext = context;
}
清空缓存区
- buffer分为frame buffer 和 render buffer2个大类。
- 其中frame buffer 相当于render buffer的管理者。
- frame buffer object即称FBO。
- render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。
#pragma mark -- 清空缓存区
- (void)deleteRenderAndFrameBuffer {
glDeleteBuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
glDeleteBuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;
}
设置RenderBuffer
- 定义一个缓存区ID;
- 申请一个缓存区标志;
- 将标识符绑定到 GL_RENDERBUFFER;
- 将可绘制对象 drawable object‘s GAEAGLLayer 的存储绑定到 OpenGL ES renderBuffer对象
#pragma mark -- 设置RendderBuffer
- (void)setupRenderBuffer {
GLuint buffer;
glGenRenderbuffers(1, &buffer);
self.myColorRenderBuffer = buffer;
glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
设置FrameBuffer
- 定义一个缓存区ID
- 申请一个缓存区标志
- 将标识符绑定到 GL_FRAMEBUFFER
- 将渲染缓存区myColorRenderBuffer, 通过glFramebufferRenderbuffer函数绑定到GL_COLOR_ATTACHMENT0上
#pragma mark -- 设置FrameBuffer
- (void)setupFrameBuffer {
GLuint buffer;
glGenBuffers(1, &buffer);
self.myColorFrameBuffer = buffer;
glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}
开始绘制
- 设置清除屏幕颜色;将屏幕清除;
- 设置适口的大小;
- 读取顶点着色器程序、片源着色器程序;
- 加载shader;
- 链接我们的 myPrograme
- 获取链接的状态,看是否有错误信息
- 使用我们的 myPrograme
- 设置 顶点、纹理坐标 (前三个是顶点坐标,后两个是纹理坐标)
- 处理顶点数据 (顶点缓存区;申请一个缓存表区标识符;将attrBuffer绑定到GL_ARRAY_BUFFER标识符上;把顶点数据从CPU内存复制到GPU上)
- 将顶点数据通过myPrograme中的传递到顶点着色程序的position (glGetAttribLocation,用来获取vertex attribute 的入口的;告诉OpenGL ES,通过glEnableVertexAttribArray;最后数据是通过glVertexAttribPointer传递过去的)
- 加载纹理
- 设置纹理采样器
- 绘图
- 从渲染缓存区显示到屏幕上
#pragma mark -- 开始绘制
- (void)renderLayer {
// 设置清屏颜色
glClearColor(0.3f, 0.4f, 0.5f, 1.0f);
// 清除屏幕
glClear(GL_COLOR_BUFFER_BIT);
// 1.设置视口大小
CGFloat scale = [[UIScreen mainScreen] scale];
glViewport(self.frame.origin.x*scale,
self.frame.origin.y*scale,
self.frame.size.width*scale,
self.frame.size.height*scale);
// 2.读取顶点着色程序、 片源着色程序
NSString *vertFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fragFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
NSLog(@"vertFile:%@", vertFile);
NSLog(@"fragFile:%@", fragFile);
// 3.加载shader
self.myPrograme = [self loadShaders:vertFile withFrag:fragFile];
// 4.链接
glLinkProgram(self.myPrograme);
GLint linkStatus;
// 获取链接状态
glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
GLchar message[500];
glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
NSString *messageString = [NSString stringWithUTF8String:message];
NSLog(@"Program Link Error:%@", messageString);
return;
} else {
NSLog(@"Program Link Success!");
}
// 5.使用program
glUseProgram(self.myPrograme);
// 6.设置顶点、纹理坐标 -- 前三个是顶点坐标,后两个是纹理坐标
GLfloat attrArr[] = {
0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
};
// 7.处理顶点数据
// (1)顶点缓存区
GLuint attrBuffer;
// (2)申请一个缓存区标识符
glGenBuffers(1, &attrBuffer);
// (3)将attrBuffer绑定到GL_ARRAY_BUFFER标识符上
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
// (4)把顶点数据从 CPU 内存复制到 GPU 上
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
//8.将顶点数据通过myPrograme中的传递到顶点着色程序的position
//(1)glGetAttribLocation,用来获取vertex attribute 的入口的
//(2)告诉OpenGL ES,通过glEnableVertexAttribArray,
//(3)最后数据是通过glVertexAttribPointer传递过去的
// 注意 :第二参数字符串必须和 shaderv.vsh 中的输入变量:position保持一致
GLuint position = glGetAttribLocation(self.myPrograme, "position");
// 设置合适的格式从buffer里面读取数据
glEnableVertexAttribArray(position);
// 设置读取方式
//参数1:index,顶点数据的索引
//参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
//参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
//参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
//参数5:stride,连续顶点属性之间的偏移量,默认为0;
//参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
// 9.处理纹理数据
//(1)glGetAttribLocation,用来获取vertex attribute 的入口的
// 注意:第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate 保持一致
GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
//(2)设置合适的格式从buffer里面读取数据
glEnableVertexAttribArray(textCoor);
//(3).设置读取方式
//参数1:index,顶点数据的索引
//参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
//参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
//参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
//参数5:stride,连续顶点属性之间的偏移量,默认为0;
//参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL+3);
//10.加载纹理
[self setupTexture:@"IMG_0068"];
//11.设置纹理采样器
glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);
//12.绘图
glDrawArrays(GL_TRIANGLES, 0, 6);
//13.从渲染缓存区显示到屏幕上
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
}
顶点着色器文件
我们定义 一个 shaderv.vsh 文件 ;用来写我们的顶点着色器的代码;
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main()
{
varyTextCoord = textCoordinate;
gl_Position = position;
}
片源着色器文件
我们定义 一个 shaderf.fsh 文件 ;用来写我们的顶点着色器的代码;
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main()
{
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
加载shader
- 定义两个临时着色器对象
- 创建program
- 编译顶点着色程序、片元着色程序
- 创建最终的程序
- 释放不需要的shader
#pragma mark -- 加载shader
- (GLuint)loadShaders:(NSString *)vert withFrag:(NSString *)frag {
GLuint verShader, fragShader;
GLuint program = glCreateProgram();
// 编译顶点着色程序、片元着色程序
//参数1:编译完存储的底层地址
//参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
//参数3:文件路径
[self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
glAttachShader(program, verShader);
glAttachShader(program, fragShader);
glDeleteShader(verShader);
glDeleteShader(fragShader);
return program;
}
编译顶点着色程序、片元着色程序
- 首先读取到文件路径,加载文件内容;
- 创建shader根据type类型;
- 将着色器源码附加到着色器对象上;
- 最后把着色器代码编译成目标代码
#pragma mark -- 编译shader
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file {
NSString *content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar *source = (GLchar *)[content UTF8String];
*shader = glCreateShader(type);
//将着色器源码附加到着色器对象上
//参数1:shader,要编译的着色器对象 *shader
//参数2:numOfStrings,传递的源码字符串数量 1个
//参数3:strings,着色器程序的源码(真正的着色器程序源码)
//参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
glShaderSource(*shader, 1, &source, NULL);
glCompileShader(*shader);
}
加载纹理
#pragma mark -- 从图片中加载纹理
- (GLuint)setupTexture:(NSString *)fileName {
// 将 UIImage 转换为 CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
// 判断图片是否获取成功
if (!spriteImage) {
NSLog(@"failed to load image %@", fileName);
exit(1);
}
// 读取图片的大小,宽和高
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
// 获取图片字节数 宽*高*4(RGBA)
GLubyte *spriteData = (GLubyte *)calloc(width*height*4, sizeof(GLubyte));
//创建上下文
/*
参数1:data,指向要渲染的绘制图像的内存地址
参数2:width,bitmap的宽度,单位为像素
参数3:height,bitmap的高度,单位为像素
参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
参数5:bytesPerRow,bitmap的没一行的内存所占的比特数
参数6:colorSpace,bitmap上使用的颜色空间 kCGImageAlphaPremultipliedLast:RGBA
*/
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
// 在CGContextRef上 --> 将图片绘制出来
/*
CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。
CGContextDrawImage
参数1:绘图上下文
参数2:rect坐标
参数3:绘制的图片
*/
CGRect rect = CGRectMake(0, 0, width, height);
// 使用默认方式绘制
CGContextDrawImage(spriteContext, rect, spriteImage);
// 画图完毕就释放上下文
CGContextRelease(spriteContext);
// 绑定纹理到默认的纹理
glBindTexture(GL_TEXTURE_2D, 0);
// 设置纹理属性
/*
参数1:纹理维度
参数2:线性过滤、为s,t坐标设置模式
参数3:wrapMode,环绕模式
*/
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
float fw = width, fh = height;
// 载入纹理2D数据
/*
参数1:纹理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
参数2:加载的层次,一般设置为0
参数3:纹理的颜色值GL_RGBA
参数4:宽
参数5:高
参数6:border,边界宽度
参数7:format
参数8:type
参数9:纹理数据
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
free(spriteData);
return 0;
}
显示到VC
将我们自定义的SMView 添加到VC 到view 上,就可以看到我们 家崽的图片了。
问题
图片此刻是倒置显示的,为什么呢?下一篇,我们来探讨下……