概述
GLSL(OpenGL Shading Language)是一种用于OpenGL的高级过程着色语言,开发者可以使用GLSL自定义编写顶点着色器和片元着色器实现图形效果。本文将使用GLSL自定义着色器着iPhone设备上渲染一张图片。
相关环境配置
开发工具
Xcode 11.4.1
系统环境
macOS Catalina 10.15.6 / iOS 13.4.1
使用语言
OC/C/GLSL
着色器编写
为了方便查看和维护,我们一般把着色器单独编写在一个独立的文件,我们新建一个shaderv.vsh文件编写顶点着色器源码,shaderf.fsh文件编写片元着色器源码。
Xcode无法编译着色器源码,所以“vsh“和”fsh“只是作为标示区分顶点着色器和片元着色器,并无其他意义。
着色器源码中不要出现中文,避免无法预知的错误
shderv.vsh中:
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main()
{
varyTextCoord = textCoordinate;
gl_Position = position;
}
attribute vec4 position; // 声明一个名为
position
、类型为4分量浮点向量的attribute
通道,用来传递顶点坐标数据
attribute vec2 textCoordinate; // 声明一个名为textCoordinate
、类型为2分量浮点向量的attribute
通道,用来传递纹理坐标数据,由于片元着色器没有attribute通道,所以需要从顶点着色器中传入
varying lowp vec2 varyTextCoord; // 声明一个名为varyTextCoord
、 低精度(因为纹理坐标范围是0~1)、类型为2分量浮点向量的变量,这个变量作用是将纹理坐标传递到片元着色器中
varyTextCoord = textCoordinate; // 将纹理坐标传递到片元着色器
gl_Position = position; // 传递顶点坐标给OpenGL ES,gl_Position是OpenGL ES的内建变量,用来传递顶点数据。
shderf.fsh中:
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main()
{
lowp vec4 temp = texture2D(colorMap, varyTextCoord);
gl_FragColor = temp;
}
precision highp float; 指定float为高精度
varying vec2 varyTextCoord;纹理坐标,从顶点着色器中传入,参数名称和类型一定要和顶点着色器中的纹理坐标参数一模一样
uniform sampler2D colorMap;纹理,通过uniform通道传入
lowp vec4 temp = texture2D(colorMap, varyTextCoord);获取纹理对应坐标下的纹素,及纹理坐标对象像素点的颜色值,texture2D()
是OpenGL ES的一个内建函数,用来获取纹素,他需要传入两个参数,第一个传递纹理,第二个传递纹理坐标,返回对应坐标的颜色值。
gl_FragColor = temp;和gl_Position
一样,gl_FragColor
时OpenGL ES的一个内建变量,用来放回颜色值。
顶点着色器的执行次数和顶点数量有关,比如10个顶点那么顶点着色器会执行10次,而片元着色器的执行次数和像素点数量有关,绘制一个100*100的图片片元着色器会志晖行100000次,顶点着色器和片元着色器的执行都是在GPU中执行,所以执行效率会很高,但是模拟器中并没有着色器,执行的时候时CPU在模拟GPU的执行。
Client代码
客户端代码的大体执行流程为:
- 创建CAEAGLLayer
- 创建上下文
- 清空缓冲区
- 设置RenderBuffer
- 设置FrameBuffer
- 开始绘制
首先创建一个ESView,并将它的layer指定CAEAGLLayer来展示绘制的结果
ESView初始化
ESView.m 中
// 导入入相关头文件
#import "ESView.h"
#import <OpenGLES/ES3/gl.h>
// 声明相关属性
@interface ESView ()
@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
指定layer为CAEAGLLayer
+ (Class)layerClass {
return CAEAGLLayer.class;
}
在layoutSubviews中开始绘制流程
- (void)layoutSubviews {
// 1. 创建CAEAGLLayer
[self setupLayer];
// 2. 创建上下文
[self setupContext];
// 3. 清空缓冲区
[self deleteRenderAndFrameBuffer];
// 4. 设置RenderBuffer
[self setupRenderBuffer];
// 5. 设置FrameBuffer
[self setupFrameBuffer];
// 6. 开始绘制
[self renderLayer];
}
创建CAEAGLLayer、创建上下文、清空缓冲区
前三步比较简单,直接上代码
// 获取layer
self.myEagLayer = (CAEAGLLayer *)self.layer;
// 设置scale
CGFloat scale = UIScreen.mainScreen.scale;
[self setContentScaleFactor:scale];
// 设置参数
self.myEagLayer.drawableProperties =
@{kEAGLDrawablePropertyRetainedBacking: @false,
kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
- (void)setupContext {
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
if (!context) {
NSLog(@"context create fail");
return;
}
// 设置当前上下文为全局上下文
if (![EAGLContext setCurrentContext:context]) {
NSLog(@"set current context fail");
return;
}
self.myContext = context;
}
- (void)deleteRenderAndFrameBuffer {
glDeleteRenderbuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
glDeleteFramebuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;
}
设置RenderBuffer和FrameBuffer
关于renderbuffer和framebuffer的关系,可以将FrameBuffer看作RenderBuffer的管理者,一个renderbuffer对象是通过应用分配的一个2D图像缓存区,它能够用来分配和存储颜色、深度或者模版值,也能够在一个framebuffer里作为颜色、深度、模版的附件,一个renderbuffer类似于窗口系统提供的可绘制的表面,比如pBuffer。而一个framebuffer对象(FBO)提供了许多附着点,他虽然有着缓冲区的字眼,但他更像一个容器,他可以保存可以进行渲染的对象,比如渲染缓冲区,采用这种方式,FBO能够在保存OpenGL
管线的输出时将需要的状态和表面绑定在一起。
设置渲染缓冲区
- (void)setupRenderBuffer {
// 申请一个缓冲区标示
GLuint buffer;
glGenRenderbuffers(1, &buffer);
// 绑定buffer到GL_RENDERBUFFER
glBindRenderbuffer(GL_RENDERBUFFER, buffer);
self.myColorRenderBuffer = buffer;
// 将layer的存储绑定到renderbuffer
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
设置帧缓冲区
- (void)setupFrameBuffer {
// 申请一个缓冲区标示
GLuint buffer;
glGenFramebuffers(1, &buffer);
// 绑定
glBindFramebuffer(GL_FRAMEBUFFER, buffer);
self.myColorFrameBuffer = buffer;
//将渲染缓冲区绑定到frameBuffer的GL_COLOR_ATTACHMENT0上
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorFrameBuffer);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status == GL_FRAMEBUFFER_COMPLETE) {
NSLog(@"framebuffer complete");
} else {
NSLog(@"not complete");
}
}
开始绘制
绘制主要流程如下:
- 设置清屏颜色
- 清空缓冲区
- 设置视口
- 加载顶点着色器和片元着色器
- 设置顶点坐标和纹理坐标
- 将顶点坐标从内存保存到顶点缓冲区
- 将顶点坐标和纹理坐标传递到着色器
- 加载纹理
- 将纹理数据传递到着色器
- 开始绘制(指定图元连接方式)
- 提交渲染
具体代码如下
- (void)renderLayer {
// 设置清屏颜色
glClearColor(1, 0, 0, 1.0);
// 清空
glClear(GL_COLOR_BUFFER_BIT);
// 设置视口大小
CGFloat scale = UIScreen.mainScreen.scale;
CGRect rect = self.frame;
glViewport(rect.origin.x*scale, rect.origin.y*scale, rect.size.width*scale, rect.size.height*scale);
// 读取顶点着色器、片元着色器
NSString *vertFile = [NSBundle.mainBundle pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fragFile = [NSBundle.mainBundle pathForResource:@"shaderf" ofType:@"fsh"];
// 加载着色器生成program
self.myPrograme = [self loadShaders:vertFile withFrag:fragFile];
// 链接program
glLinkProgram(self.myPrograme);
// 获取链接状态
GLint linkStatus;
glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
if (GL_FALSE == linkStatus) {
// 获取错误信息
GLchar message[512];
glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
NSString *msgStr = [NSString stringWithUTF8String:message];
NSLog(@"link program failure: %@", msgStr);
return;
}
// 使用program
glUseProgram(self.myPrograme);
//设置顶点
GLfloat attArr[] = {
-1.0, -0.5, 0.0, 0.0, 0.0,
1.0, -0.5, 0.0, 1.0, 0.0,
1.0, 0.5, 0.0, 1.0, 1.0,
-1.0, 0.5, 0.0, 0.0, 1.0,
};
// 将顶点数据从内存保存到显存中
// 申请一个缓冲区标示
GLuint attBuffer;
glGenBuffers(1, &attBuffer);
// 绑定
glBindBuffer(GL_ARRAY_BUFFER, attBuffer);
// 传递到显存
glBufferData(GL_ARRAY_BUFFER, sizeof(attArr), attArr, GL_DYNAMIC_DRAW);
// 将数据传递到着色器
// 获取position通道
GLuint position = glGetAttribLocation(self.myPrograme, "position");
//苹果默认关闭attribute通道,需要打开通道
glEnableVertexAttribArray(position);
// 将顶点坐标传递到顶点着色器
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
// 传递纹理坐标到着色器
GLuint textCoordinate = glGetAttribLocation(self.myPrograme, "textCoordinate");
glEnableVertexAttribArray(textCoordinate);
glVertexAttribPointer(textCoordinate, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (GLfloat *)NULL + 3);
// 加载纹理
[self setupTexture:@"benzema"];
//获取颜色uniform通道
GLuint colorMap = glGetUniformLocation(self.myPrograme, "colorMap");
//设置纹理采样器
// 0 标示默认
glUniform1f(colorMap, 0);
// 绘图
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
// 从渲染缓冲区显示到屏幕上
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
}
加载着色器
加载着色器的过程如下:
- 获取着色器源码
- 将着色器源码附着到着色器对象
- 编译着色器
- 将着色器连接到program
- 链接program
- 使用program
具体代码如下:
// 加载顶点着色器和片元着色器,返回program
- (GLuint)loadShaders:(NSString *)vert withFrag:(NSString *)frag {
// 定义两个临时着色器对象
GLuint vertShader, fragShader;
// 创建program
GLuint program = glCreateProgram();
// 编译着色器
[self compileShader:&vertShader type:GL_VERTEX_SHADER file:vert];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
// 将着色器连接到program
glAttachShader(program, vertShader);
glAttachShader(program, fragShader);
// 释放不需要的shader
glDeleteShader(vertShader);
glDeleteShader(fragShader);
return program;
}
// 编译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
*shader = glCreateShader(type);
//将着色器源码附着到shader
//参数1:shader,要编译的着色器对象 *shader
//参数2:numOfStrings,传递的源码字符串数量 1个
//参数3:strings,着色器程序的源码(真正的着色器程序源码)
//参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
glShaderSource(*shader, 1, &source, NULL);
//编译着色器源码
glCompileShader(*shader);
}
加载纹理
加载纹理的过程为:
- 获取图片数据
- 创建一个纹理id
- 绑定纹理id
- 设置纹理属性
- 载入纹理2D数据到纹理缓冲区
- 释放图片数据
而通过我所能拿到的是UIImage对象,我们可以使用CoreGraphics框架获得纹理数据,具体流程为
- 通过UIImage获得CGImageRef对象
- 创建上下文
- 创建一个大小为CGImageRef大小的内存空间
- 使用CGContextDrawImage重绘获得纹理数据
- 释放上下文
具体代码如下:
- (GLuint)setupTexture:(NSString *)fileName {
// 将uiimage转化为cgimagref
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
if(!spriteImage) {
NSLog(@"failed to load image %@", fileName);
exit(1);
}
// 使用CoreGraphics重绘获取位图数据
// 读取图片的大小
size_t width = CGImageGetWidth(spriteImage);
size_t heith = CGImageGetHeight(spriteImage);
// 创建空间存放数据
GLubyte *spriteData = calloc(width*heith*4, sizeof(GLubyte));
// 创建上下文
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, heith, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
// 绘制图片
CGRect rect = CGRectMake(0, 0, width, heith);
CGContextDrawImage(spriteContext, rect, spriteImage);
// 释放上下文
CGContextRelease(spriteContext);
// 绑定纹理到默认id
glBindBuffer(GL_TEXTURE_2D, 0);
// 设置纹理属性
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// 载入纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (float)width, (float)heith, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
// 释放纹理
free(spriteData);
return 0;
}
执行
最终显示的结果为
15962994997321.jpg
可以看到图片虽然显示成功,但是是上下颠倒的,具体原因将在下一篇文章作出解释和解决。
网友评论