iOS图像:OpenGL ES 入门

原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、知识点
    • 1、OpenGL ES的介绍
    • 2、图形管线
  • 二、GLKit 框架
    • 1、GLKit 框架的介绍
    • 2、绘制一张图片
    • 3、绘制一个自动旋转的立方体
    • 4、GLKit渲染金字塔
  • 三、GLSL
    • 1、GLSL 语法
    • 2、纹理图片加载
    • 3、绘制一个自动旋转的金字塔
  • 四、光照计算
    • 1、工具类里提供的方法和属性
    • 2、导入的框架和使用到的属性
    • 3、初始化上下文
    • 4、设置着色器
    • 5、设置缓冲区
    • 6、绘制GLKView
    • 7、辅助方法
  • Demo
  • 参考文献

一、知识点

1、OpenGL ES的介绍

OpenGL ES开放式图形库(OpenGL的) 用于可视化的二维和三维数据。它是一个多功能开放标准图形库,支持2D和3D数字内容创建,机械和建筑设计,虚拟原型设计,飞行模拟,视频游戏等应用程序。您可以使用OpenGL配置3D图形管道并向其提交数据。顶点被变换和点亮,组合成图元,并光栅化以创建2D图像。OpenGL旨在将函数调用转换为可以发送到底层图形硬件的图形命令。由于此底层硬件专用于处理图形命令,因此OpenGL绘图通常非常快。OpenGL for Embedded Systems (OpenGL ES)是OpenGL的简化版本,它消除了冗余功能,提供了一个既易于学习又更易于在移动图形硬件中实现的库。

OpenGL ES允许应用程序利用底层图形处理器的强大功能。iOS设备上的GPU可以执行复杂的2D和3D绘图,以及最终图像中每个像素的复杂着色计算。


2、图形管线

顶点着色器
输入
  • 着色器程序 —— 描述顶点上执⾏操作的顶点着色器程序源代码(GLSL代码)或者可执行⽂文件。
  • 顶点着色器输入(或者属性Attributes)—— 用顶点数组提供的每个顶点的数据。
  • 统一变量(uniform)—— 顶点(或者片段)着色器使用的不变数据(例如旋转矩阵)。
  • 采样器 —— 代表顶点着色器使用纹理的特殊统一变量类型。
输出
  1. 通过矩阵变换位置
  2. 计算光照公式生成逐顶点颜色
  3. 生成或变换纹理坐标
  4. 着色器必须將变换后的位置写入内建变量glPosition(点的大小glPointSize
⽚段着⾊器
输入
  • 着色器程序 —— 描述⽚段上执⾏操作的片元着⾊器程序源代码/可执行⽂件。
  • 输入变量—— 光栅化单元用插值为每个片段生成的顶点着⾊器输出。
  • 统一变量(uniform)—— 顶点(或者片段)着色器使用的不变数据。
  • 采样器 —— 代表⽚元着色器使⽤纹理的特殊统一变量类型。
输出
  1. 计算颜色
  2. 获取纹理值
  3. 往像素点中填充颜色值【纹理值/颜色值】
  4. 着色器必须将最终计算得到的颜色写入内建变量gl_ FragColor

二、GLKit 框架

1、GLKit 框架的介绍

a、EAGL

OpenGL ES 命令需要渲染上下文和绘制表面才能完成图形图像的绘制。OpenGL ES API 并没有提供如何创建渲染上下文或者上下文如何连接到原⽣窗⼝系统。Apple 提供自己的EAGL作为窗口。

b、GLKit 框架简介

GLKit 框架使⽤数学库,背景纹理加载,预先创建的着色器效果,以及标准视图和视图控制器来实现渲染。简单的来说,GLKit 就是为了让 iOS 开发者在使用OpenGL ES 或 OpenGL 的时候更简便更容易上手,封装了一堆库,我们直接只写核心代码就行了。虽然苹果弃用 OpenGL ES ,但 iOS 开发者可以继续使用。


c、GLKView 继承 UIView,提供绘制场所(View)
❶ 初始化视图
- (instancetype)initWithFrame:(CGRect)frame context:(EAGLContext *)context;
❷ 设置视图的代理
delegate
❸ 配置帧缓冲区对象
drawableColorFormat //颜色缓冲区的格式
drawableDepthFormat //深度缓冲区的格式
drawableStencilFormat //模板缓冲区的格式
drawableMultisample //多重采样缓冲区的格式
❹ 设置帧缓冲区属性
drawableHeight //底层缓存区对象的高度
drawableWidth //底层缓存区对象的宽度
❺ 绘制视图的内容
context //存储绘制视图内容时使用的 OpenGL ES 上下文状态
bindDrawable //将底层 FrameBuffer 对象绑定到 OpenGL ES
enableSetNeedsDisplay //布尔值,指定视图是否响应使得视图内容无效的消息
display //立即重绘视图内容
snapshot //UIImage 类型,绘制视图内容并将其作为新图像对象返回
❻ 删除视图 FrameBuffer 对象
deleteDrawable //删除与视图关联的可绘制对象
❼ 实现 GLKViewDelegate 代理方法
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect; //绘制视图内容

d、GLKViewController 继承 UIViewController,用于绘制视图内容的管理与呈现
❶ 配置帧速率
preferredFramesPerSecond //视图控制器调用视图以及更新视图内容的速率,默认为30
framesPerSecond //视图控制器调用视图以及更新视图内容的实际速率
❷ 配置 GLKViewController 代理
delegate
❸ 控制帧更新
paused //布尔值,渲染循环是否已暂停
pauseOnWillResignActive //布尔值,当前程序重新激活活动状态时视图控制器是否自动暂停渲染循环
resumeOnDidBecomeActive //布尔值,当前程序变为活动状态时视图控制是否自动恢复呈现循环
❹ 获取有关 View 的更新信息
framesPerSecond //视图控制器自创建以来发送的帧更新数
timeSinceFirstResume //视图控制器第一次恢复发送更新事件以来经过的时间量
timeSinceLastResume //自上次视图控制器恢复发送更新事件以来更新的时间量
timeSinceLastUpdate //自上次视图控制器调用委托方法以及经过的时间量
timeSinceLastDraw //自上次视图控制器调用视图 display 方法以来经过的时间量
❺ 实现代理方法
- (void)glkViewControllerUpdate:(GLKViewController *)controller; //处理更新事件
- (void)glkViewController:(GLKViewController *)controller willPause:(BOOL)pause; //暂停/恢复通知

e、GLKBaseEffect 是 GLKit 提供的一种简单的光照/着色系统,用于基于着色器 OpenGL 渲染
❶ label 给 Effect(效果)命名
❷ transform 绑定效果时应用于顶点数据的模型视图,投影和纹理变换
❸ 配置光照效果
lightingType //用于计算每个片段的光照策略
GLKLightingTypePerVertex //表示在三⻆形中每个顶点执行光照计算,然后在三⻆形进⾏插值
GLKLightingTypePerPixel //表示光照计算的输入在三角形内插入,并且在每个⽚段执行光照计算
❹ 配置光照
lightModelTwoSided //布尔值,表示为基元的两侧计算光照
material //计算渲染图元光照使⽤的材质属性
lightModelAmbientColor //环境颜⾊,应⽤效果渲染的所有图元
light0,light1,light2 //分别为场景中第 1、2、3 个光照属性,GLKit 最多就支持3个光照
❺ 配置纹理
texture2d0 //readonly,第一个纹理属性
texture2d1 //readonly,第二个纹理属性,如果要支持多个纹理,就不能用 GLKit 得自己写了
textureOrder //纹理应⽤于渲染图元的顺序
❻ fog 应用于场景的雾属性
❼ 配置颜色信息
colorMaterialEnabled //布尔值,表示计算光照与材质交互时是否使用颜色顶点属性
useConstantColor //布尔值,指示是否使用常量颜⾊
constantColor //不提供每个顶点颜色数据时使⽤的常量颜⾊
❽ 准备绘制效果
- (void) prepareToDraw //准备渲染效果(绘制时同步所有效果更改以保持一致状态),绘制之前必须写

2、绘制一张图片

使用OpenGL ES绘制一张图片

a、导入头文件和创建属性

创建一个带默认 storyboard 的工程,为了方便,直接把自带的 ViewClass类型改为了 GLKView,当然我们也可以用代码 alloc 创建。

然后我们在.h文件中导入头文件 GLKit,并且把 ViewController 的父类改为 GLKViewController

#import <UIKit/UIKit.h>
#import <GLKit/GLKit.h>

@interface ViewController : GLKViewController

@end

接下来在 .m 文件中导入头文件。

#import <OpenGLES/ES3/gl.h>
#import <OpenGLES/ES3/glext.h>

定义两个全局变量 EAGLContextGLKBaseEffect

@implementation ViewController
{
    EAGLContext *context;
    GLKBaseEffect *effect;
}

方法调用的整体流程

- (void)viewDidLoad
{
    [super viewDidLoad];

    // 1.OpenGL ES相关初始化
    [self setUpConfig];
    
    // 2.加载顶点/纹理坐标数据
    [self setUpVertexData];
    
    // 3.加载纹理数据(使用GLBaseEffect)
    [self setUpTexture];
}

b、OpenGL ES相关初始化
- (void)setUpConfig
{
}
❶ 初始化上下文
// EAGLContext是苹果iOS平台下实现OpenGLES 渲染层
context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];

