上一节,说好如何使用iOS 中内置的框架来构建一个三角形。使用到了GLKBaseEffect这个类来直接创建着色器。
我们并不懂其中系统内部是如何构建的,可能也对着色器这词语很陌生,也不懂这是什么,现在我们不使用内部的类,就只是使用OpenGL es来构建一个三角形。
这里我们就需要使用到着色器的编译和使用。
构建OpenGL视图环境
因为OpenGL需要在一个固定的渲染环境,而iOS视图都有进行了进一步的封装。需要把它缺省的layer设置为一个特殊的layer。(CAEAGLLayer)。
/**
设置OpenGL 上下文环境
*/
- (void)setupContext{
self.context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
if (!self.context) {
NSLog(@"Failed to initialize OpenGLES 2.0 context");
exit(1);
}
//设置为当前上下文
if (![EAGLContext setCurrentContext:self.context]) {
NSLog(@"Failed to set current OpenGL context");
exit(1);
}
}
/**
想要显示OpenGL的内容,你需要把它缺省的layer设置为一个特殊的layer。(CAEAGLLayer)。
这里通过直接复写layerClass的方法。
*/
- (void)setupLayer {
_eaglayer = (CAEAGLLayer *)self.layer;
//设置放大倍数
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
// CALayer 默认是透明的,必须将它设为不透明才能让其可见(CALayer是透明的。而透明的层对性能负荷很大,特别是OpenGL的层。)
_eaglayer.opaque = YES;
// 设置描绘属性,在这里设置不维持渲染内容以及颜色格式为 RGBA8
self.eaglayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
@(NO), kEAGLDrawablePropertyRetainedBacking,
kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
}
+ (Class)layerClass {
return [CAEAGLLayer class];
}
这里需要注意的是需要写一个类方法,每一个UIView都是寄宿在一个CALayer的示例上。这个图层是由视图自动创建和管理的,那我们可以用别的图层类型替代它么?一旦被创建,我们就无法代替这个图层了。但是如果我们继承了UIView,那我们就可以重写+layerClass方法使得在创建的时候能返回一个不同的图层子类。UIView会在初始化的时候调用+layerClass方法,然后用它的返回类型来创建宿主图层 。
构建帧缓存区
用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件丢弃特定片段的模板缓冲。这些缓冲结合起来叫做帧缓冲(Framebuffer),它被储存在内存中。OpenGL允许我们定义我们自己的帧缓冲,也就是说我们能够定义我们自己的颜色缓冲,甚至是深度缓冲和模板缓冲。
和OpenGL中的其它对象一样,我们会使用一个叫做glGenFramebuffers的函数来创建一个帧缓冲对象(Framebuffer Object, FBO):
/**
设置帧缓存区
*/
- (void)setupFrameBuffer{
GLuint buffer;
glGenFramebuffers(1, &buffer);
_colorFrameBuffer = buffer;
glBindFramebuffer(GL_FRAMEBUFFER, _colorFrameBuffer);
}
构建渲染缓存区
/**
设置渲染缓存区
*/
- (void)setupRenderBuffer {
GLuint buffer;
glGenRenderbuffers(1, &buffer);
_colorRenderBuffer = buffer;
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer);
//分配内存空间
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.eaglayer];
//将 _colorRenderBuffer 装配到 GL_COLOR_ATTACHMENT0 这个装配点上
glFramebufferRenderbuffer(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER,
_colorRenderBuffer);
}
渲染缓冲对象(Renderbuffer Object)是在构建帧缓存后,作为一个可用的帧缓冲附件类型的。渲染缓冲对象是一个真正的缓冲,即一系列的字节、整数、像素等。渲染缓冲对象附加的好处是,它会将数据储存为OpenGL原生的渲染格式,它是为离屏渲染到帧缓冲优化过的。
构建着色器
着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。
着色器分为顶点着色器和片段着色器
顶点着色器
每个输入变量也叫顶点属性(Vertex Attribute)。我们能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限。
attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsOut;
void main(void)
{
//用来展现纹理的多边形顶点
gl_Position = Position;
//表示使用的纹理的范围的顶点,因为是2D纹理,所以用vec2类型
TextureCoordsOut = TextureCoords;
}
片段着色器
precision mediump float;
uniform sampler2D Texture;
varying vec2 TextureCoordsOut;
void main(void)
{
//获取纹理的像素
vec4 mask = texture2D(Texture, TextureCoordsOut);
gl_FragColor = vec4(mask.rgb, 1.0);
}
它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)
着色器模块的知识点比较多,具体详情可以参考:LearnOpenGL-着色器
或者后续我稍微整理下我所理解的知识点。
编译着色器
构建出两个着色器后,我们需要对这两个文件进行编译处理,生成一个shaderProgram。连接着色器中的数据。
/**
编译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);
glShaderSource(*shader, 1, &source, NULL);
glCompileShader(*shader);
GLint compileSuccess;
glGetShaderiv(*shader, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
GLchar messages[256];
glGetShaderInfoLog(*shader, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"%@", messageString);
exit(1);
}
}
/**
* glsl的编译过程主要有glCompileShader、glAttachShader、glLinkProgram三步;
*
* @return 编译成功的shaders
*/
- (GLuint)compileShader{
GLuint verShader, fragShader;
GLint program = glCreateProgram();
//1、编译shader
NSString* vertFilePath = [[NSBundle mainBundle] pathForResource:kVertexFileName ofType:@"glsl"];
[self compileShader:&verShader type:GL_VERTEX_SHADER file:vertFilePath];
NSString* fragFilePath = [[NSBundle mainBundle] pathForResource:kFragmentFileName ofType:@"glsl"];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragFilePath];
glAttachShader(program, verShader);
glAttachShader(program, fragShader);
//2、链接
glLinkProgram(program);
GLint linkSuccess;
glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) { //连接错误
GLchar messages[256];
glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"error%@", messageString);
return 0;
}
else {
NSLog(@"link ok");
glUseProgram(program); //成功便使用,避免由于未使用导致的的bug
}
//3、释放不需要的shader
glDeleteShader(verShader);
glDeleteShader(fragShader);
return program;
}
这就是编译着色器的代码,比较固定。正常需求下一般不需要修改。
主要注意glUseProgram(program);
如果编译成功,就需要开启使用。有些人会把这个提取到外面,需要使用到着色器的时候在启用。我为了防止自己忘记,在编译成功时候就直接开启使用。
因为这编译代码都是C++的处理,里面的一些变量并不遵循iOS的内存池管理,需要手动进行销毁。不然会造成内存警告。
构建三角形顶点位置
static GLfloat vertices[] =
{
0.5f, -0.5f, -1.0f,
-0.5f, 0.5f, -1.0f,
-0.5f, -0.5f, -1.0f,
};
连接和启用着色器中的变量
/**
* 为编译好的着色器中的顶点、纹理坐标和旋转矩阵赋值
*
**/
- (void)setupValueForShader:(GLuint)shader{
//将顶点数据拷贝到GPU
GLuint verticesBuffer;
glGenBuffers(1, &verticesBuffer);
glBindBuffer(GL_ARRAY_BUFFER, verticesBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);
//设置position值
GLuint position = glGetAttribLocation(shader, "position");
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
glEnableVertexAttribArray(position);
//设置textCoordinate值
GLuint textCoor = glGetAttribLocation(shader, "textCoordinate");
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
glEnableVertexAttribArray(textCoor);
}
这里面主要需要理解的就是如何通过代码来连接到着色器中的变量,用iOS的代码去控制这些变量。
渲染三角形
/**
渲染,最后一步
*/
- (void)render {
//清屏
glClearColor(0, 1.0, 0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
CGFloat scale = [[UIScreen mainScreen] scale]; //获取视图放大倍数,可以把scale设置为1试试
//设置视口大小
glViewport(self.frame.origin.x * scale, self.frame.origin.y * scale, self.frame.size.width * scale, self.frame.size.height * scale);
glDrawArrays(GL_TRIANGLES, 0, 3); //3是顶点的数量
//将最终渲染结果提交.
[self.context presentRenderbuffer:GL_RENDERBUFFER];
}
这里绘制三角形的方式和上节使用索引绘制的不一样,这也就是上节说的另外一种方法。