首先,我们看效果,如图所示,我们在屏幕上展示一个图片。
image.png
Shader
在前面的文章,我们讲过,GLSL语言中,我们只能对顶点着色器
和片元着色器
进行操作,所以我们只需要创建顶点着色器和片元着色器文件。
- commond + N ——> Empty ——>
-
创建顶点着色器文件(shaderv.vsh)和片元着色器文件(shaderf.fsh)
image.png
-
vsh
和fsh
只是让开发者能够分得清顶点着色器和片元着色器,其实它的本质是一段NSString
。 - 不建议用
NSString
来直接编写,否则后期会混乱 - 不建议使用中文注释,可能会导致错误调试很难
shaderv.vsh
shaderv.vsh中的代码如下:
//顶点向量
attribute vec4 position;
//纹理向量
attribute vec2 textCoordinate;
//纹理(内部传递)
varying lowp vec2 varyTextCoord;
void main()
{
varyTextCoord = textCoordinate;
gl_Position = position;
}
- 在GLSL中语言中,我们最常用的有3种修饰类型:
1.uniform
修饰从外部(oc、swift)传递到顶点着色器、片元着色器的变量,类似于常量
2.attribute
的特点是只能传入到顶点着色器,也只能在顶点着色器使用,一般修饰顶点坐标、纹理坐标、颜色等。
2.varying
,当我们需要将数据从顶点着色器传入片元着色器到中,就使用varying
,就相当于一个桥接,注意2个着色器的变量名要一致。 -
gl_Position
是内建变量(GLSL创建 ,我们只需要使用),表示顶点着色器计算后的顶点结果,因为这里并没有发生仿射变换,所以直接传给gl_Position
就可以。
shaderf.fsh
//定义使用高精度
precision highp float;
//纹理坐标(从顶点着色器传来)
varying lowp vec2 varyTextCoord;
//纹理数据(采样器)
uniform sampler2D textCoordMap;
void main(){
//获取纹素
gl_FragColor = texture2D(textCoordMap,varyTextCoord);
}
-
precision highp float
表示默认使用高精度,如果不设置这段代码,有可能因为精度过小而产生bug。-
highp
表示高精度 -
lowp
表示低精度
-
-
sampler2D textCoordMap
传递纹理数据,是一个采样器,其实我们拿到的是一个纹理ID。 -
texture2D(textCoordMap,varyTextCoord)
的意义就是拿到纹素
,而纹素就是纹理对应坐标点的颜色值。-
textCoordMap
表示纹理 -
varyTextCoord
表示纹理坐标 -
gl_FragColor
也是一个内建变量,是一个颜色值
-
OC函数(编译、调用Shader)
准备工作
我们创建几个变量
//在iOS和tvOS上绘制OpenGL ES内容的图层,继承自CALyayer
@property(nonatomic,strong)CAEAGLLayer *myLayer;
//EAGL上下文
@property(nonatomic,strong)EAGLContext *context;
//frameBuffer
@property(nonatomic,assign)GLuint myFrameBuffer;
//renderBuffer
@property(nonatomic,assign)GLuint myRenderBuffer;
//Program
@property(nonatomic,assign)GLuint myProgram;
-
CAEAGLLayer
提供了一个可用的绘图表面,就是将我们图形绘制到CAEAGLLayer上 -
EAGLContext
用来保存发生的状态 -
FrameBuffer
和RenderBuffer
,我们平常创建的纹理(Texture)
,深度缓冲区(Depth Buffer)
,模板缓冲区(Stencil Buffer)
都是基于渲染缓冲区对象(Render Buffer Objects)
,而渲染缓冲区最终会附着到FrameBuffer
上。 -
Program
,我们写的shader都会附着到Program上
开始
//1 设置图层
[self setupLayer];
//2 设置上下文
[self setupContext];
//3.清空缓存区
[self deleteRenderAndFrameBuffer];
//4.设置RenderBuffer
[self setupRenderBuffer];
//5.设置setupFrameBuffer
[self setupFrameBuffer];
//6.开始绘制
[self renderLayer];
流程图.png
setupLayer
// 1.设置图层
-(void)setupLayer{
//1.创建特殊图层
/*
重写layerClass,将CCView返回的图层从CALayer替换成CAEAGLLayer
*/
self.myLayer = (CAEAGLLayer *)self.layer;
NSLog(@"%@",self.myLayer);
//设置scale
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
//3.设置描述属性,这里设置不维持渲染内容以及颜色格式为RGBA8
/*
kEAGLDrawablePropertyRetainedBacking 表示绘图表面显示后,是否保留其内容。
kEAGLDrawablePropertyColorFormat
可绘制表面的内部颜色缓存区格式,这个key对应的值是一个NSString指定特定颜色缓存区对象。默认是kEAGLColorFormatRGBA8;
kEAGLColorFormatRGBA8:32位RGBA的颜色,4*8=32位
kEAGLColorFormatRGB565:16位RGB的颜色,
kEAGLColorFormatSRGBA8:sRGB代表了标准的红、绿、蓝,即CRT显示器、LCD显示器、投影机、打印机以及其他设备中色彩再现所使用的三个基本色素。sRGB的色彩空间基于独立的色彩坐标,可以使色彩在不同的设备使用传输中对应于同一个色彩坐标体系,而不受这些设备各自具有的不同色彩坐标的影响。
*/
// self.myLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,nil];
self.myLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@false,kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8};
}
这一步操作就是将layer转化成CAEAGLLayer
,注意,我们还需要加下面这个代码,否则转化不了
//没有该方法,layer不会赋值给CAEAGLLayer
+(Class)layerClass{
return [CAEAGLLayer class];
}
setupContext
// 2.设置上下文
-(void)setupContext{
//设置上下文
EAGLContext *context = [[EAGLContext alloc] initWithAPI:(kEAGLRenderingAPIOpenGLES3)];
//判断是否创建成功
if (!context) {
NSLog(@"创建失败");
return;
}
//设置当前上下文
if (![EAGLContext setCurrentContext:context]) {
NSLog(@"设置当前上下文失败");
return;
}
//把上下文设置全局
self.context = context;
}
设置上下文的目的就是用来保存发生的状态
deleteRenderAndFrameBuffer
因为OpenGL会保存状态,所以每次创建前都要进行重置、初始化
// 3.清空缓冲区
-(void)deleteRenderAndFrameBuffer{
/*
buffer分为frame buffer 和 render buffer2个大类。
其中frame buffer 相当于render buffer的管理者。
frame buffer object即称FBO。
render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。
*/
glDeleteRenderbuffers(1, &(_myRenderBuffer));
self.myRenderBuffer = 0;
glDeleteFramebuffers(1, &_myFrameBuffer);
self.myFrameBuffer = 0;
}
setupRenderBuffer
//设置RenderBuffer
-(void)setupRenderBuffer{
GLuint renderBuffer;
//申请一个缓冲区标志
glGenRenderbuffers(1, &renderBuffer);
//绑定标识符到GL_RENDERBUFFER
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
//将可绘制对象drawable object's CAEAGLLayer的存储绑定到OpenGL ES renderBuffer对象
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myLayer];
self.myRenderBuffer = renderBuffer;
}
针对renderbufferStorage
这个方法,官方解释如下:
Attaches an EAGLDrawable as storage for the OpenGL ES renderbuffer object bound to <target>
实际上就是将context
跟layer
以renderbuffer
的形式作绑定,就是之后的会绘制到CAEAGLLayer上。
setupFrameBuffer
//设置FrameBuffer
-(void)setupFrameBuffer{
GLuint frameBuffer;
//申请一个缓冲区标志
glGenFramebuffers(1, &frameBuffer);
//绑定标识符到GL_RENDERBUFFER
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
/*生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
*/
self.myFrameBuffer = frameBuffer;
//5.将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到 GL_COLOR_ATTACHMENT0上。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myRenderBuffer);
}
glFramebufferRenderbuffer
就是讲RenderBuffer附着到FrameBuffer上
renderLayer
- 清理缓冲区、设置视口
//清理屏幕颜色
glClearColor(0, 0, 0, 1);
//清理颜色缓冲区
glClear(GL_COLOR_BUFFER_BIT);
//设置视口大小
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);
- 读取顶点着色器和片元着色器路径
//读取顶点着色器、片元着色器
NSString *vShader = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fShader = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
- 编译着色器、附着到Program
#pragma -mark loadShader
-(GLuint)loadVertexShader:(NSString *)vertext andFragShader:(NSString *)fragment{
GLuint vShader,fShader;
//创建program
GLint program = glCreateProgram();
//2.编译顶点着色程序、片元着色器程序
//参数1:编译完存储的底层地址
//参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
//参数3:文件路径
[self compileShader:&vShader withType:GL_VERTEX_SHADER andPath:vertext];
[self compileShader:&fShader withType:GL_FRAGMENT_SHADER andPath:fragment];
//把编译好的程序附着到shader(shader -> program)
glAttachShader(program, vShader);
glAttachShader(program, fShader);
//删除shader 以免占用内存
glDeleteShader(vShader);
glDeleteShader(fShader);
return program;
}
#pragma -mark compileShader
-(void)compileShader:(GLuint *)shader withType:(GLenum)type andPath:(NSString *)path{
//1.读取文件路径字符串
NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
//转化成c语言字符串
const GLchar *source = [content UTF8String];
//2.创建一个shader(根据type类型)
*shader = glCreateShader(type);
//3.将着色器源码附加到着色器对象上。
//参数1:shader,要编译的着色器对象 *shader
//参数2:numOfStrings,传递的源码字符串数量 1个
//参数3:strings,着色器程序的源码(真正的着色器程序源码)
//参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
glShaderSource(*shader, 1, &source, nil);
//把着色器代码编译成目标代买
glCompileShader(*shader);
}
1. compileShader
这个方法的shader需要传递指针,以存放编译好的shader
2. 编译完成后要进行删除glDeleteShader
,以便清空缓存,释放空间
- 使用Program
//链接Program
glLinkProgram(self.myProgram);
//查看链接是否成功
GLint linkStatus;
glGetProgramiv(self.myProgram, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
GLchar message[512];
glGetProgramInfoLog(self.myProgram, sizeof(message), 0, &message[0]);
NSString *messageString = [NSString stringWithUTF8String:message];
NSLog(@"Program Link Error:%@",messageString);
return;
}
NSLog(@"链接成功");
glUseProgram(self.myProgram);
1. glLinkProgram
链接程序
2. glGetProgramiv
可以查看链接状态,glGetProgramInfoLog
打印错误日志
3. 编译完成后要进行删除glUseProgram
,使用程序
- 处理顶点坐标和纹理坐标
//6.设置顶点、纹理坐标
//前3个是顶点坐标,后2个是纹理坐标
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,
};
//处理顶点着色器
GLuint verBuffer;
//(2)申请一个缓存区标识符
glGenBuffers(1, &verBuffer);
//(3)将verBuffer绑定到GL_ARRAY_BUFFER标识符上
glBindBuffer(GL_ARRAY_BUFFER, verBuffer);
//(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传递过去的
//读取顶点通道ID
GLuint positon = glGetAttribLocation(self.myProgram, "position");
//开启通道
glEnableVertexAttribArray(positon);
//(3).设置读取方式
glVertexAttribPointer(positon, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, 0);
//读取纹理通道ID
GLuint textCoord = glGetAttribLocation(self.myProgram, "textCoordinate");
//开启通道
glEnableVertexAttribArray(textCoord);
//(3).设置读取方式
glVertexAttribPointer(textCoord, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
1.glGetAttribLocation(self.myProgram, "position")
是获取通道ID
2.position
、textCoordinate
要和vsh
中的数据一模一样
3.glVertexAttribPointer
传递数据到顶点着色器
- 加载纹理
-(void)setupTexture:(NSString *)fileName{
//将image转化成 CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
//判断图片读取是否成功
if (!spriteImage) {
NSLog(@"读取失败");
exit(1);
}
// 获取宽度
CGFloat width = CGImageGetWidth(spriteImage);
// 获取高度
CGFloat height = CGImageGetHeight(spriteImage);
//3.获取图片字节数 宽*高*4(RGBA)
GLubyte * spriteData = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
//4.创建上下文
/*
参数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);
//5、在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);
//绑定纹理到默认的纹理ID
glBindTexture(GL_TEXTURE_2D, 0);
//9.设置纹理属性
/*
参数1:纹理维度
参数2:线性过滤、为s,t坐标设置模式
参数3:wrapMode,环绕模式
*/
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);
//载入纹理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, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
//释放spriteData
free(spriteData);
}
glBindTexture(GL_TEXTURE_2D, 0)
可以省略,因为当前只有一个纹理
- 绘制到屏幕
//设置纹理采样器 sampler2D
glUniform1i(glGetUniformLocation(self.myProgram, "textCoordMap"), 0);
//绘制
glDrawArrays(GL_TRIANGLES, 0, 6);
//从渲染缓冲区到屏幕上
[self.context presentRenderbuffer:GL_RENDERBUFFER];
1.glUniform1i(glGetUniformLocation(self.myProgram, "textCoordMap"), 0)
就是将纹理ID传进去,跟textCoordMap
进行绑定,因为此时纹理本身已经载入成功了。
2.glGetUniformLocation
是拿到shader中纹理的ID,因为textCoordMap是用uniform
修饰,所以传递数据也要用uniform。
3.[self.context presentRenderbuffer:GL_RENDERBUFFER]
将context中的数据渲染到屏幕上。
最后效果,如图:
image.png
翻转策略
因为之前我们已经讲过,由于纹理坐标原点是左下角,而图片显示原点是左上角,所以不设置就会发生翻转
解决方法
//设置顶点
varyTextCoord = vec2(textCoordinate.x,1.0-textCoordinate.y);
我们在顶点着色器中加这么一句话就可以了,其实原理也很简单,保持x轴坐标不变,y轴坐标取反,就可以达到翻转效果了。
当然,还有很多种解决办法,大家可以自己去尝试一下。
网友评论