// 判断context是否创建成功
if (!context)
{
    NSLog(@"Create ES context Failed");
}

// 设置当前上下文
[EAGLContext setCurrentContext:context];
❷ 获取GLKView
GLKView *view =(GLKView *) self.view;
view.context = context;
❸ 配置视图创建的渲染缓存区
view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;// 颜色缓存区格式
view.drawableDepthFormat = GLKViewDrawableDepthFormat16;// 深度缓存区格式
❹ 设置背景颜色
glClearColor(1, 0, 0, 1.0);

c、加载顶点/纹理坐标数据
- (void)setUpVertexData
{
}
❶ 设置顶点数组(顶点坐标、纹理坐标)

纹理坐标系取值范围[0,1]。原点是左下角(0,0),故而(0,0)是纹理图像的左下角,点(1,1)是右上角。顶点坐标、纹理坐标都放在了一个数组里,前面xyz是顶点坐标,后面的st是纹理坐标。

GLfloat vertexData[] =
{
    0.5, -0.5, 0.0f,    1.0f, 0.0f, //右下
    0.5, 0.5, -0.0f,    1.0f, 1.0f, //右上
    -0.5, 0.5, 0.0f,    0.0f, 1.0f, //左上
    
    0.5, -0.5, 0.0f,    1.0f, 0.0f, //右下
    -0.5, 0.5, 0.0f,    0.0f, 1.0f, //左上
    -0.5, -0.5, 0.0f,   0.0f, 0.0f, //左下
};
❷ 开辟顶点缓存区

性能更高的做法是,提前分配一块显存,将顶点数据预先传入到显存当中,这部分的显存,就被称为顶点缓冲区。

// 创建顶点缓存区标识符ID
GLuint bufferID;
glGenBuffers(1, &bufferID);
// 绑定顶点缓存区(存储数组的缓冲区)
glBindBuffer(GL_ARRAY_BUFFER, bufferID);
// 将顶点数组的数据copy到顶点缓存区中(GPU显存中)
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
❸ 打开读取通道

在iOS中,默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的。意味着顶点数据在着色器端(服务端)是不可用的,即使你已经使用glBufferData方法将顶点数据从内存拷贝到顶点缓存区中(GPU显存中)。所以必须由glEnableVertexAttribArray方法打开通道指定访问属性才能让顶点着色器能够访问到从CPU复制到GPU的数据。

// 允许顶点着色器读取GPU(服务器端)数据
glEnableVertexAttribArray(GLKVertexAttribPosition);// 顶点
  • index:指定要修改的顶点属性的索引值
  • size:每次读取数量。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a),纹理则是2个)
  • type:指定数组中每个组件的数据类型
  • normalized:指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE
  • stride:指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为它们是紧密排列在一起的。初始值为0
  • ptr:指定一个指针,指向数组中第一个顶点属性的第一个组件。初始值为0
// 上传顶点数据到显存的方法(设置合适的方式从buffer里面读取数据)(每次读取3个数据xyz,连续顶点之间的偏移量为5即每行5个元素,读取数据的首地址为0)
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 0);

纹理坐标数据

glEnableVertexAttribArray(GLKVertexAttribTexCoord0);// 纹理
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 3);

d、加载纹理数据(使用GLBaseEffect)
- (void)setUpTexture
{
}

获取纹理图片路径

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"kunkun" ofType:@"jpg"];

设置纹理参数。纹理坐标原点是左下角,但是图片显示原点应该是左上角,所以需要图片翻转

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:@(1),GLKTextureLoaderOriginBottomLeft, nil];
GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:options error:nil];

使用苹果GLKit提供GLKBaseEffect完成着色器工作(顶点/片元)

effect = [[GLKBaseEffect alloc]init];
effect.texture2d0.enabled = GL_TRUE;// 使用纹理
effect.texture2d0.name = textureInfo.name;// 纹理的名称

e、绘制视图的内容

GLKView对象使其OpenGL ES上下文成为当前上下文,并将其framebuffer绑定为OpenGL ES呈现命令的目标,然后委托方法应该绘制视图的内容。

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    glClear(GL_COLOR_BUFFER_BIT);
    
    // 准备绘制
    [effect prepareToDraw];
    
    // 开始绘制
    glDrawArrays(GL_TRIANGLES, 0, 6);// 三角形,从0个顶点开始画,画6个
}

3、绘制一个自动旋转的立方体

a、使用到的属性

顶点结构体包括顶点坐标、纹理坐标、法线

typedef struct {
    GLKVector3 positionCoord;   //顶点坐标
    GLKVector2 textureCoord;    //纹理坐标
    GLKVector3 normal;          //法线
} Vertex;

正方体有6个面,每个面有两个三角形共6个顶点,包括重复顶点共36个

static NSInteger const kCoordCount = 36;

b、各方法的调用流程
- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blackColor];
   
    // 1.OpenGL ES 相关初始化
    [self commonInit];
    
    // 2.顶点/纹理坐标数据
    [self vertexDataSetup];
    
    // 3.添加CADisplayLink
    [self addCADisplayLink];
}

