前言
前面的基础文章列表
iOS-零基础学习OpenGL ES入门教程(一)
iOS-OpenGL ES入门教程(二)最简单的纹理Demo
iOS-OpenGL ES入门教程(三)纹理取样,混合,多重纹理
iOS-OpenGL ES入门教程(四)光照
写在前面:OpenGLES系列是自己在做音视频时候边学边记录的文章 距离最初记录已经三年有余,现在回头看补齐这个当初的学习系列也算是一种回溯吧,学习总是让人开心的,由陌生到熟练再到更进一步精通的过程充满着乐趣。希望自己抽出时间尽快将这个基础系列补齐。如今虽然OpenGL在iOS上已经被苹果废弃了,不过图形框架的基础原理和思路还是很相像的,熟悉opengl再熟悉metal会更快上手
初识GLSL
之前的章节提到过 我们使用GLKit框架所以避开了写繁琐的GLSL
GLSL是一个以C语言为基础的高阶着色语言 通过编写glsl 提供开发者对绘图管线更多的直接控制,而无需使用汇编语言或硬件规格语言
我们来看下怎么用GLSL
同样是一个最简单的绘制一张图片应用
准备绘制工作
不同于GLkit 我们直接使用GLkitView 我们先自定义一个UIView的子类 作为opengl绘制的载体View
@interface GLSLDemoView : UIView
@end
重载layerClass 指定为CAEAGLLayer iOS下只有CAEAGLLayer才支持opengl的绘制
+ (Class)layerClass{
return [CAEAGLLayer class];
}
初始化opengl上下文和配置好CAEAGLLayer的相关属性
//设置Layer
self.mEagLayer = (CAEAGLLayer *) self.layer;
self.contentScaleFactor = [UIScreen mainScreen].scale;
self.mEagLayer.opaque = YES;
self.mEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
//设置openglES 上下文
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
EAGLContext *context = [[EAGLContext alloc]initWithAPI:api];
if (!context) {
NSLog(@"error to init openglES context");
}
if (![EAGLContext setCurrentContext:context]) {
NSLog(@"error to set current openglES context");
}
self.mContext = context;
绑定framebuffer和renderbuffer
//Render Buffer
GLuint renderBuffer;
glGenRenderbuffers(1, &renderBuffer);
self.mColorRenderBuffer = renderBuffer;
glBindRenderbuffer(GL_RENDERBUFFER, self.mColorRenderBuffer);
[self.mContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.mEagLayer];
//Frame Buffer
GLuint frameBuffer;
glGenFramebuffers(1, &frameBuffer);
self.mColorFrameBuffer = frameBuffer;
glBindFramebuffer(GL_FRAMEBUFFER, self.mColorFrameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.mColorRenderBuffer);
常规的opengl渲染管线的准备工作 接下来 我们就要开始加载我们自己的shader(着色器),
之前我们提过opengl的渲染管线中
Vetex shader 顶点着色器
Fragment shader 片元着色器
加载着色器
着色器自然是一种编程语言 我们首先要做的是编写一个最简单的着色器 了解下着色器语法
GLSL的官方文档:https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.1.20.pdf
这里介绍下最简单的修饰符
const:常量值必须在声明是初始化。它是只读的不可修改的。
attribute:表示只读的顶点数据,只用在顶点着色器中。数据来自当前的顶点状态或者顶点数组。它必须是全局范围声明的,不能在函数内部。一个attribute可以是浮点数类型的标量,向量,或者矩阵。不可以是数组或者结构体
uniform:一致变量。在着色器执行期间一致变量的值是不变的。与const常量不同的是,这个值在编译时期是未知的是由着色器外部初始化的。一致变量在顶点着色器和片段着色器之间是共享的。它也只能在全局范围进行声明。
varying:顶点着色器的输出。例如颜色或者纹理坐标,(插值后的数据)作为片段着色器的只读输入数据。必须是全局范围声明的全局变量。可以是浮点数类型的标量,向量,矩阵。不能是数组或者结构体。
最常用的内置变量
gl_Position:vec4类型 输出属性-变换后的顶点的位置,用于后面的固定的裁剪等操作。所有的顶点着色器都必须写这个值。
gl_FragColor:vec4类型 片元着色器的输出颜色 可用于后续使用
知道了这些最简单的修饰符和内置变量 我们就可以编写起自己的shader啦
首先先编写顶点着色器:(一般顶点着色器文件以.vsh结尾)
attribute vec4 position;
attribute vec2 textCoordinate;
uniform mat4 rotateMatrix;
varying lowp vec2 varyTextCoord;
void main()
{
varyTextCoord = textCoordinate;
vec4 vPos = position;
vPos = vPos * rotateMatrix;
gl_Position = vPos;
}
rotateMatrix对应的旋转矩阵 可以用来向量运算 旋转矩阵
varing 定义了 varyTextCoord 用来传递纹理坐标给片元着色器
这里的lowp表示是精度 精度要求越低 性能消耗越低
gl_Position输出属性-变换后的顶点的位置 这里做了个最简单的矩阵变换 vPos * rotateMatrix
接下来编写片元着色器
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main()
{
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
sampler2D 二维纹理的句柄也就是当前绑定的纹理 这里我们根据顶点着色器传入的纹理坐标 仅仅做个texture2D 取色 返回纹理对应坐标的颜色
这样我们就编写好了顶点和片元shader 接下来我们来代码加载shader
+ (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file{
NSError *error;
NSString *content = [NSString stringWithContentsOfFile:file
encoding:NSUTF8StringEncoding
error:&error];
if (error) {
NSLog(@"加载shader 本地file失败");
return;
}
const GLchar *source = (GLchar *)[content UTF8String];
*shader = glCreateShader(type);
glShaderSource(*shader, 1, &source, NULL);
glCompileShader(*shader);
}
+ (GLuint)loadShaders:(NSString *)vshFilePath
fsh:(NSString *)fshFilePath{
GLuint vShader,fShader;
GLint program = glCreateProgram();
//编译
[self compileShader:&vShader type:GL_VERTEX_SHADER file:vshFilePath];
[self compileShader:&fShader type:GL_FRAGMENT_SHADER file:fshFilePath];
glAttachShader(program, vShader);
glAttachShader(program, fShader);
glDeleteShader(vShader);
glDeleteShader(fShader);
return program;
}
NSString* vertFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString* fragFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
self.mProgram = [GLShaderUtils loadShaders:vertFile fsh:fragFile];
编译和绑定好了shader之后 我们开始link shader 其实这正是对应JIT类型的语言的编译 链接 运行
+ (BOOL)linkProgram:(GLuint)program{
glLinkProgram(program);
GLint linkRet;
glGetProgramiv(program, GL_LINK_STATUS, &linkRet);
if (linkRet == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"error%@", messageString);
return NO;
}
else{
return YES;
}
}
如果shader写的有误 这里会报错 不过相比于xcode这些编译器的报错 给到的debug信息比较弱
可以看到 加载shader是比较常规的固定代码流程 适合封装到工具方法中 方便复用
接下来就是创建VBO 也就是顶点 并且绑定到我们的shader上 相当于将数据从cpu拷贝到gpu中
GLfloat attrArr[] =
{
1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
};
GLuint attrBuffer;
glGenBuffers(1, &attrBuffer);
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
GLuint position = glGetAttribLocation(self.mProgram, "position");
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
glEnableVertexAttribArray(position);
GLuint textCoor = glGetAttribLocation(self.mProgram, "textCoordinate");
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
glEnableVertexAttribArray(textCoor);
这里的关键点glGetAttribLocation(self.mProgram, "position") 和 glGetAttribLocation(self.mProgram, "textCoordinate") 正是对应的着色器代码中的position和textCoordinate输入变量
接下来绑定纹理
CGImageRef image = [UIImage imageNamed:imageName].CGImage;
if (!image) {
NSLog(@"load Image texture failed");
return;
}
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
GLubyte * imageData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
CGContextRef imageContext = CGBitmapContextCreate(imageData, width, height, 8, width*4,
CGImageGetColorSpace(image), kCGImageAlphaPremultipliedLast);
CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), image);
CGContextRelease(imageContext);
glBindTexture(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);
float fw = width, fh = height;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
glBindTexture(GL_TEXTURE_2D, 0);
free(imageData);
我们还有个旋转矩阵变量需要设置输入到着色器中
GLuint rotate = glGetUniformLocation(self.mProgram, "rotateMatrix");
float radians = 180 * 3.14159f / 180.0f; //旋转180度让图片正向
float s = sin(radians);
float c = cos(radians);
//z轴旋转矩阵
GLfloat zRotation[16] = {
c,-s,0,0,
s,c,0,0,
0,0,1,0,
0,0,0,1
};
//设置旋转矩阵
glUniformMatrix4fv(rotate, 1, GL_FALSE, (GLfloat *)&zRotation[0]);
最后一步绘制
glDrawArrays(GL_TRIANGLES, 0, 6);
[self.mContext presentRenderbuffer:GL_RENDERBUFFER];
这样我们最简单的图片绘制就完成了
总结:
着色器语言GLSL是一种基于C的编程语言 可以理解为GPU编程 方便我们操作顶点和片元相关的渲染管线 实现我们自定义的绘制效果。和普通的编程一样。需要编写代码 编译 运行 不过shader的加载编译运行是在app运行是动态解释执行的。理解了着色器的运行流程。再去编写GLSL。会有更加深入的理解。
Demo代码地址:LearnOpenGLESDemo
参考文章:
GLSL基础语法介绍