上一篇文章我们介绍了如何用GLKit加载图片,为从OpenGL到OpenGL ES做一个过渡,并且介绍了EGL 和 EAGL。
今天这篇文章,就真正的进入到OpenGL ES ,也是以加载一张图片作为案例,后续会慢慢更新更多的内容。
OpenGL ES 跟GLKit 加载图片的区别
1. 加载纹理
GLKit中的GLKTextureLoader的作用是加载纹理图片,为方便开发者开发,于是对GLSL做了很大的优化,只需要一句代码就可以加载出纹理图片并生成GLKTextureInfo供开发者使用。
在OpenGL ES中,需要开发者自己从读取图片到利用CoreGraphics绘制图片再到设置线性过滤、环绕方式等等,需要一个比较长的流程。
2.顶点着色器/片元着色器
GLKBaseEffect的作用是执行顶点着色器和片元着色器的工作,便于开发者使用,用起来也非常方便。简单的几句代码便可以完成顶点着色器、片元着色器的工作。
在OpenGL ES中,需要开发者自己创建缓冲区、着色器、编译着色器等等繁杂的工作。
3.绘制
在GLKit当中,GLKViewDelegate提供了代理方法,只需要很简单的代码便可以完成图形的绘制。
而在OpenGL ES当中却需要开发者主动去将内容呈现在显示器上。
思维导图
接下来,我们跟随思维导图一步一步做。
创建顶点着色器
tips:着色器中的注释在使用时尽量去掉,免得出现不必要的错误
- 创建empty文件并命名为shaderv.vsh,命名规则无所谓,目的是为了让开发者自己能分辨清楚哪个文件的作用是什么。也可以命名为
vertexSahder.a
,vertexSahder.b
等等等等,都可以。只要自己分得清即可。这里vsh
也是vertexSahder
的缩写。
- 声明变量
//四维向量 顶点坐标
attribute vec4 position;
//二维向量 纹理坐标
attribute vec2 textCoordinate;
//低精度二维向量 纹理坐标
/*
此处用varying 修饰,表示要通过这个变量,将纹理坐标传递给片元着色器
lowp表示低精度
精度可分为highp/mediump/lowp 分别对应高/中/低
****************************************
此处声明变量的方式,以及变量名。
在片元着色器中,要同样声明一个一模一样的,才能完成纹理坐标的传递。
****************************************
*/
varying lowp vec2 varyTextCoord;
- main方法
void main(){
//varying 修饰,将纹理坐标传递到片元着色器
varyTextCoord = textCoordinate;
//给内建变量赋值
gl_Position = position;
}
创建片元着色器
tips:着色器中的注释在使用时尽量去掉,免得出现不必要的错误
- 同样创建一个empty文件,命名为shaderf.fsh。这里的
fsh
为fragmentShader
的缩写。 - 变量声明
//纹理坐标
varying lowp vec2 varyTextCoord;
//纹理采样器
uniform sampler2D colorMap;
- main方法
void main(){
/*
texture2D(纹理采样器,纹理坐标)
这个方法可以获取坐标对应的纹素
gl_FragColor 是GLSL的内建变量,用来将纹理颜色添加到对应的像素点上
*/
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
初始化
-
创建view
- import
#import
- 变量声明
@property(strong,nonatomic)CAEAGLLayer *eaglLayer;
@property(strong,nonatomic)EAGLContext *context;
@property(assign,nonatomic)GLuint program;
@property(assign,nonatomic)GLuint frameBuffer;
@property(assign,nonatomic)GLuint renderBuffer;
创建CAEAGLLayer
-(void)setupLayer{
//创建特殊图层
/*
重写layerClass,将当前View返回的图层从CALayer替换成CAEAGLLayer
*/
self.eaglLayer = (CAEAGLLayer *)self.layer;
//设置缩放
[self setContentScaleFactor:[UIScreen mainScreen].scale];
/*
kEAGLDrawablePropertyRetainedBacking :NO (告诉CoreAnimation不要试图保留任何以前绘制的图像留作以后重用)
kEAGLDrawablePropertyColorFormat :kEAGLColorFormatRGBA8 (告诉CoreAnimation用8位来保存RGBA的值)
也可以不设置。默认值就是这两个
链接:https://www.jianshu.com/p/b3852409edbc
*/
NSDictionary *options = @{kEAGLDrawablePropertyRetainedBacking:@(false),
kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8};
self.eaglLayer.drawableProperties = options;
}
//重写layer
+(Class)layerClass{
return [CAEAGLLayer class];
}
设置EAGLContext上下文
-(void)setupContext{
//创建context
self.context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];
if (!_context) {
NSLog(@"context创建失败");
return;
}
//设置当期那context并判断是否设置成功
if ([EAGLContext setCurrentContext:self.context]==false) {
NSLog(@"设置当前context失败!");
return;
}
}
清除缓冲区
这个其实也可以不用写
-(void)deleteBuffers{
glDeleteBuffers(1, &_frameBuffer);
_frameBuffer = 0;
glDeleteBuffers(1, &_renderBuffer);
_renderBuffer = 0 ;
}
创建RenderBuffer
-(void)setupRenderBuffer{
//定义标识符ID
GLuint bufferID;
//glGenRenderbuffers申请标识符
glGenRenderbuffers(1, &bufferID);
self.renderBuffer = bufferID;
//绑定缓冲区,注意此处为glBindRenderbuffer,不是glBindBuffer
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
//将可绘制对象的存储绑定到OpenGL ES renderbuffer对象。
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.eaglLayer];
}
创建FrameBuffer
-(void)setupFrameBuffer{
//定义标识符ID
GLuint bufferID;
//glGenFramebuffers申请标识符
glGenFramebuffers(1, &bufferID);
self.frameBuffer = bufferID;
//绑定缓冲区glBindFramebuffer
glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
//生成帧缓冲区,把RenderBuffer跟FrameBuffer绑定到一起
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.renderBuffer);
}
绘制
-(void)draw{
//设置背景色
glClearColor(0.8, 0.8, 0.8, 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);
//获取vsh/fsh路径
NSString *vertexShaderPath = [[NSBundle mainBundle]pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fragmentShaderPath = [[NSBundle mainBundle]pathForResource:@"shaderf" ofType:@"fsh"];
NSLog(@"%@ --- %@",vertexShaderPath,fragmentShaderPath);
//创建shader
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//创建program
GLuint program = glCreateProgram();
//读取vsh/fsh内容
NSString *vertexContent = [NSString stringWithContentsOfFile:vertexShaderPath encoding:NSUTF8StringEncoding error:nil];
NSString *fragmentContent = [NSString stringWithContentsOfFile:fragmentShaderPath encoding:NSUTF8StringEncoding error:nil];
//NSString转C字符串
const char *vertexSource = (GLchar *)[vertexContent UTF8String];
const char *fragmentSource = (GLchar *)[fragmentContent UTF8String];
//替换shader源码内容
glShaderSource(vertexShader, 1, &vertexSource, NULL);
glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
//编译shader
glCompileShader(vertexShader);
glCompileShader(fragmentShader);
//附着shader到program
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
//删除shader
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
self.program = program;
//连接program
glLinkProgram(self.program);
//声明变量存储连接状态
GLint linkStatus;
//获取program连接状态
glGetProgramiv(self.program, GL_LINK_STATUS, &linkStatus);
//如果连接失败
if (linkStatus == false) {
NSLog(@"连接失败");
char msg[1024];
//获取programInfo 信息
glGetProgramInfoLog(self.program, sizeof(msg), 0, &msg[0]);
//char 转 NSString
NSString *message = [NSString stringWithCString:msg encoding:NSUTF8StringEncoding];
NSLog(@"%@",message);
return;
}
NSLog(@"program 连接成功!");
//使用program
glUseProgram(self.program);
//编辑顶点坐标数组
GLfloat vertexData[] = {
0.5, -0.25, 0.0f, 1.0f, 0.0f, //右下
0.5, 0.25, -0.0f, 1.0f, 1.0f, //右上
-0.5, 0.25, 0.0f, 0.0f, 1.0f, //左上
0.5, -0.25, 0.0f, 1.0f, 0.0f, //右下
-0.5, 0.25, 0.0f, 0.0f, 1.0f, //左上
-0.5, -0.25, 0.0f, 0.0f, 0.0f, //左下
};
//定义标识符
GLuint bufferID;
//申请标识符
glGenBuffers(1, &bufferID);
//绑定缓冲区
glBindBuffer(GL_ARRAY_BUFFER, bufferID);
//将顶点数组的数据copy到顶点缓冲区中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
//从program中获取position 顶点属性
GLuint position = glGetAttribLocation(self.program, "position");
//开启顶点属性通道
glEnableVertexAttribArray(position);
//设置顶点读取方式
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *) NULL + 0);
//从program中获取textCoordinate 纹理属性
GLuint textCoordinate = glGetAttribLocation(self.program, "textCoordinate");
//开启纹理属性通道
glEnableVertexAttribArray(textCoordinate);
//设置纹理读取方式
glVertexAttribPointer(textCoordinate, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *) NULL + 3);
//获取纹理图片
CGImageRef cgImgRef = [UIImage imageNamed:@"test"].CGImage;
if (!cgImgRef) {
NSLog(@"纹理获取失败");
}
//获取图片长、宽
size_t width = CGImageGetWidth(cgImgRef);
size_t height = CGImageGetHeight(cgImgRef);
//计算图片所占字节数 长 * 宽 * RGBA占4个字节
GLubyte *byte = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
//
//创建CGContextRef画布
/*
参数1:data,指向要渲染的绘制图像的内存地址
参数2:width,bitmap的宽度,单位为像素
参数3:height,bitmap的高度,单位为像素
参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
参数5:bytesPerRow,bitmap的没一行的内存所占的比特数
参数6:colorSpace,bitmap上使用的颜色空间 kCGImageAlphaPremultipliedLast:RGBA
*/
CGContextRef contextRef = CGBitmapContextCreate(byte, width, height, 8, width * 4, CGImageGetColorSpace(cgImgRef), kCGImageAlphaPremultipliedLast);
//长宽转成float 方便下面方法使用
float w = width;
float h = height;
//绘制图片的位置
CGRect rect = CGRectMake(0, 0, w, h);
//在CGContextRef上--> 将图片绘制出来
/*
CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,CoreGraphics框架的原点在屏幕的左下角。
参数1:绘图上下文
参数2:rect坐标
参数3:绘制的图片
*/
CGContextDrawImage(contextRef, rect, cgImgRef);
//图片绘制完成后,contextRef就没用了,释放
CGContextRelease(contextRef);
//0 代表第0个纹理 对应采样器的0
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);
//载入纹理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, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, byte);
//释放byte
free(byte);
//设置纹理采样器,这里的 0 对应 glBindTexture的 0
glUniform1i(glGetUniformLocation(self.program, "colorMap"), 0);
//绘图
glDrawArrays(GL_TRIANGLES, 0, 6);
//将渲染缓冲区 呈现到 屏幕上
[self.context presentRenderbuffer:GL_RENDERBUFFER];
}
调用上述方法
-(void)layoutSubviews{
[super layoutSubviews];
//设置layer
[self setupLayer];
//设置context
[self setupContext];
//清除缓存区
[self deleteBuffers];
//设置渲染缓冲区
[self setupRenderBuffer];
//设置帧缓冲区
[self setupFrameBuffer];
//绘制
[self draw];
}
最后在ViewController中创建OpenGLESView
- (void)viewDidLoad {
[super viewDidLoad];
self.glesView = [[OpenGLESView alloc]initWithFrame:self.view.bounds];
self.view = self.glesView;
}
效果图
看了效果图之后,聪明的你肯定发现了,这是一张macOS mojave的壁纸截图,而且图片倒过来了,图片本身是正的。
于是这就引出了另外一个问题 ——OpenGL 纹理翻转
以下内容摘取自CC
关于纹理翻转
纹理翻转的原因是因为OpenGL要求纹理坐标原点(0,0)在图片左下角。
而图片信息中的原点(0,0)一般都在左上角,一行行绘制出来,就导致了图片的上下翻转。
iOS纹理翻转解决策略
第1种: 旋转矩阵翻转图形,不翻转纹理
让图形顶点坐标旋转180°, 而纹理保持原状。
GLuint rotate = glGetUniformLocation(self.myPrograme, "rotateMatrix");
float radians = 180 * 3.14159f / 180.0f;
float s = sin(radians);
float c = cos(radians);
GLfloat zRotation[16] = {
c, -s, 0, 0,
s, c, 0, 0,
0, 0, 1.0, 0,
0.0, 0, 0, 1.0
};
glUniformMatrix4fv(rotate, 1, GL_FALSE, (GLfloat *)&zRotation[0]);
第2种: 解压图片时,将图片源文件翻转
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
CGRect rect = CGRectMake(0, 0, width, height);
CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
CGContextTranslateCTM(spriteContext, 0, rect.size.height);
CGContextScaleCTM(spriteContext, 1.0, -1.0);
CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
CGContextDrawImage(spriteContext, rect, spriteImage);
CGContextRelease(spriteContext);
glBindTexture(GL_TEXTURE_2D, 0);
第3种: 修改片元着色器,纹理坐标
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main()
{
gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0-varyTextCoord.y));
}
第4种: 修改顶点着色器,纹理坐标
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main()
{
varyTextCoord = vec2(textCoordinate.x,1.0-textCoordinate.y);
gl_Position = position;
}
第5种:直接从源纹理坐标数据修改
GLfloat attrArr[] =
{
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, //右下
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, // 左上
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 左下
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // 右上
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, // 左上
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, // 右下
};
翻转过后的效果图,我用了第三种方法
那么这篇文章就到此为止了,感谢阅读。 ^ _ ^