c、OpenGL ES 相关初始化
❶ 创建context后设置为当前context
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
[EAGLContext setCurrentContext:context];
❷ 创建GLKView并设置代理
CGRect frame = CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.width);
self.glkView = [[GLKView alloc] initWithFrame:frame context:context];
self.glkView.backgroundColor = [UIColor clearColor];
self.glkView.delegate = self;
❸ 使用深度缓存
self.glkView.drawableDepthFormat = GLKViewDrawableDepthFormat24;
// 深度缓存区默认深度值计算后取值范围是(0, 1),由于正方体围绕Z轴旋转,往屏幕外旋转的话需要将0和1反过来
glDepthRangef(1, 0);
❹ 将GLKView添加到self.view上
[self.view addSubview:self.glkView];
❺ 获取纹理图片
NSString *imagePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"kunkun.jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
❻ 设置纹理参数
// 纹理坐标原点是左下角,但是图片显示原点应该是左上角,所以需要图片翻转
NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft : @(YES)};
GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:[image CGImage]
                                                           options:options
                                                             error:NULL];
❼ 使用baseEffect进行纹理的设置
self.baseEffect = [[GLKBaseEffect alloc] init];
self.baseEffect.texture2d0.name = textureInfo.name;
self.baseEffect.texture2d0.target = textureInfo.target;

d、顶点/纹理坐标数据
❶ 开辟顶点数据空间(数据结构Vertex 大小 * 顶点个数)
self.vertices = malloc(sizeof(Vertex) * kCoordCount);
❷ 绘制以(0,0,0)为中心,边长为1的立方体
  • -0.5, 0.5, 0.5 这3个点表示的是顶点坐标xyz
  • 0, 1 这2个点表示的是纹理坐标st,当纹理图片存在正反关系的时候,映射的时候就需要考虑位置关系的改变
  • 暂不考虑法线所以不设置其值
// 前面的第一个三角形
self.vertices[0] = (Vertex){{-0.5, 0.5, 0.5},  {0, 1}};
self.vertices[1] = (Vertex){{-0.5, -0.5, 0.5}, {0, 0}};
self.vertices[2] = (Vertex){{0.5, 0.5, 0.5},   {1, 1}};

// 前面的第二个三角形
self.vertices[3] = (Vertex){{-0.5, -0.5, 0.5}, {0, 0}};
self.vertices[4] = (Vertex){{0.5, 0.5, 0.5},   {1, 1}};
self.vertices[5] = (Vertex){{0.5, -0.5, 0.5},  {1, 0}};

// 上下左右后面
.......
❸ 开辟缓存区将顶点数据存储到显存
glGenBuffers(1, &_vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
GLsizeiptr bufferSizeBytes = sizeof(Vertex) * kCoordCount;
glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_STATIC_DRAW);
❹ 允许顶点着色器读取GPU(服务器端)数据
  • sizeof(Vertex):跨行的是结构体
  • offsetof(Vertex, positionCoord):每次读取结构体中的顶点数据/纹理数据
glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), NULL + offsetof(Vertex, positionCoord));

glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), NULL + offsetof(Vertex, textureCoord));

e、GLKViewDelegate 进行绘制
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    // 1.开启深度测试
    glEnable(GL_DEPTH_TEST);
    // 2.清除颜色缓存区&深度缓存区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // 3.准备绘制
    [self.baseEffect prepareToDraw];
    
    // 4.绘图
    glDrawArrays(GL_TRIANGLES, 0, kCoordCount);
}

f、旋转时需要不断重新绘制
❶ 添加计时器
// CADisplayLink类似定时器,提供一个周期性调用的方法,属于QuartzCore.framework中
- (void)addCADisplayLink
{
    self.angle = 0;
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
❷ 不断重新绘制
  • GLKMathDegreesToRadians:是将度数转化为弧度
  • XYZ:001表示围绕Z轴旋转,010表示围绕Y轴旋转,0.3, 1, -0.7表示围绕任意轴旋转
- (void)update
{
    // 1.计算旋转度数。每次加5度
    self.angle = (self.angle + 5) % 360;
    
    // 2.旋转、平移、缩放都属于模型视图矩阵的变化(不是投影矩阵)
    self.baseEffect.transform.modelviewMatrix = GLKMatrix4MakeRotation(GLKMathDegreesToRadians(self.angle), 0.3, 1, -0.7);
    
    // 3.重新渲染
    [self.glkView display];
}

g、清空
❶ 重置当前上下文
if ([EAGLContext currentContext] == self.glkView.context)
{
    [EAGLContext setCurrentContext:nil];
}
❷ 释放顶点数据
if (_vertices)
{
    free(_vertices);
    _vertices = nil;
}
❸ 释放_vertexBuffer这个ID对应的缓存区
if (_vertexBuffer)
{
    glDeleteBuffers(1, &_vertexBuffer);
    _vertexBuffer = 0;
}
❹ displayLink 失效
[self.displayLink invalidate];

4、GLKit渲染金字塔

a、导入的框架和使用到的属性
导入的框架
#import <GLKit/GLKit.h>
@interface ViewController : GLKViewController
使用到的私有属性
@interface ViewController ()

@property(nonatomic,strong)EAGLContext *context;
@property(nonatomic,strong)GLKBaseEffect *effect;

@property(nonatomic,assign)int count;

// 旋转的度数
@property(nonatomic,assign)float XDegree;
@property(nonatomic,assign)float YDegree;
@property(nonatomic,assign)float ZDegree;

// 是否旋转X,Y,Z
@property(nonatomic,assign) BOOL XB;
@property(nonatomic,assign) BOOL YB;
@property(nonatomic,assign) BOOL ZB;

@end
成员变量
@implementation ViewController
{
    dispatch_source_t timer;
}

b、新建图层
- (void)setupContext
{
    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    
    GLKView *view = (GLKView *)self.view;
    view.context = self.context;
    view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
    view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
    
    [EAGLContext setCurrentContext:self.context];
    glEnable(GL_DEPTH_TEST);
}

c、渲染图形
❶ 顶点数据
// 前3个元素,是顶点数据;中间3个元素,是顶点颜色值,最后2个是纹理坐标
GLfloat attrArr[] =
{
    -0.5f, 0.5f, 0.0f,      0.0f, 0.0f, 0.5f,       0.0f, 1.0f,//左上
    0.5f, 0.5f, 0.0f,       0.0f, 0.5f, 0.0f,       1.0f, 1.0f,//右上
    -0.5f, -0.5f, 0.0f,     0.5f, 0.0f, 1.0f,       0.0f, 0.0f,//左下
    0.5f, -0.5f, 0.0f,      0.0f, 0.0f, 0.5f,       1.0f, 0.0f,//右下
    0.0f, 0.0f, 1.0f,       1.0f, 1.0f, 1.0f,       0.5f, 0.5f,//顶点
};
❷ 绘图索引
GLuint indices[] =
{
    0, 3, 2,
    0, 1, 3,
    0, 2, 4,
    0, 4, 1,
    2, 3, 4,
    1, 4, 3,
};
// 计算顶点个数
self.count = sizeof(indices) /sizeof(GLuint);
❸ 将顶点数组放入数组缓冲区中 GL_ARRAY_BUFFER
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_STATIC_DRAW);
❹ 将索引数组存储到索引缓冲区 GL_ELEMENT_ARRAY_BUFFER
GLuint index;
glGenBuffers(1, &index);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
❺ 使用顶点、颜色、纹理数据
// 使用顶点数据
glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 8, NULL);

