本文为L_Ares个人写作,包括图片皆为个人亲自操作,如需转载请表明原文出处。
之前的渲染一直都是用GLKit来帮助我们完成,那么如果不借助GLKit框架的话,想要实现渲染效果,我们就需要自己来进行渲染代码的编写。
想要使用着色器进行渲染的话,前提条件就是一定要有2个基本的对象:着色器对象和程序对象,程序对象,关于这一个步骤,在链接中的文章已经说明了原因和他们的作用。
在创建了两个基本对象,并获取链接之后,着色器对象就需要开始它的工作,这个工作一般包含了大概如下的步骤:
-
创建顶点着色器对象,创建片元着色器对象。
-
将源代码链接到每个着色器对象。
-
编译着色器对象。
-
创建程序对象。
-
将已经编译过的着色器对象和程序对象链接。
-
链接程序对象。
在没有完成这些步骤之前,我们是很难直接将着色器里面的内容和我们Client中的内容进行交互的。
一、渲染缓冲区
渲染缓冲区英文名:RenderBuffer
RenderBuffer是一个通过应用分配的2D缓冲区。RenderBuffer可以用来分配和存储颜色、深度、模版,也可以用过一个framebuffer的颜色、深度、模板的附件。RenderBuffer就类似于窗口系统提供的一个可绘制的表面。
但是,RenderBuffer不能被拿来当作一个GL的纹理直接使用。
RenderBuffer里面包含了深度缓冲区(DepthBuffer)、模版缓冲区
(StencilBuffer)、纹理(Texture)。
在GLKit的相关介绍中说过,显示在你屏幕上的图形,是在帧缓冲区(FrameBuffer)中被呈现上去的,只不过GLKit框架帮我们创建过了FrameBuffer。FrameBuffer在OpenGl ES是非常重要的组件,GLKit本身也是苹果基于OpenGL ES来进行的封装,所以在不使用GLKit之后,图形依然也是在FrameBuffer中完成设置后呈现到屏幕上的,但是这里的framebuffer因为不用GLKit框架了,没有GLKView了, 就要我们自己来创建。
在OpenGL ES中,常称FrameBuffer对象为FBO。
那么framebuffer又和这里要说的RenderBuffer是什么关系呢?
直接点的说,FameBuffer是来管理RenderBuffer的。真正用来存储颜色、深度、模版值的是RenderBuffer,而FrameBuffer是他们的一个附着点。关系图如下1.1所示:
1.1.png图中颜色本身就是可以当作纹理使用的,比如纯色纹理。深度则是在OpenGL中就说过,深度的大小会影响颜色缓冲区存储的颜色值,近存远删。
二、简单的GLSL实现图片渲染的案例
先说明一下.vsh
和.fsh
文件的创建,其实就是empty文件的创建,大家应该都使用过了,我就直接贴图2.1了,记得把后缀名加上就行。
然后说一下这着色器文件中,最简单的,也是最必需要写的东西,因为最好不要在这两个文件中写注释,所以就单独拿出来解释一下。
.vsh(顶点着色器代码)
//顶点坐标
attribute vec4 position;
//纹理坐标
attribute vec2 textCoordinate;
//varying是一个标记,声明了这个变量是用来在vsh和fsh文件之间传递的变量
//lowp是指这个二维向量的单位:GLFloat,它的精度
//这个参数是存储纹理坐标的
varying lowp vec2 varyTextCoord;
void main ()
{
//把纹理坐标值赋值给传递变量,由传递变量将纹理坐标传输到片元着色器
varyTextCoord = textCoordinate;
//gl_Position是GLSL的内建变量,也就是GLSL已经创建好了的,用来保存顶点坐标的变量
gl_Position = position;
}
.fsh(顶点着色器代码)
//这个就是刚才从顶点着色器传过来的纹理坐标,注意这里最好直接复制过来,因为一个字母都不许差
varying lowp vec2 varyTextCoord;
//uniform属性 sampler2D代表的是声明纹理属性,就是说声明这个变量是纹理,他是以类似标识符的方式存储的
//也就是说不是把你真的纹理放进来了,而是给纹理声明了一个身份ID,由ID去索引相应的纹理
uniform sampler2D colorMap;
void main ()
{
//内建变量gl_FragColor(纹理采样器,纹理坐标)
//参数1 : 纹理的身份ID
//参数2 : 纹理坐标。
//内建函数会返回一个vec4类型的rgba值
//它的作用就是读取纹素
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
这里的代码要用直接复制走的话,用的时候记得把中文注释都删除掉,尽量避免出现错误的情况。
下面直接上代码,但是这次绘制的图片是翻转过来的,原因很简单,这次的代码没有做之前GLKit里面设置的OriginBottomLeft,所以纹理原点没有在左下角,而是和view的一样,在左上角。
另外,这只是自定义View里面的内容,所以要显示出来记得要在viewcontroller里面把view加上去。
//
// JDView.m
// 04GLSL渲染图片
//
// Created by EasonLi on 2020/9/20.
// Copyright © 2020 EasonLi. All rights reserved.
//
#import "JDView.h"
#import <OpenGLES/ES2/gl.h>
#define MY_ORIGIN self.frame.origin
#define MY_SIZE self.frame.size
@interface JDView ()
//继承于CALayer。是在iOS上用于绘制OpenGL ES的图层类
@property (nonatomic, strong) CAEAGLLayer *eaglLayer;
//上下文
@property (nonatomic,strong) EAGLContext *nContext;
//渲染缓冲区
@property (nonatomic,assign) GLuint nRenderBuffer;
//帧缓冲区
@property (nonatomic,assign) GLuint nFrameBuffer;
//Program
@property (nonatomic,assign) GLuint nProgram;
@end
@implementation JDView
#pragma mark - 重绘View
- (void)layoutSubviews
{
//创建图层
[self createLayer];
//创建图形的上下文
[self createContext];
//清空缓存区
[self cleanUpBuffers];
/********************************************/
//这里要注意,必须是先有渲染缓存区,再有帧缓存区,因为renderbuffer才是真的缓存颜色,模版,深度的地方
//frameBuffer是附着点!!!相当于只是管理着renderbuffer
//设置渲染缓存区
[self setUpRenderBuffer];
//设置帧缓存区
[self setUpFrameBuffer];
/********************************************/
//渲染并呈现
[self rendLayer];
}
#pragma mark - 创建图层
- (void)createLayer
{
//要重写layerClass,把JDView的图层强转成CAEAGLLayer类型,并赋值给eaglLayer
self.eaglLayer = (CAEAGLLayer *)self.layer;
//配置一下分辨率的缩放因子
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
//设置layer绘制的描述属性
//描述属性接收字典类型,这里设置了绘图表面显示之后,不保留其内容。(一般默认都是不保留,就是说下一次重新绘制)
//以及颜色格式是RGBA8888
self.eaglLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@false,kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8};
}
//重写一下返回图层类的方法,宿主图层换成CALayer子类CAEAGLLayer
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
#pragma mark - 设置图层上下文
- (void)createContext
{
//初始化上下文,设置OpenGL ES的版本,因为需求不大,OpenGL ES2.0足够,可以用3.0
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
//判断这个上下文是否创建成功
if (!context) {
NSLog(@"上下文创建失败");
return;
}
//设置当前上下文
if (![EAGLContext setCurrentContext:context]) {
NSLog(@"设置当前上下文失败");
return;
}
//将局部变量的context赋值给我们的属性
self.nContext = context;
}
#pragma mark - 清空缓存区
- (void)cleanUpBuffers
{
// Buffer(缓存区)分为renderBuffer(渲染缓存区)和frameBuffer(帧缓存区)两种。都要清空
//清空renderBuffer
glDeleteBuffers(1, &_nRenderBuffer);
self.nRenderBuffer = 0;
//清空frameBuffer
glDeleteBuffers(1, &_nFrameBuffer);
self.nFrameBuffer = 0;
}
#pragma mark - 申请并设置渲染缓冲区
- (void)setUpRenderBuffer
{
//定义一个存储缓存区的ID的变量
GLuint renderBufferID;
//申请缓存区,并将其身份ID赋值
glGenRenderbuffers(1, &renderBufferID);
//将渲染缓存区的身份ID赋值给属性来保存
self.nRenderBuffer = renderBufferID;
//根据缓存区ID绑定缓存区的类型
glBindRenderbuffer(GL_RENDERBUFFER, self.nRenderBuffer);
//将刻绘制对象,也即是我们的CAEAGLLayer图层对象,绑定到RenderBuffer对象
BOOL result = [self.nContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.eaglLayer];
if (!result) {
NSLog(@"绘制图层和渲染缓存区绑定失败");
}
}
#pragma mark - 申请并设置帧缓存区
- (void)setUpFrameBuffer
{
//定义保存帧缓存区ID的对象
GLuint frameBufferID;
//申请帧缓存区并将身份ID赋值
glGenFramebuffers(1, &frameBufferID);
//将得到的frameBufferID赋值给属性
self.nFrameBuffer = frameBufferID;
//根据缓存区ID,把它的绑定到对应的缓存区类型
glBindFramebuffer(GL_FRAMEBUFFER, self.nFrameBuffer);
//把framebuffer和renderbuffer绑定在一起
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.nRenderBuffer);
}
#pragma mark - 渲染并呈现
- (void)rendLayer
{
//绘制前一样要设置好清屏颜色,和OpenGL是一样的
glClearColor(0.3f, 0.3f, 0.3f, 1.f);
//绘制前一定要清空缓冲区
glClear(GL_COLOR_BUFFER_BIT);
//设置视口大小
//先拿到mainScreen主屏的缩放因子
CGFloat scale = [UIScreen mainScreen].scale;
//设置视口
glViewport(MY_ORIGIN.x * scale, MY_ORIGIN.y * scale, MY_SIZE.width * scale, MY_SIZE.height * scale);
//加载着色器,链接program,使用program
[self loadShaderAndLinkUseProgram];
//设置顶点
[self makeVertex];
//处理纹理信息
[self makeTextureInfo];
//绘图
glDrawArrays(GL_TRIANGLES, 0, 6);
//将渲染缓冲区(RenderBuffer)上的内容渲染到屏幕上
[self.nContext presentRenderbuffer:GL_RENDERBUFFER];
}
#pragma mark - 加载着色器,链接并使用程序program
- (void)loadShaderAndLinkUseProgram
{
//读取顶点着色器和片元着色器的程序文件
//拿到顶点着色器和片元着色器的程序路径
NSString *vshFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fshFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
//加载着色器文件,并创建最终的程序
self.nProgram = [self loadVertex:vshFile Fragment:fshFile];
//链接程序
glLinkProgram(self.nProgram);
//获取链接的状态
GLint linkStatus;
glGetProgramiv(self.nProgram, GL_LINK_STATUS, &linkStatus);
//判断程序是否链接成功
if (linkStatus == GL_FALSE) {
//失败的话要拿取错误信息,存储在数组里面
//定义错误信息数组GLChar类型数组,直接分配内存空间
GLchar message[512];
//参数:(1)程序 (2)错误信息的内存大小 (3)从哪里开始放 (4)错误信息放在哪里,直接写message一样,数组首地址
glGetProgramInfoLog(self.nProgram, sizeof(message), 0, &message[0]);
NSLog(@"程序链接失败,失败信息 : %@",[NSString stringWithUTF8String:message]);
return;
}
//使用Program
glUseProgram(self.nProgram);
}
#pragma mark - 处理顶点数据
- (void)makeVertex
{
//设置顶点坐标数组
GLfloat vertexArr[] = {
0.5f,-0.5f,0.f, 1.f,0.f,
-0.5f,0.5f,0.f, 0.f,1.f,
-0.5f,-0.5f,0.f, 0.f,0.f,
0.5f,0.5f,0.f, 1.f,1.f,
-0.5f,0.5f,0.f, 0.f,1.f,
0.5f,-0.5f,0.f, 1.f,0.f
};
//处理顶点信息
//定义变量存储顶点缓存区ID
GLuint vertexID;
//申请顶点缓存区,并将ID赋值
glGenBuffers(1, &vertexID);
//绑定缓存区ID和对应的缓存区类型
glBindBuffer(GL_ARRAY_BUFFER, vertexID);
//将顶点数据从CPU拷贝到GPU中,也就是内存数据放入显存
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexArr), vertexArr, GL_DYNAMIC_DRAW);
//将顶点数据通过Program,传入到顶点着色器的position中,并返回一个属性变量的位置
//第二个参数必须和顶点着色器中的顶点坐标属性字母完全一致
GLuint position = glGetAttribLocation(self.nProgram, "position");
//打开属性通道,并且以合适的格式传输从buffer中读取顶点数据
glEnableVertexAttribArray(position);
//设置顶点坐标读取方式
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
}
#pragma mark - 设置纹理信息
- (void)makeTextureInfo
{
//将纹理坐标通过Program传入到顶点和片元着色器,同样的,字母名称必须和着色器中定义的变量完全一致
GLuint textCoord = glGetAttribLocation(self.nProgram, "textCoordinate");
//打开属性通道,传输纹理坐标
glEnableVertexAttribArray(textCoord);
//设置纹理坐标的读取方式
glVertexAttribPointer(textCoord, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
//加载纹理
[self loadTexture:@"image1"];
//设置纹理采样器
//参数:
//(1). 第一个是得到纹理的ID索引的位置,因为纹理是不经常改变的,所以用Uniform通道
//(2). 第几个纹理
glUniform1i(glGetUniformLocation(self.nProgram, "colorMap"), 0);
}
#pragma mark - 加载着色器shader,并返回Program信息
- (GLuint)loadVertex:(NSString *)vertexFile Fragment:(NSString *)fragmentFile
{
//定义两个临时的着色器变量
GLuint vertextShader, fragmentShader;
//创建程序
GLuint program = glCreateProgram();
//编译顶点着色器和片元着色器程序
//参数:
//(1). 编译完成后的着色器的内存地址
//(2). 编译的是哪个着色器,也就是着色器的类型。
//(3). 着色器文件的项目路径
//编译顶点着色器
[self compileShader:&vertextShader type:GL_VERTEX_SHADER file:vertexFile];
//编译片元着色器
[self compileShader:&fragmentShader type:GL_FRAGMENT_SHADER file:fragmentFile];
//把着色器都附着或者说链接上程序
//附着顶点着色器
glAttachShader(program, vertextShader);
//附着片元着色器
glAttachShader(program, fragmentShader);
//用完了这两个临时的着色器变量,也就是附着到程序上面了,就可以删除掉了
//删除顶点着色器
glDeleteShader(vertextShader);
//删除片元着色器
glDeleteShader(fragmentShader);
return program;
}
//编译着色器
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
{
//读取shader文件的路径
NSString *shaderFile = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
//因为glShaderSouce这个函数需要的是字符串类型的指针,所以这里转成C语言的字符串
const GLchar *source = (GLchar *)[shaderFile UTF8String];
//创建一个shader,并直接将创建的shader放入参数传过来的着色器内容(这里的*不是指的地址,是指的临时着色器的内容)
*shader = glCreateShader(type);
//将着色器源码附着到着色器对象上
//参数:
//(1). shader,要编译的着色器对象(*shader)
//(2). 着色器源码字符串的数量,就是用了几个字符串写的或者说承载的着色器源码
//(3). 真正的着色器程序的源码,也就是vsh和fsh里面的。(这就是第二个参数说的那一个字符串的地址)
//(4). 着色器源码字符串的长度,如果不知道或者说不确定,写NULL,NULL代表字符串的终止位
glShaderSource(*shader, 1, &source, NULL);
//将着色器源码编译成目标代码
glCompileShader(*shader);
}
#pragma mark - 从图片中加载纹理
- (void)loadTexture:(NSString *)textureFile
{
//将UIImage类型的图片转换成CGImageRef,因为纹理最终需要的是像素位图,也就是要解压图片
CGImageRef spriteImage = [UIImage imageNamed:textureFile].CGImage;
//可以判断一下是否获得到了像素位图
if (!spriteImage) {
NSLog(@"解压缩图片失败 : %@",textureFile);
//非正常运行程序导致程序退出。exit(0)是正常运行程序导致退出
exit(1);
}
//成功拿到位图了,获取图片的宽高的大小
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
//获取图片字节数是多少 也就是图片面积 * 颜色通道数量(RGBA就是4个)
//也可以用malloc,malloc(width * height * 4 * sizeof(GLubyte));
//稍提一嘴,calloc就是在内存的动态存储区上,分配第一个参数个数量的,每个单位长度为第二个参数的大小的连续空间
//返回值是指向分配起始地址的指针,分配失败的话,返回值是NULL
//calloc会清空分配的内存,而malloc不会。所以自行选择
GLubyte *spriteByte = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
//创建上下文
//参数:
//(1). 指向要渲染的绘制图像的地址
//(2). bitmap(位图)的宽,单位是像素
//(3). bitmap(位图)的高,单位是像素
//(4). bitsPerComponent是指内存中,像素的每个组件的位数,比如32位的RGBA,那么每一个颜色位都是8
//(5). bytesPerRow指的是bitmap每一行内存需要多少bit(位)内存
//(6). space指的是bitmap使用的颜色空间,可以通过CGImageGetColorSpace()获取
//(7). bitmapInfo是枚举类型,CGImageAlpahInfo
CGContextRef spriteContext = CGBitmapContextCreate(spriteByte, width, height, 8, width * 4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
//在上下文上把图片绘制出来
//定义变量,存储位图的尺寸CGRect
CGRect rect = CGRectMake(0, 0, width, height);
//使用默认的方法绘制
CGContextDrawImage(spriteContext, rect, spriteImage);
//绘制完成后就可以释放上下文了
CGContextRelease(spriteContext);
//绑定纹理到默认的纹理ID,因为glUniform里面也设置的0
glBindTexture(GL_TEXTURE_2D, 0);
//设置纹理属性,这里就不多说了,可以参考OpenGL的文章,里面有纹理的属性设置
//参数:
//(1). 纹理维度
//(2). 要设置的纹理属性的名字
//(3). 要设置的纹理属性的参数
//这里要设置纹理过滤方式和环绕方式
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);
//要转一下图片宽高的类型,不然会提示,毕竟一个是unsigned的size_t,但是载入纹理要的是int_32
float tWidth = width,tHeight = height;
//载入2D纹理
//https://www.jianshu.com/p/4e2bb76e31c3 这里有解释
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tWidth, tHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteByte);
//图片数据也用完了,可以释放了
free(spriteByte);
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
@end
效果图如下2.2所示:
2.2.png
网友评论