// 使用颜色数据
glEnableVertexAttribArray(GLKVertexAttribColor);
glVertexAttribPointer(GLKVertexAttribColor, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 8, (GLfloat *)NULL + 3);

// 使用纹理数据
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 8, (GLfloat *)NULL + 6);
❻ 设置纹理参数
// 获取纹理路径
NSString *filePath = [[NSBundle mainBundle]pathForResource:@"cTest" ofType:@"jpg"];
// 解决纹理翻转问题
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:@"1",GLKTextureLoaderOriginBottomLeft, nil];
// 加载纹理
GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:options error:nil];
❼ 添加着色器
self.effect = [[GLKBaseEffect alloc]init];
self.effect.texture2d0.enabled = GL_TRUE;
self.effect.texture2d0.name = textureInfo.name;
❽设置投影矩阵
CGSize size = self.view.bounds.size;
float aspect = fabs(size.width / size.height);
GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(90.0), aspect, 0.1f, 10.f);
projectionMatrix = GLKMatrix4Scale(projectionMatrix, 1.0f, 1.0f, 1.0f);
self.effect.transform.projectionMatrix = projectionMatrix;
❾ 模型视图矩阵
GLKMatrix4 modelViewMatrix = GLKMatrix4Translate(GLKMatrix4Identity, 0.0f, 0.0f, -2.0f);
self.effect.transform.modelviewMatrix = modelViewMatrix;
❿ 使用定时器修改旋转角度
double seconds = 0.1;
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC, 0.0);
dispatch_source_set_event_handler(timer, ^{
   
    self.XDegree += 0.1f * self.XB;
    self.YDegree += 0.1f * self.YB;
    self.ZDegree += 0.1f * self.ZB ;
    
});
dispatch_resume(timer);

d、场景数据变化
- (void)update
{
    GLKMatrix4 modelViewMatrix = GLKMatrix4Translate(GLKMatrix4Identity, 0.0f, 0.0f, -2.0f);
    
    modelViewMatrix = GLKMatrix4RotateX(modelViewMatrix, self.XDegree);
    modelViewMatrix = GLKMatrix4RotateY(modelViewMatrix, self.YDegree);
    modelViewMatrix = GLKMatrix4RotateZ(modelViewMatrix, self.ZDegree);
    
    self.effect.transform.modelviewMatrix = modelViewMatrix;
}

e、代理方法
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    [self.effect prepareToDraw];
    glDrawElements(GL_TRIANGLES, self.count, GL_UNSIGNED_INT, 0);
}

三、GLSL

1、GLSL 语法

a、CPU 与 GPU 之间的关系

CPU在执⾏任务的时候,⼀个时刻只会处理⼀个数据,不存在真
正意义上的并行,⽽GPU则有多个处理器核,在一个时刻可以并
行处理多个数据。GPU具有⾼并行的结构,所以在处理图形数据和复杂算法比CPU更加有效率。


b、向量的运算

声明4分量的float 类型向量

vec4 V1;

声明向量并对其进行构造

vec4 V2 = vec4(1,2,3,4);

向量的加法和点乘

vec4 v;
vec4 vOldPos = vec4(1,2,3,4);
vec4 vOffset = vec4(1,2,3,4);

v = vOldPos + vOffset;
v = vNewPos;
v += vec4(10,10,10,10);
v = vOldPos * vOffset;
v *= 5;

向量中单独的成分可以通过 {x,y,z,w}, {r,g,b,a}或者{s,t,p,q}的记法来表示。这些不同的记法用于 顶点,颜色,纹理坐标。

例如有向量 v1 和 v2,可以通过{x,y,z,w}, {r,g,b,a}或者{s,t,p,q}来取出向量中的元素值。

vec3 v1 = {0.5, 0.35, 0.7};
vec4 v2 = {0.1, 0.2, 0.3, 0.4};

// 通过 x,y,z,w
v2.x = 3.0f;
v2.xy = vec2(3.0f,4.0f);
v2.xyz = vec3(3,0f,4,0f,5.0f);

// 通过 r,g,b,a
v2.r = 3.0f;
v2.rgba = vec4(1.0f,1.0f,1.0f,1.0f);

// 通过 s,t,q,r
v2.stqr = vec2(1.0f, 0.0f, 0.0f, 1.0f);

向量还支持一次性对所有分量操作

// 分开算
v1.x = v2.x +5.0f; 
v1.y = v2.y +4.0f; 
v1.z = v2.z +3.0f;

// 一次性
v1.xyz = v2.xyz + vec3(5.0f,4.0f,3.0f);

c、矩阵的运算

创建矩阵

mat4 m1,m2,m3;

构造单元矩阵

mat4 m2 = mat4(1.0f,0.0f,0.0f,0.0f
                    0.0f,1.0f,0.0f,0.0f,
                    0.0f,0.0f,1.0f,0.0f,
                    0.0f,0.0f,0.0f,1.0f);
// 或者
mat4 m4 = mat4(1.0f);

d、变量存储限定符
varying
  • 顶点着色器的输出,主要负责在 vertexfragment之间传递变量。
  • 例如颜色或者纹理坐标,作为片段着色器的只读输入数据。
uniform
  • 在着色器执行期间一致变量的值是不变的。
  • const 常量不同的是,这个值在编译时期是未知的是由着色器外部初始化的。
  • 一致变量在顶点着色器和片段着色器之间是共享的。
attribute
  • 表示只读的顶点数据,只用在顶点着色器中。
  • 数据来自当前的顶点状态或者顶点数组。

e、函数参数限定符

GLSL 允许自定义函数,但参数默认是以值形式(in 限定符)传入的,也就是说任何变量在传入时都会被拷贝一份,若想以引用方式传参,需要增加函数参数限定符。

in
  • 用在函数的参数中,表示这个参数是输入的,在函数中改变这个值,并不会影响对调用的函数产生副作用。
  • 这个是函数参数默认的修饰符。
out
  • 用在函数的参数中,表示该参数是输出参数,值是会改变的。
inout
  • 用在函数的参数,表示这个参数即是输入参数也是输出参数。

2、纹理图片加载

a、编写GLSL文件

前面我们使用 GLKit 加载了一个立体图形,但是我们知道苹果提供的 GLKit 的功能是有限的,所以这次我们就不用 GLKitGLKBaseEffect,而使用编译链接自定义的着色器(shader),用简单的 GLSL 语言来实现顶点、片元着色器,并实现加载一张图片。

我们写的 GLSL 代码对 Xcode 来说就是一长串的字符串,所以后缀名用什么都无所谓,当我们阅读 GPUImage 源码、ijkplayer 源码的时候,可以发现它们的 shader 就是用普通的字符串来存储的,这是没有任何问题的,缺点只是不易于阅读和书写。所以我们一般会创建一个文件,后缀为.vsh.fsh和通用后缀 glsl.vsh 代表 verterx shader 顶点着色器,.fsh 代表 fragment shader 片元着色器。

如果着色器文件命名的是通用的后缀 glsl,那如何区分哪个是顶点着色器哪个是片元着色器?顶点着色器里一定有对内建函数gl_Position的赋值,片元着色器内则有对内建函数gl_FragColor的赋值,可以以此来判断。

顶点着色器
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;

void main()
{
    varyTextCoord = textCoordinate;
    gl_Position = position;
}
片元着色器
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;

void main()
{
    gl_FragColor = texture2D(colorMap, varyTextCoord);
}

b、准备工作
导入头文件
#import <OpenGLES/ES2/gl.h>
使用到的属性
@interface View()

// 绘制OpenGL ES内容的图层,继承自CALayer
@property(nonatomic,strong) CAEAGLLayer *eagLayer;
// 上下文
@property(nonatomic,strong) EAGLContext *context;

// 渲染缓冲区
@property(nonatomic,assign) GLuint colorRenderBuffer;
// 帧缓冲区
@property(nonatomic,assign) GLuint colorFrameBuffer;

// 程序
@property(nonatomic,assign) GLuint programe;

@end
GLSL 加载图片的完整流程
- (void)layoutSubviews
{
    // 1.设置图层
    [self setupLayer];
    
    // 2.设置图形上下文
    [self setupContext];
    
    // 3.清空缓存区
    [self deleteRenderAndFrameBuffer];

    // 4.设置RenderBuffer
    [self setupRenderBuffer];
    
    // 5.设置FrameBuffer
    [self setupFrameBuffer];
    
    // 6.开始绘制
    [self renderLayer];
}

c、设置图层
- (void)setupLayer
{
}
❶ 创建特殊图层。重写layerClass,将View返回的图层从CALayer替换成CAEAGLLayer
self.eagLayer = (CAEAGLLayer *)self.layer;

+ (Class)layerClass
{
    return [CAEAGLLayer class];
}
❷ 设置scale
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
❸ 设置描述属性。设置不维持渲染内容以及颜色格式为RGBA8
self.eagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,nil];

d、设置图形上下文
- (void)setupContext
{
}
❶ 指定OpenGL ES 渲染API版本
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES3;
❷ 创建图形上下文
EAGLContext *context = [[EAGLContext alloc] initWithAPI:api];
❸ 判断是否创建成功
if (!context)
{
    NSLog(@"创建图形上下文失败!");
    return;
}
❹ 设置当前图形上下文
if (![EAGLContext setCurrentContext:context])
{
    NSLog(@"设置当前图形上下文失败!");
    return;
}
❺ 将局部context变成全局的
self.context = context;

e、使用之前清空渲染和帧缓存区
- (void)deleteRenderAndFrameBuffer
{
    glDeleteBuffers(1, &_colorRenderBuffer);// 根据ID清空缓存区
    self.colorRenderBuffer = 0;// ID重置
    
    // frame buffer 相当于render buffer的管理者
    glDeleteBuffers(1, &_colorFrameBuffer);
    self.colorFrameBuffer = 0;
}

f、设置渲染缓冲区
- (void)setupRenderBuffer
{
}
❶ 定义一个缓存区ID
GLuint buffer;
❷ 申请一个缓存区标志
glGenRenderbuffers(1, &buffer);
self.colorRenderBuffer = buffer;
❸ 将标识符绑定到GL_RENDERBUFFER
glBindRenderbuffer(GL_RENDERBUFFER, self.colorRenderBuffer);
❹ 将可绘制对象的CAEAGLLayer的存储绑定到renderBuffer对象
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.eagLayer];

g、设置帧缓冲区
- (void)setupRenderBuffer
{
}
❶ 定义一个缓存区ID
GLuint buffer;
❷ 申请一个缓存区标志
glGenRenderbuffers(1, &buffer);
self.colorFrameBuffer = buffer;
❸ 将标识符绑定到GL_FRAMEBUFFER
glBindFramebuffer(GL_FRAMEBUFFER, self.colorFrameBuffer);
❹ 生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定
// 调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.colorRenderBuffer);

h、开始绘制
- (void)renderLayer
{
    // 设置清屏颜色并清除屏幕
    glClearColor(0.3f, 0.45f, 0.5f, 1.0f);
    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 *vertFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fragFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
NSLog(@"vertFile:%@",vertFile);
NSLog(@"fragFile:%@",fragFile);
❷ 加载shader
self.programe = [self loadShaders:vertFile Withfrag:fragFile];
❸ 链接程序并获取链接状态
glLinkProgram(self.programe);
GLint linkStatus;
// 判断是否链接成功
glGetProgramiv(self.programe, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE)
{
    // 打印链接失败的原因
    GLchar message[512];
    glGetProgramInfoLog(self.programe, sizeof(message), 0, &message[0]);
    NSString *messageString = [NSString stringWithUTF8String:message];
    NSLog(@"程序链接错误原因: %@",messageString);
    return;
}
NSLog(@"程序链接成功!");
// 使用链接后的程序
glUseProgram(self.programe);
❹ 设置顶点、纹理坐标
// 前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 attrBuffer;
// 申请一个缓存区标识符
glGenBuffers(1, &attrBuffer);
// 将attrBuffer绑定到GL_ARRAY_BUFFER标识符上
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
// 把顶点数据从CPU内存复制到GPU上
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
❻ 将顶点数据通过programe中的传递到顶点着色程序的position

用来获取vertex attribute的入口。第二个参数字符串必须和shaderv.vsh中的输入变量position保持一致

GLuint position = glGetAttribLocation(self.programe, "position");

设置合适的格式从buffer里面读取数据

glEnableVertexAttribArray(position);

设置读取方式,将数据传递过去

  • 参数1:index,顶点数据的索引
  • 参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
  • 参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
  • 参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE
  • 参数5:stride,连续顶点属性之间的偏移量,默认为0
  • 参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
❼ 处理纹理数据
GLuint textCoor = glGetAttribLocation(self.programe, "textCoordinate");
glEnableVertexAttribArray(textCoor);
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL + 3);
❽ 加载纹理
[self setupTexture:@"kunkun"];
❾ 设置纹理采样器
glUniform1i(glGetUniformLocation(self.programe, "colorMap"), 0);// 获取第1个纹理(写的0)
❿ 数组绘图
glDrawArrays(GL_TRIANGLES, 0, 6);
🔚 从渲染缓存区显示到屏幕上
[self.context presentRenderbuffer:GL_RENDERBUFFER];

I、加载shader
- (GLuint)loadShaders:(NSString *)vert Withfrag:(NSString *)frag
{
    // 定义2个临时着色器对象
    GLuint verShader, fragShader;
    ......
    return program;
}
❶ 创建program
GLint program = glCreateProgram();
❷ 编译顶点着色程序、片元着色器程序
  • 参数1:编译完存储的底层地址
  • 参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
  • 参数3:文件路径
[self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
❸ 将着色器与程序附着,创建最终的程序
glAttachShader(program, verShader);
glAttachShader(program, fragShader);
❹ 删除着色器,释放不需要的shader
glDeleteShader(verShader);
glDeleteShader(fragShader);

J、编译shader
❶ 读取文件路径字符串,C语言字符串
NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar* source = (GLchar *)[content UTF8String];
❷ 根据type类型创建一个shader
*shader = glCreateShader(type);
❸ 将着色器源码附加到着色器对象上
  • shader:要编译的着色器对象
  • numOfStrings:传递的源码字符串数量 1个
  • strings:着色器程序的源码(真正的着色器程序源码)
  • lenOfStrings:长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
glCompileShader(*shader);

H、从图片中加载纹理
- (GLuint)setupTexture:(NSString *)fileName
{
}
❶ 将 UIImage 转换为 CGImageRef 进行解压图片
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
// 判断图片是否获取成功
if (!spriteImage)
{
    NSLog(@"加载图片失败:%@", fileName);
    exit(1);
}
❷ 读取图片的大小,宽和高
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
// 获取图片字节数 宽*高*4(RGBA)
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
❸ 创建上下文
  • data:指向要渲染的绘制图像的内存地址
  • width:bitmap的宽度,单位为像素
  • height:bitmap的高度,单位为像素
  • bitPerComponent:内存中像素的每个组件的位数,比如32位RGBA,就设置为8
  • bytesPerRow:bitmap的没一行的内存所占的比特数
  • colorSpace:bitmap上使用的颜色空间
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
❹ 在CGContextRef上使用默认方式将图片绘制出来
  • CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角
  • 参数1:绘图上下文
  • 参数2:rect坐标
  • 参数3:绘制的图片
CGRect rect = CGRectMake(0, 0, width, height);
CGContextDrawImage(spriteContext, rect, spriteImage);
CGContextRelease(spriteContext);// 画图完毕就释放上下文
❺ 绑定纹理到默认的纹理ID
glBindTexture(GL_TEXTURE_2D, 0);// 默认1个纹理(写0)
❻ 设置纹理属性
  • 参数1:纹理维度
  • 参数2:线性过滤、为s,t坐标设置模式
  • 参数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);
❼ 载入纹理2D数据
  • 参数1:纹理模式,GL_TEXTURE_1DGL_TEXTURE_2DGL_TEXTURE_3D
  • 参数2:加载的层次,一般设置为0
  • 参数3:纹理的颜色值GL_RGBA
  • 参数4:
  • 参数5:
  • 参数6:border,边界宽度
  • 参数7:format
  • 参数8:type
  • 参数9:纹理数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
❽ 释放spriteData
free(spriteData);

I、纹理翻转的策略
修改片元着色器中的纹理坐标
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;

void main() {
    // 这里是修改了片元着色器的代码,x 坐标不动,将 y 坐标改为 1-y,这样就达到了翻转的效果
    //gl_FragColor = texture2D(colorMap, varyTextCoord);
    gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0-varyTextCoord.y));
}
修改顶点着色器中的纹理坐标
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;

void main() {
    // 在顶点着色器传入的时候,就翻转。相比上面的片元着色器,执行次数要少很多,有几个顶点执行几次
    //varyTextCoord = textCoordinate;
    varyTextCoord = vec2(textCoordinate.x,1.0-textCoordinate.y);
    gl_Position = position;
}

3、绘制一个自动旋转的金字塔

a、编写GLSL文件
顶点着色器
attribute vec4 position;// 每个点
attribute vec4 positionColor;// 每个点的颜色

uniform mat4 projectionMatrix;// 立体图形的投影矩阵
uniform mat4 modelViewMatrix;// 模型视图矩阵用来旋转

varying lowp vec4 varyColor;// 将颜色传递到片段着色器

void main()
{
    varyColor = positionColor;
    
    vec4 vPos;// 传递计算后的顶点
    vPos = projectionMatrix * modelViewMatrix * position;
    gl_Position = vPos;
}
片元着色器
varying lowp vec4 varyColor;// 从顶点着色器传过来的颜色
void main()
{
    gl_FragColor = varyColor;// 将颜色值给着色器
}

b、绘制

在绘制函数中获取顶点着色程序、片元着色器程序文件位置

NSString* vertFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"glsl"];
NSString* fragFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"glsl"];

判断self.program是否存在,存在则清空其文件

if (self.program)
{
    glDeleteProgram(self.program);
    self.program = 0;
}

加载程序到program中来

self.program = [self loadShader:vertFile frag:fragFile];

链接程序

glLinkProgram(self.program); 

获取链接状态

GLint linkSuccess;
glGetProgramiv(self.program, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE)
{
    GLchar messages[256];
    glGetProgramInfoLog(self.program, sizeof(messages), 0, &messages[0]);
    NSString *messageString = [NSString stringWithUTF8String:messages];
    NSLog(@"error%@", messageString);
    
    return ;
}
else
{
    glUseProgram(self.program);
}

创建顶点数组。前3顶点值(x,y,z),后3位颜色值(RGB)

GLfloat attrArr[] =
{
    -0.5f, 0.5f, 0.0f,      1.0f, 0.0f, 1.0f, //左上0
    0.5f, 0.5f, 0.0f,       1.0f, 0.0f, 1.0f, //右上1
    -0.5f, -0.5f, 0.0f,     1.0f, 1.0f, 1.0f, //左下2
    
    0.5f, -0.5f, 0.0f,      1.0f, 1.0f, 1.0f, //右下3
    0.0f, 0.0f, 1.0f,       0.0f, 1.0f, 0.0f, //顶点4
};

创建索引数组

GLuint indices[] =
{
    0, 3, 2,
    0, 1, 3,
    0, 2, 4,
    0, 4, 1,
    2, 3, 4,
    1, 4, 3,
};

判断顶点缓存区是否为空,如果为空则申请一个缓存区标识符

if (self.vertices == 0)
{
    glGenBuffers(1, &_vertices);
}

处理顶点数据

// 将_vertices绑定到GL_ARRAY_BUFFER标识符上
glBindBuffer(GL_ARRAY_BUFFER, _vertices);
// 把顶点数据从CPU内存复制到GPU上
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
// 将顶点数据通过programe中的传递到顶点着色程序的position
GLuint position = glGetAttribLocation(self.program, "position");
// 打开position
glEnableVertexAttribArray(position);
// 设置读取方式
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, NULL);

处理顶点颜色值

// 用来获取vertex attribute的入口的
GLuint positionColor = glGetAttribLocation(self.program, "positionColor");
// 设置合适的格式从buffer里面读取数据
glEnableVertexAttribArray(positionColor);
// 设置读取方式
glVertexAttribPointer(positionColor, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, (float *)NULL + 3);

找到program中的projectionMatrixmodelViewMatrix2个矩阵的地址。如果找到则返回地址,否则返回-1表示没有找到2个对象

GLuint projectionMatrixSlot = glGetUniformLocation(self.program, "projectionMatrix");
GLuint modelViewMatrixSlot = glGetUniformLocation(self.program, "modelViewMatrix");
创建4 * 4投影矩阵
  • 参数1:矩阵
  • 参数2:视角,度数为单位
  • 参数3:纵横比
  • 参数4:近平面距离
  • 参数5:远平面距离
  • location:指要更改的uniform变量的位置
  • count:更改矩阵的个数
  • transpose:是否要转置矩阵,并将它作为uniform变量的值。必须为GL_FALSE
  • value:执行count个元素的指针,用来更新指定uniform变量
KSMatrix4 _projectionMatrix;
// 获取单元矩阵
ksMatrixLoadIdentity(&_projectionMatrix);
// 计算纵横比例 = 长/宽
float width = self.frame.size.width;
float height = self.frame.size.height;
float aspect = width / height; //长宽比
// 获取透视矩阵
ksPerspective(&_projectionMatrix, 30.0, aspect, 5.0f, 20.0f); //透视变换,视角30°
// 将投影矩阵传递到顶点着色器
glUniformMatrix4fv(projectionMatrixSlot, 1, GL_FALSE, (GLfloat*)&_projectionMatrix.m[0][0]);

创建一个4 * 4 矩阵,模型视图矩阵

KSMatrix4 _modelViewMatrix;
// 获取单元矩阵
ksMatrixLoadIdentity(&_modelViewMatrix);
// 平移,z轴平移-10
ksTranslate(&_modelViewMatrix, 0.0, 0.0, -10.0);

创建一个4 * 4 矩阵,旋转矩阵

KSMatrix4 _rotationMatrix;
// 初始化为单元矩阵
ksMatrixLoadIdentity(&_rotationMatrix);
// 旋转
ksRotate(&_rotationMatrix, xDegree, 1.0, 0.0, 0.0); //绕X轴
ksRotate(&_rotationMatrix, yDegree, 0.0, 1.0, 0.0); //绕Y轴
ksRotate(&_rotationMatrix, zDegree, 0.0, 0.0, 1.0); //绕Z轴

把变换矩阵相乘。将_modelViewMatrix矩阵与_rotationMatrix矩阵相乘,结合到模型视图

ksMatrixMultiply(&_modelViewMatrix, &_rotationMatrix, &_modelViewMatrix);
将模型视图矩阵传递到顶点着色器
  • location:指要更改的uniform变量的位置
  • count:更改矩阵的个数
  • transpose:是否要转置矩阵,并将它作为uniform变量的值。必须为GL_FALSE
  • value:执行count个元素的指针,用来更新指定uniform变量
glUniformMatrix4fv(modelViewMatrixSlot, 1, GL_FALSE, (GLfloat*)&_modelViewMatrix.m[0][0]);

开启剔除操作效果

glEnable(GL_CULL_FACE);
使用索引绘图
  • mode:要呈现的画图的模型
  • count:绘图个数
  • type:类型
  • indices:绘制索引数组
glDrawElements(GL_TRIANGLES, sizeof(indices) / sizeof(indices[0]), GL_UNSIGNED_INT, indices);

要求本地窗口系统显示OpenGL ES渲染目标

[self.context presentRenderbuffer:GL_RENDERBUFFER];

c、开启计时器进行旋转

开启xyz轴的计时器

// 开启X轴
- (void)xbuttonClicked
{
    // 开启定时器
    if (!timer)
    {
        timer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(reDegree) userInfo:nil repeats:YES];
    }
    bX = !bX;
    self.glslView.bX = bX;
}

// 开启Y轴和Z轴
......

更新度数后重新渲染

- (void)reDegree
{
    // 如果停止X轴旋转,X = 0则度数就停留在暂停前的度数
    // 更新度数
    xDegree += _bX * 5;
    yDegree += _bY * 5;
    zDegree += _bZ * 5;
    // 重新渲染
    [self render];
}

四、光照计算

1、工具类里提供的方法和属性

a、数据结构
顶点数据结构
typedef struct
{
    GLKVector3  position; //顶点向量
    GLKVector3  normal;   //法线向量
}
SceneVertex;
三角形数据结构
typedef struct
{
    SceneVertex vertices[3];
}
SceneTriangle;
b、8个顶点坐标{x,y,z},法线坐标{x,y,z}
static const SceneVertex vertexA = {{-0.5,  0.5, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexB = {{-0.5,  0.0, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexC = {{-0.5, -0.5, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexD = {{ 0.0,  0.5, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexE = {{ 0.0,  0.0, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexF = {{ 0.0, -0.5, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexG = {{ 0.5,  0.5, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexH = {{ 0.5,  0.0, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexI = {{ 0.5, -0.5, -0.5}, {0.0, 0.0, 1.0}};
c、宏
///八个面
#define NUM_FACES (8)

///8 * 6 = 48个顶点,用于绘制8个面,每个面3个点,每个点有顶点坐标和顶点法线
#define NUM_NORMAL_LINE_VERTS (48)


/// 3 * 8 * 2 + 2。8个面,24个点,每个点需要2个顶点来画法向量,最后2个顶点是光照向量
#define NUM_LINE_VERTS (NUM_NORMAL_LINE_VERTS + 2)
d、提供的方法

静态函数,创建一个三角形

SceneTriangle SceneTriangleMake( const SceneVertex vertexA,const SceneVertex vertexB,const SceneVertex vertexC);

以点0为出发点,通过叉积计算平面法向量

GLKVector3 SceneTriangleFaceNormal(const SceneTriangle triangle);

计算三角形平面法向量,更新每个点的平面法向量

void SceneTrianglesUpdateFaceNormals(SceneTriangle someTriangles[NUM_FACES]);

计算各三角形的法向量,通过平均值求出每个点的法向量

void SceneTrianglesUpdateVertexNormals(SceneTriangle someTriangles[NUM_FACES]);

通过向量A和向量B的叉积求出平面法向量,单元化后返回

void SceneTrianglesNormalLinesUpdate(const SceneTriangle someTriangles[NUM_FACES],GLKVector3 lightPosition,GLKVector3 someNormalLineVertices[NUM_LINE_VERTS]);

通过向量A和向量B的叉积求出平面法向量,单元化后返回

GLKVector3 SceneVector3UnitNormal(const GLKVector3 vectorA,const GLKVector3 vectorB);

2、导入的框架和使用到的属性

导入框架
#import <GLKit/GLKit.h>

@interface ViewController : GLKViewController
Effect
// 基本Effect
@property(nonatomic,strong)GLKBaseEffect *baseEffect;
// 额外Effect,用来绘制法线,实际工程中只需要光照效果,不需要绘制法线
@property(nonatomic,strong)GLKBaseEffect *extraEffect;
缓存区
// 顶点缓存区
@property(nonatomic,strong)AGLKVertexAttribArrayBuffer *vertexBuffer;
// 法线位置缓存区
@property(nonatomic,strong)AGLKVertexAttribArrayBuffer *extraBuffer;
其他属性
// 是否绘制法线
@property(nonatomic,assign)BOOL shouldDrawNormals;
// 中心点的高
@property(nonatomic,assign) GLfloat centexVertexHeight;
成员变量
@implementation ViewController
{
    // 三角形-8面,每个面有3个顶点,每个顶点包括顶点向量和法线向量
    SceneTriangle triangles[NUM_FACES];
}

3、初始化上下文

- (void)setupES
{
    // 1.新建OpenGL ES 上下文
    self.context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
    
    // 2.设置GLKView
    GLKView *view = (GLKView *)self.view;
    view.context = self.context;
    view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
    view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
    
    // 3.设置当前上下文
    [EAGLContext setCurrentContext:self.context];
}

4、设置着色器

- (void)setUpEffect
{
}
a、金字塔Effect
self.baseEffect = [[GLKBaseEffect alloc]init];
self.baseEffect.light0.enabled = GL_TRUE;
光的漫射部分的颜色 GLKVector4Make(R,G,B,A)
self.baseEffect.light0.diffuseColor = GLKVector4Make(0.7f, 0.7f, 0.7, 1.0f);
世界坐标中的光的位置
self.baseEffect.light0.position = GLKVector4Make(1.0f, 1.0f, 0.5f, 0.0f);
b、法线Effect
self.extraEffect = [[GLKBaseEffect alloc]init];
self.extraEffect.useConstantColor = GL_TRUE;
c、为了更好的观察需要调整模型矩阵
    if (true)
    {
        // 围绕x轴旋转-60度
        // 返回一个4x4矩阵进行绕任意矢量旋转
        GLKMatrix4 modelViewMatrix = GLKMatrix4MakeRotation(GLKMathDegreesToRadians(-60.0f), 1.0f, 0.0f, 0.0f);
        
        // 围绕z轴,旋转-30度
        modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix,GLKMathDegreesToRadians(-30.0f), 0.0f, 0.0f, 1.0f);
        
        // 围绕Z方向,移动0.25f
        modelViewMatrix = GLKMatrix4Translate(modelViewMatrix, 0.0f, 0.0f, 0.25f);
        
        // 设置baseEffect,extraEffect模型矩阵
        self.baseEffect.transform.modelviewMatrix = modelViewMatrix;
        self.extraEffect.transform.modelviewMatrix = modelViewMatrix;
    }

5、设置缓冲区

- (void)setUpBuffer
{
}
调用工具类中创建三角形的函数确定图形的8个三角形面
triangles[0] = SceneTriangleMake(vertexA, vertexB, vertexD);
triangles[1] = SceneTriangleMake(vertexB, vertexC, vertexF);
triangles[2] = SceneTriangleMake(vertexD, vertexB, vertexE);
triangles[3] = SceneTriangleMake(vertexE, vertexB, vertexF);
triangles[4] = SceneTriangleMake(vertexD, vertexE, vertexH);
triangles[5] = SceneTriangleMake(vertexE, vertexF, vertexH);
triangles[6] = SceneTriangleMake(vertexG, vertexD, vertexH);
triangles[7] = SceneTriangleMake(vertexH, vertexF, vertexI);
初始化顶点缓存区
self.vertexBuffer = [[AGLKVertexAttribArrayBuffer alloc] initWithAttribStride:sizeof(SceneVertex) numberOfVertices:sizeof(triangles)/sizeof(SceneVertex) bytes:triangles usage:GL_DYNAMIC_DRAW];
初始化法线缓存区,不知道法线的数目和内容,先赋值为空
self.extraBuffer = [[AGLKVertexAttribArrayBuffer alloc]initWithAttribStride:sizeof(SceneVertex) numberOfVertices:0 bytes:NULL usage:GL_DYNAMIC_DRAW];
中心点的高为0,表示在原点
self.centexVertexHeight = 0.0f;

6、绘制GLKView

准备绘制
[self.baseEffect prepareToDraw];
准备绘制顶点数据
[self.vertexBuffer prepareToDrawWithAttrib:GLKVertexAttribPosition numberOfCoordinates:3 attribOffset:offsetof(SceneVertex,position)shouldEnable:YES];
准备绘制光照法线数据
[self.vertexBuffer prepareToDrawWithAttrib:GLKVertexAttribNormal numberOfCoordinates:3 attribOffset:offsetof(SceneVertex, normal) shouldEnable:YES];
使用着色器进行绘制顶点数据
[self.vertexBuffer drawArrayWithMode:GL_TRIANGLES startVertexIndex:0 numberOfVertices:sizeof(triangles)/sizeof(SceneVertex)];
是否要绘制光照法线
if (self.shouldDrawNormals)
{
    [self drawNormals];
}

7、辅助方法

a、更新每个点的平面法向量
- (void)updateNormals
{
    SceneTrianglesUpdateFaceNormals(triangles);
    
    [self.vertexBuffer reinitWithAttribStride:sizeof(SceneVertex) numberOfVertices:sizeof(triangles)/sizeof(SceneVertex) bytes:triangles];
}
b、设置中心点的高
- (void)setCentexVertexHeight:(GLfloat)centexVertexHeight
{
    _centexVertexHeight = centexVertexHeight;
    
    // 更新金字塔的顶点E
    SceneVertex newVertexE = vertexE;
    newVertexE.position.z = _centexVertexHeight;
    
    // 更改顶点E影响下的底面顶点
    triangles[2] = SceneTriangleMake(vertexD, vertexB, newVertexE);
    triangles[3] = SceneTriangleMake(newVertexE, vertexB, vertexF);
    triangles[4] = SceneTriangleMake(vertexD, newVertexE, vertexH);
    triangles[5] = SceneTriangleMake(newVertexE, vertexF, vertexH);
    
    // 更新法线
    [self updateNormals];
}
c、绘制法线
- (void)drawNormals
{
    GLKVector3 normalLineVertices[NUM_LINE_VERTS];
    .....
}
以每个顶点的坐标为起点,顶点坐标加上法向量的偏移值作为终点,更新法线显示数组
  • 参数1:三角形数组
  • 参数2:光源位置
  • 参数3:法线显示的顶点数组
SceneTrianglesNormalLinesUpdate(triangles, GLKVector3MakeWithArray(self.baseEffect.light0.position.v), normalLineVertices);
为extraBuffer重新开辟空间
[self.extraBuffer reinitWithAttribStride:sizeof(GLKVector3) numberOfVertices:NUM_LINE_VERTS bytes:normalLineVertices];
准备绘制数据
[self.extraBuffer prepareToDrawWithAttrib:GLKVertexAttribPosition numberOfCoordinates:3 attribOffset:0 shouldEnable:YES];
指示是否使用常量颜色的布尔值

如果该值设置为gl_true,那么存储在设置属性的值为每个顶点的颜色值。如果该值设置为gl_false,那么你的应用将使glkvertexattribcolor属性提供每顶点颜色数据。默认值是gl_false

self.extraEffect.useConstantColor = GL_TRUE;
// 设置光源颜色为绿色,画顶点法线
self.extraEffect.constantColor = GLKVector4Make(0.0f, 1.0f, 0.0f, 1.0f);
绘制-绿色的法线
[self.extraEffect prepareToDraw];
[self.extraBuffer drawArrayWithMode:GL_LINES startVertexIndex:0 numberOfVertices:NUM_NORMAL_LINE_VERTS];
设置光源颜色为黄色,并且画光源线
self.extraEffect.constantColor = GLKVector4Make(1.0f, 1.0f, 0.0f, 1.0f);
// 准备绘制-黄色的光源方向线
[self.extraEffect prepareToDraw];
// 2点确定一条线
[self.extraBuffer drawArrayWithMode:GL_LINES startVertexIndex:NUM_NORMAL_LINE_VERTS numberOfVertices:2];

续文见下篇:iOS多媒体:OpenGL ES(下)


Demo

Demo在我的Github上,欢迎下载。
Multi-MediaDemo

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容