opengl es 好文

http://blog.oo87.com/opengl/8732.html  //大神地址来自

目录结构:

第一步,明确要干嘛

 a.目标
 b.效果
 c.分析

第二步,怎么去画(纯理论)

 a.OpenGL ES 2 的渲染管线
 b.简述绘制流程的每一个单元【至左向右】
     1) OpenGL ES 2.0 API
     2) Vertex Arrays / Buffer Objects
     3) Vertex Shader
     4) Primitive Assembly
     5) Rasterization 
     6) Texture Memory
     7) Fragment Shader
     8) Per-Fragment Operations
     9) Render Buffer & Frame Buffer
     10) EAGL API
 c. OpenGL ES Shader Language 简述
      *) 简单流程图

第三步,怎么去画(实战)

 a.OpenGL ES 2 的渲染流程 细化
    1) 配置环境
    2) 初始化数据
    3) 配置 OpenGL ES Shader
    4) 渲染绘制
 b.流程代码化
  一、配置渲染环境
    1) 配置渲染窗口 [ 继承自 UIView ]
    2) 配置渲染上下文
    3) 配置帧渲染
    4) 配置渲染缓存
    5) 帧缓存装载渲染缓存的内容
    6) 渲染上下文绑定渲染窗口(图层)
  二、修改背景色
  三、 初始化数据
  四、 配置 OpenGL ES Shader
    1) 编写 Vertex Shader Code 文件
    2) 编写 Fragment Shader Code 文件
    3) 配置 Vertex Shader
    4) 配置 Fragment Shader
    5) 创建 Shader Program
    6) 装载 Vertex ShaderFragment Shader
    7) 链接 Shader Program
  五、渲染绘制
    1) 清空旧渲染缓存
    2) 设置渲染窗口
    3) 使用 Shder Program
    4) **关联数据**
    5) 绘制图形
 c.面向对象的重新设计

第四步,练练手

 a.修改背景色
 b.修改三角形的填充色
 c.修改三角形的三个顶点的颜色(填充色)


第一步,明确要干嘛

1. 目标:

使用 OpenGL ES 2.0 在 iOS 模拟器中绘制一个三角形。

2. 效果:

3. 分析图形:

1) 背景颜色是蓝色
--> 修改背景颜色

2) 直角三角形
--> 绘制三角形

4. 绘制三角形?三角形由什么组成?

--> 三个端点 + 三条线 + 中间的填充色,即三个点连成线形成一个三角面。

1). 三个什么端点(屏幕坐标点)?
要回答这个问题要先了解 OpenGL ES 的坐标系在屏幕上是怎样分布的:

OpenGL ES 的坐标系 {x, y, z}

注:图片截自 《Learning OpenGL ES For iOS》一书

a. 通过图片的三维坐标系可以知道:

- 它是一个三维坐标系 {x, y, z}
- 三维坐标中心在正方体的几何中心 {0, 0, 0}
- 整个坐标系是 [0, 1] 的点,也就是说 OpenGL 中只支持 0 ~ 1 的点

注意,这里所讲的 0 和 1 ,最好理解成 0 --> 无限小, 1 --> 无限大 ,它并不是指 0 个单位的长度,或 1 个单位的长度。

b. 再来看看我们绘制的三角形,在 iOS 模拟器 或真机上 的坐标是怎样构成的:

三维坐标 + 坐标值 演示图

注:图片通过 CINEMA4D (c4d)三维软件绘制

二维就是长这样的了:

二维坐标( z = 0 )

2) 三条线?

a. 连接三个端点形成封闭的三角面,那么 OpenGL ES 能不能直接绘制三角形 ? --> 答案是能。

b. 那么 OpenGL 能直接画正方形么?
--> 答案是不能。

c. 那 OpenGL 能直接绘制什么?
--> 答案是:点精灵、线、三角形,它们统称为 图元(Primitive)。

注:答案来自于《OpenGL ES 2.0 Programming Guide》 7. Primitive Assembly and Rasterization 一章,截图如下:

1) 线元

Line Strip , 指首尾相接的线段,第一条线和最后一条线没有连接在一起;
Line Loops, 指首尾相接的线段,第一条线和最后一条线连接在一起,即闭合的曲线;

Line

2) 三角图元

Triangle Strip, 指条带,相互连接的三角形
Triangle Fan, 指扇面,相互连接的三角形

Triangle

扇面

3) 点精灵 【主要应用在 纹理 方面】

3)填充色?

就是指 RGBA 的颜色值;( ^_^ 感觉好废但还是要说)


第二步,怎么去画(纯理论)

怎么去画,就是通过多少个步骤完成一个完整的绘制渲染流程,当然这里指 OpenGL ES 2 的渲染管线流程)

OpenGL ES 2 的渲染管线

图形管线(Graphics Pipeline)

因为这里是 iOS 端的图,所以重新绘制了一下:

OpenGL ES 2 渲染流程图

注:此图根据 《OpenGL ES 2.0 programming guide》的 Graphics Pipeline 和 Diney Bomfim [All about OpenGL ES 2.x - (part 2/3)] 的管线图进行重新绘制。【绘制的软件为:Visio 2016】

1. 简述绘制流程的每一个单元【至左向右】

OpenGL ES 2.0 API :

iOS 环境下

gltypes.h 是包含了 OpenGL ES 2.0 的基本数据类型的定义;
glext.h 是包含各种宏定义,以及矩阵运算等常用的函数;
gl.h 是 OpenGL ES 2.0 所有的核心函数(命令);

扩展
OpenGL ES 2.0 Reference (函数查询)在线

左边选择要查询的函数即可

离线的函数 Card

红框处单击打开

红箭头处选择保存即可

本人推荐使用离线的卡,不受网络影响,而且一目了然。配合官方的编程指南使用就最佳了。

2. Vertex Arrays / Buffer Objects :

1) Vertex Arrays Objects (简称:VAOs),顶点数组对象,就是一个数组,包含顶点坐标、颜色值、纹理坐标等数据;通过 CPU 内存关联到 GPU 的内存区被 GPU 所使用;

【官方解释:Vertex data may be sourced from arrays that are stored in application memory (via a pointer) or faster GPU memory (in a buffer object).(意指:顶点数组保存在程序内存或快速 GPU 内存中,前者通过数组指针访问数据,后者直接通过 Buffer Objects 访问。【就是指 VAOs 或 VBOs 方式访问】)】

绘制的三角形的数组(三个顶(端)点坐标)如下图:

顶点数组

VFVertex

这是 C 语言的知识,应该不难理解。

2) Vertex Buffer Objects , (简称:VBOs [ Vertex Buffer Objects]),缓存对象,就是持有顶点数组数据或数据下标的对象【并不是指面向对象里面的对象哦,其实一块 GPU 内存块】。

【官方解释:Buffer objects hold vertex array data or indices in high-performance server memory. (意指:VBOs 是持有保存在 GPU 快速内存区的顶点数据或顶点数据下标的缓存对象。)】

a. 为什么是 server ?
--> 答,OpenGL 是基于 CS 模式的设计而成,客户端操作就相当于我们写的 OpenGL API ( OpenGL commands ) 的各种操作,服务器就是图形处理相关的硬件。( ES 当然也是这意思咯。)

【官方解释:OpenGL is implemented as a client-server system, with the application you write being considered the client, and the OpenGL implementation provided by the manufacturer of your computer graphics hardware being the server.】

注:
1) a.b. 里面的【官方解释...】在 OpenGL ES 2.0 Reference Card 可以找到。
2) b.1 的【官方解释...】在《OpenGL Programming Guide》第八版 Introduction OpenGL 一章的第一小节 What Is OpenGL 中的解释。

3. Vertex Shader (顶点着色器) :

处理顶点相关的数据,包括顶点在屏幕的位置(矩阵变换),顶点处的光照计算,纹理坐标等。

顶点着色器的信号图:

注:图片截自:《OpenGL ES 2.0 Programming Guide》 1. Introduction to OpenGL ES 2.0 -- OpenGL ES 2.0 -- Vertex Shader 一节中

1) 输入信号:Attributes、Uniforms、Samplers (optional)

a. Attributes : 属性的意思,指每一个顶点数据;

b. Uniforms :

b-1. 统一的意思 , 是一个只读全局常量,存储在程序的常量区;
b-2. 当 Vertex Shader 和 Fragment Shader 定义了同名同类型的 Uniform 常量时,此时的 Uniform 常量就变成了全局常量(指向同一块内存区的常量);

c. Samplers (可选的) : 
是一个特殊的 Uniforms 保存的是 Texteures(纹理) 数据;

2) 输出信号: Varying

Varying : 
a. 它是 Vertex Shader 与 Fragment Shader 的接口,是为了解决功能性问题(两个 Shader 的信息交互);

b. 储存 Vertex Shader 的输出信息;

c. Vertex Shader 与 Fragment Shader 中必须要有必须要同名同类型的 Varying 变量,不然会编译错误;(因为它是两个 Shader 的信息接口啊,不一样还接什么口啊。)

3) 交互信息: Temporary Variables

Temporary Variables :
a. 指临时变量;
b. 储存 Shader 处理过程中的中间值用的;
c. 声明在 Funtions(函数) 或 Variable(变量) 内部;

4) 输出的内建变量:gl_Position、gl_FrontFacing、gl_PointSize

a. gl_Position (highp vec4 变量) :
就是 Vertex Position,Vertex Shader 的输出值,而且是必须要赋值的变量;只有在 Vertex Shader 中使用才会有效

注:highp vec4, highp (high precision) 高精度的意思,是精度限定符;vec4 ( Floating Point Vector ) 浮点向量 , OpenGL ES 的数据类型。

b. gl_PointSize (mediump float 变量) :
告诉 Vertex Shader 栅格化点的尺寸(pixels, 像素化),想要改变绘制点的大小就是要用这个变量 只有在 Vertex Shader 中使用才会有效

注:mediump , mediump (medium precision) 中等精度的意思,是精度限定符;还有最后一个精度限制符是 lowp ( low precision ),低精度的意思。

c. gl_FrontFacing (bool 变量) : 
改变渲染物体的 Front Facing 和 Back Facing , 是用于处理物体光照问题的变量,双面光照(3D 物体里外光照)问题的时候才会使用的变量,只能在 Vertex Shader 中进行设置, Fragment Shader 是只读的

4. Primitive Assembly (图元装配) :

1) 第一步,把 Vertex Shader 处理后的顶点数据组织成 OpenGL ES 可以直接渲染的基本图元:点、线、三角形;

2) 第二步,裁剪 (Clipping) ,只保留在渲染区域(视锥体,视觉区域)内的图元;

3) 第二步,剔除 (Culling),可通过编程决定剔除前面、后面、还是全部;

注:
视锥体,实际上是一个三维锥体包含的空间区域,由摄影机和物体的捕捉关系形成;

视锥体

图片来源 《透视投影详解》一文

5. Rasterization (光栅化) :

光栅化的信号图:

作用是,将基本图元(点、线、三角形)转换成二维的片元(Fragment, 包含二维坐标、颜色值、纹理坐标等等属性), 像素化基本图元使其可以在屏幕上进行绘制(显示)。

6. Texture Memory (纹理内存) :

Texture 就是指保存了图片(位图)的所有颜色的缓存;Texture Memory 就是图片的颜色(像素)内存;每一个嵌入式系统对 Texture Memory 的大小都是有限制的;

1) 完整的 iOS 渲染绘制管线图中,向上指向 Vertex Shader 的虚线,意指 Texture Coordinate (纹理坐标)信息是通过程序提供给它的;

2) 完整的 iOS 渲染绘制管线图中,指向 Fragment Shader 的实线,因为 Fragment Shader 处理的是光栅化后的数据,即像素数据,而 Texture 本身就是像素数据,所以 Texture Memory 可以直接当成 Fragment Shader 的输入;

7. Fragment Shader (片元着色器) :

片元着色器信号图:

1) 输入信号: Varying、Uniforms、Samples
与 Vertex Shader 的输入是同一个意思,具体请查看 Vertex Shader 处的解释~~~;

2) 输入的内建变量:gl_FragCoord、gl_FrontFacing、gl_PointCoord

a. gl_FragCoord (mediump vec4 只读变量) :
是保存窗口相对坐标的 {x, y, z, 1/w} 的变量,z 表示深度 (will be used for the fragment's depth), w 表示旋转;

b. gl_PointCoord (mediump int 只读变量) : 
是包含了当前片元原始点位置的二维坐标;点的范围是 [0, 1] ;

c. gl_FrontFacing 
请查看 Vertex Shader 处的解释;

3) 输出信号 (内建变量) : gl_FragColor、gl_FragData (图上没写)

a. gl_FragColor (mediump vec4) :
片元的颜色值;

b. gl_FragData (mediump vec4) : 
是一个数组,片元颜色集;

注:两个输出信号只能同时存在一个,就是 写了 gl_FragColor 就不要写 gl_FragData , 反之亦然;【If a shader statically assigns a value to gl_FragColor, it may not assign a value to any element of gl_FragData. If a shader statically writes a value to any element of gl_FragData, it may not assign a value to gl_FragColor. That is, a shader may assign values to either gl_FragColor or gl_FragData, but not both.

-

补充知识 (For Shader)

8. Per-Fragment Operations :

信号图:

1) Pixel ownership test (像素归属测试) :
判断像素在 Framebuffer 中的位置是不是为当前 OpenGL ES Context 所有,即测试某个像素是否属于当前的 Context 或是否被展示(是否被用户可见);

2) Scissor Test (裁剪测试) :
判断像素是否在由 glScissor* 定义的裁剪区域内,不在该剪裁区域内的像素就会被丢弃掉;

3) Stencil Test (模版测试):
将模版缓存中的值与一个参考值进行比较,从而进行相应的处理;

4) Depth Test (深度测试) :
比较下一个片段与帧缓冲区中的片段的深度,从而决定哪一个像素在前面,哪一个像素被遮挡;

5) Blending (混合) :
将片段的颜色和帧缓存中已有的颜色值进行混合,并将混合所得的新值写入帧缓存 (FrameBuffer) ;

6) Dithering (抖动) :
使用有限的色彩让你看到比实际图象更为丰富的色彩显示方式,以缓解表示颜色的值的精度不够大而导致颜色剧变的问题。

9. Render Buffer & Frame Buffer:

关系图:

1) Render Buffer (渲染缓存) :

a. 简称 RBO , Render Buffer Object;
b. 是由程序(Application)分配的 2D 图片缓存;
c. Render Buffer 可以分配和存储颜色(color)、深度(depth)、模版(stectil)值,也可以把这三种值装载到 Frame Buffer 里面;

2) Frame Buffer (帧缓存) :

a. 简称 FBO , Frame Buffer Object;
b. 是颜色、深度、模板缓存装载在 FBO 上所有装载点的合集;
c. 描述颜色、深度、模板的大小和类型的属性状态;
d. 描述 Texture 名称的属性状态;
e. 描述装载在 FBO 上的 Render Buffer Objects (渲染缓存对象) 的属性状态;

扩充知识(FBO):

FBO API 支持的操作如下:
1) 只能通过 OpenGL ES 命令 (API) 创建 FBO 对象;
2) 使用一个 EGL Context 去创建和使用多个 FBO , 即不要为每一个 FBO 对象创建一个正在渲染的上下文(rendering context);
3) 创建 off-screen 的颜色、深度、模板渲染缓存和纹理需要装载在 FBO 上;
4) 通过多个 FBO 来共享颜色、深度、模板缓存;
5) 正确地装载纹理的颜色或深度到 FBO 中,避免复制操作;

10. EAGL API :

官方的是 EGL API 与平台无关,因为它本身是可以进行平台定制的,所以 iOS 下就被 Apple 定制成了 EAGL API 。

EAGL.h : 里面的核心类是 EAGLContext , 上下文环境;
EAGLDrawable.h : 用于渲染绘制输出的 EAGLContext 分类;

注:除了上面的两个外,还有一个类 CAEAGLLayer ,它就是 iOS 端的渲染窗口寄宿层;

【 看这里:
1) EGL API 设计出来的目的就是为了在 OpenGL ES 2 能在窗口系统 (屏幕 ,iOS 是 CAEAGLLayer 类为寄宿层的 View)进行渲染绘制;

2) 可以进行 EGL 渲染的前提是:

a. 可以进行显示的设备( iOS 下当然是手机或模拟器 )
b. 创建渲染面(rendering surface), 设备的屏幕 (on-screen) 或 像素缓存 ( pixel Buffer ) ( off-screen )

注: pixel Buffer , 这种 buffer 是不能直接显示的,只能成为渲染面或通过其它 API 分享出去,如: pbuffers 经常被用于 Texture 的 maps , 因为 Texture 本身也是像素嘛;

3) 创建渲染上下文 ( rendering context ), 即 OpenGL ES 2 Rendering Context ;

注:

OpenGL ES Context : 保存了渲染过程中的所有数据和状态信息;
图示解释:

图片截自, RW. Beginning. OpenGL ES.and.GLKit Tutorials 教程

OpenGL ES Shader Language 简述

流程图中出现的 Vertex Shader 与 Fragment Shader 都是要使用 GLSL ES 语言来进行编程操作的

1. GLSL ES 版本:

OpenGL ES 2.0 对应的 GLSL ES 版本是 1.0,版本编号是 100;

2. iOS Shader 类:

iOS 环境下 GLKit 提供了一个简单的 Shader 类——GLKBaseEffect 类;

GLKit APIs

3. OpenGL 本身是 C Base 的语言,可以适应多个平台,而在 iOS 下的封装就是 GLKit ;

4. GLSL ES (也称 ESSL) ?

简单流程图:

OpenGL ES Shader 流程图

1) 编写 Shader 代码:

a. 同时编写 Vertex Code 和 Fragment Code
b. 建议以文件的形式来编写,不建议使用 "......" 字符串的形式进行编写,前者会有编译器的提示作为辅助防止一定的输入错误,但后者不会,为了不必要的麻烦,使用前者;
c. 文件的名称使用应该要形如 xxxVertexShader.glsl / xxxFragmentShader.glsl;

注:(其实文件名和后缀都可以随意的,但是你在编程的时候为了可读性,建议这样写,也是为了防止不必要的麻烦);【 Xcode 只会在 glsl 的文件后缀的文件进行提示,当然有时候会抽一风也是正常的 】

d. 要掌握的知识点是 Shader 的 Data Typies(数据类型,如:GLfloat 等)、Build-in Variables(内置变量,如:attribute 等)、流程控制语句(if、while 等);

2) 除编写 Shader Code 外,其它的流程都由一个对应的 GLSL ES 的 API (函数) 进行相应的操作;

注:此处只是做了一个 Program 的图,不是只能有一个 Program,而是可以有多个,需要使用多少个,由具体项目决定。


第三步,怎么去画(实战)

以本文的小三角为例,开始浪吧~~~!

e981fd1c1e0c35f7e91735fb473b2bec.gif

OpenGL ES 2 的渲染流程 实际绘制环境,流程细化

OpenGL ES 2 iOS 渲染逻辑流程图. png

1. 配置环境:

1) 主要工作是,EAGL API 的设置。

EAGL Class

2) 核心操作:

a. CAEAGLLayer 替换默认的 CALayer,配置绘制属性;
b. EAGLContext,即 Render Context ,设置成 OpenGL ES 2 API 环境,并使其成为当前活跃的上下文环境;
c. Frame Buffers / Render Buffer 的创建和使用,以及内容绑定;
d. EAGLContext 绑定渲染的窗口 (on-screen),CAEAGLLayer 

扩展:
CAEAGLLayer 
1) 继承链:

CALayer 有的,当然 CAEAGLLayer 也有;

2) 作用:
a. The CAEAGLLayer class supports drawing OpenGL content in iPhone applications. If you plan to use OpenGL for your rendering, use this class as the backing layer for your views by returning it from your view’s layerClass class method. The returned CAEAGLLayer object is a wrapper for a Core Animation surface that is fully compatible with OpenGL ES function calls.
--> 大意就是,CAEAGLLayer 是专门用来渲染 OpenGL 、OpenGL ES 内容的图层;如果要使用,则要重写 layerClass 类方法。

b. Prior to designating the layer’s associated view as the render target for a graphics context, you can change the rendering attributes you want using the drawableProperties property.
--> 大意就是,在 EAGLContext 绑定 CAEAGLLayer 为渲染窗口之前,可以通过修改 drawableProperties 属性来改变渲染属性。

3) 使用注意:
a. 修改 opaque 属性为 YES (CAEAGLLayer.opaque = YES;);
b. 不要修改 Transform ;
c. 当横竖屏切换的时候,不要去修改 CAEAGLLayer 的 Transform 而进行 Rotate, 而是要通过 OpenGL / OpenGL ES 来 Rotate 要渲染的内容。

EAGLContext 
是管理 OpenGL ES 渲染上下文(包含,信息的状态、openGL ES 的命令(API)、OpenGL ES 需要绘制的资源)的对象,要使用 OpenGL ES 的 API (命令) 就要使该 Context 成为当前活跃的渲染上下文。(原文: An EAGLContext object manages an OpenGL ES rendering context—the state information, commands, and resources needed to draw using OpenGL ES. To execute OpenGL ES commands, you need a current rendering context.)

2. 初始化数据

这里主要是考虑是否使用 VBOs ,由于移动端对效率有所要求,所以一般采用 VBOs 快速缓存;

3. 配置 OpenGL ES Shader

1) 这里的核心工作是 Shader Code ,即学习 GLSL ES 语言;
2) iOS 端采用 glsl 后缀的文件来编写代码;

4. 渲染绘制

1) 这里要注意的是 清空旧缓存、设置窗口,虽然只是一句代码的问题,但还是很重要的;
2) 核心是学习 glDraw* 绘制 API ;


流程代码化

1. 配置渲染环境

1) 配置渲染窗口 [继承自 UIView]

a. 重写 layerClass 类方法

+ (Class)layerClass {
   return [CAEAGLLayer class];
}

b. 配置 drawableProperties ,就是绘制的属性

- (void)commit {

    CAEAGLLayer *glLayer = (CAEAGLLayer *)self.layer;

    // Drawable Property Keys
    /*
     // a. kEAGLDrawablePropertyRetainedBacking
     // The key specifying whether the drawable surface retains its contents after displaying them.
     // b. kEAGLDrawablePropertyColorFormat
     // The key specifying the internal color buffer format for the drawable surface.
     */

    glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking : @(YES), // retained unchange
                                   kEAGLDrawablePropertyColorFormat     : kEAGLColorFormatRGBA8 // 32-bits Color
                                   };

    glLayer.contentsScale = [UIScreen mainScreen].scale;
    glLayer.opaque = YES;

}

2) 配置渲染上下文

// a. 定义 EAGLContext
@interface VFGLTriangleView ()
@property (assign, nonatomic) VertexDataMode vertexMode;
@property (strong, nonatomic) EAGLContext *context;
@end
// b. 使用 OpenGL ES 2 的 API,并使该 Context ,成为当前活跃的 Context
- (void)settingContext {

    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.context];

}

3) 配置帧渲染

- (GLuint)createFrameBuffer {

    GLuint ID;

    glGenFramebuffers(FrameMemoryBlock, &ID);
    glBindFramebuffer(GL_FRAMEBUFFER, ID);

    return ID;

}
函数 描述
glGenFramebuffers 创建 帧缓存对象
glBindFramebuffer 使用 帧缓存对象
glGenFramebuffers
void glGenFramebuffers (GLsizei n, GLuint * framebuffers)
n 指返回多少个 Frame Buffer 对象
framebuffers 指 Frame Buffer 对象的标识符的内存地址
glBindFramebuffer
void glBindFramebuffer (GLenum target, GLuint framebuffer)
target _只能填 GLFRAMEBUFFER
framebuffer 指 Frame Buffer 对象的标识符

4) 配置渲染缓存

- (GLuint)createRenderBuffer {

    GLuint ID;

    glGenRenderbuffers(RenderMemoryBlock, &ID);
    glBindRenderbuffer(GL_RENDERBUFFER, ID);

    return ID;

}
函数 描述
glGenRenderbuffers 创建 渲染缓存对象
glBindRenderbuffer 使用 渲染缓存对象
glGenRenderbuffers
void glGenRenderbuffers(GLsizei n, GLuint *renderbuffers)
n 指返回多少个 Render Buffer 对象
renderbuffers 指 Render Buffer 对象的标识符的内存地址
glBindRenderbuffer
void glBindRenderbuffer(GLenum target, GLuint renderbuffer)
target _只能填 GLRENDERBUFFER
renderbuffers 指 Render Buffer 对象的标识符

5) 帧缓存装载渲染缓存的内容

- (void)attachRenderBufferToFrameBufferWithRenderID:(GLuint)renderBufferID {

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBufferID);

}
函数 描述
glFramebufferRenderbuffer 装载 渲染缓存的内容到帧缓存对象中
glFramebufferRenderbuffer
void glFramebufferRenderbuffer (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer)
target _只能填 GLFRAMEBUFFER
attachment _只能是三个中的一个:GL_COLOR_ATTACHMENT0 (颜色缓存)、GL_DEPTH_ATTACHMENT ( 深度缓存 )、GL_STENCILATTACHMENT (模板缓存)
renderbuffertarget _只能填 GLRENDERBUFFER
renderbuffer 指 Render Buffer 对象的标识符,而且当前的 Render Buffer 对象一定要是可用的

6) 渲染上下文绑定渲染窗口(图层)

- (void)bindDrawableObjectToRenderBuffer {

    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];

}
函数 描述
renderbufferStorage: fromDrawable: 关联 当前渲染上下文和渲染窗口
renderbufferStorage: fromDrawable:
- (BOOL)renderbufferStorage:(NSUInteger)target fromDrawable:(id<EAGLDrawable>)drawable
target _只能填 GLRENDERBUFFER
drawable 只能是 CAEAGLLayer 对象

函数解释:
1) 为了使创建的 Render Buffer 的内容可以显示在屏幕上,要使用这个函数绑定 Render Buffer 而且分配共享内存;
2) 要显示 Render Buffer 的内容, 就要使用 presentRenderbuffer:来显示内容;
3) 这个函数的功能等同于 OpenGL ES 中的它【内容太多,简书不好排版】

函数 描述
glRenderbufferStorage 保存渲染缓存内容
glRenderbufferStorage
void glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height)
target _只能填 GLRENDERBUFFER
internalformat 分三种 color render buffer、 depth render buffer、stencil render buffer
width _像素单位,大小必须 <= GL_MAX_RENDERBUFFERSIZE
height _像素单位,大小必须 <= GL_MAX_RENDERBUFFERSIZE
internalformat
color render buffer [01] GL_RGB565, GL_RGBA4, GL_RGB5_A1,
color render buffer [02] GL_RGB8_OES, GL_RGBA8_OES
depth render buffer [01] GL_DEPTH_COMPONENT16,
depth render buffer [02] GL_DEPTH_COMPONENT24_OES, GL_DEPTH_COMPONENT32_OE
stencil render buffer GL_STENCIL_INDEX8, GL_STENCIL_INDEX4_OES, GL_STENCIL_INDEX1_OE

2. 修改背景色

typedef struct {
    CGFloat red;
    CGFloat green;
    CGFloat blue;
    CGFloat alpha;
} RGBAColor;

static inline RGBAColor RGBAColorMake(CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) {

    RGBAColor color = {

        .red = red,
        .green = green,
        .blue = blue,
        .alpha = alpha,

    };

    return color;

}

- (void)setRenderBackgroundColor:(RGBAColor)color {

    glClearColor(color.red, color.green, color.blue, color.alpha);

}
函数 描述
glClearColor 清空 Render Buffer 的 Color Render Buffer 为 RGBA 颜色
glClearColor
void glClearColor (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
red 指 [0, 1] 的红色值
green 指 [0, 1] 的绿色值
blue 指 [0, 1] 的蓝色值
alpha 指 [0, 1] 的透明度值

注: 不想定义 RGBAColor 的话,可以直接使用 GLKit 提供的 GLKVector4 ,原型是

#if defined(__STRICT_ANSI__)
struct _GLKVector4
{
    float v[4];
} __attribute__((aligned(16)));
typedef struct _GLKVector4 GLKVector4;  
#else
union _GLKVector4
{
    struct { float x, y, z, w; };
    struct { float r, g, b, a; };  // 在这呢......
    struct { float s, t, p, q; };
    float v[4];
} __attribute__((aligned(16)));
typedef union _GLKVector4 GLKVector4; // 是一个共用体
#endif
GLK_INLINE GLKVector4 GLKVector4Make(float x, float y, float z, float w)
{
    GLKVector4 v = { x, y, z, w };
    return v;
}

3. 初始化数据

如果要使用 VBOs 最好在这里创建 VBOs 对象并绑定顶点数据,当然直接在关联数据一步做也没问题;

#define VertexBufferMemoryBlock    (1)

- (GLuint)createVBO {

    GLuint vertexBufferID;
    glGenBuffers(VertexBufferMemoryBlock, &vertexBufferID);

    return vertexBufferID;

}

#define PositionCoordinateCount      (3)

typedef struct {
    GLfloat position[PositionCoordinateCount];
} VFVertex;

static const VFVertex vertices[] = {
    {{-0.5f, -0.5f, 0.0}}, // lower left corner
    {{ 0.5f, -0.5f, 0.0}}, // lower right corner
    {{-0.5f,  0.5f, 0.0}}, // upper left corner
};

- (void)bindVertexDatasWithVertexBufferID:(GLuint)vertexBufferID {

    glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);

    // 创建 资源 ( context )
    glBufferData(GL_ARRAY_BUFFER,   // 缓存块 类型
                 sizeof(vertices),  // 创建的 缓存块 尺寸
                 vertices,          // 要绑定的顶点数据
                 GL_STATIC_DRAW);   // 缓存块 用途

}
函数 描述
glGenBuffers 申请 VBOs 对象内存
glBindBuffer 绑定 VBOs 对象
glBufferData 关联顶点数据,并创建内存
glGenBuffers
void glGenBuffers (GLsizei n, GLuint * buffers)
n 指返回多少个 VBO
buffers 指 VBO 的标识符内存地址
glBindBuffer
void glBindBuffer (GLenum target, GLuint buffer)
target _可以使用 GL_ARRAY_BUFFER 或 GL_ELEMENT_ARRAYBUFFER
buffer 指 VBO 的标识符
glBufferData
void glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage)
target _可以使用 GL_ARRAY_BUFFER 或 GL_ELEMENT_ARRAYBUFFER
size 字节单位,数据在内存中的大小(sizeof(...))
data 顶点数据的内存指针
usage 告诉程序怎么去使用这些顶点数据
usage
GL_STATIC_DRAW 程序只指定一次内存对象的数据(顶点数据),而且数据会被多次(非常频繁地)用于绘制图元。
GL_DYNAMIC_DRAW 程序不断地指定内存对象的数据(顶点数据),而且数据会被多次(非常频繁地)用于绘制图元。
GL_STREAM_DRAW 程序只指定一次内存对象的数据(顶点数据),而且数据会被数次(不确定几次)用于绘制图元。

glGenBuffers 、glBindBuffer、glBufferData 都干了什么?

1) glGenBuffers 会在 OpenGL ES Context (GPU) 里面,申请一块指定大小的内存区;

2) glBindBuffer 会把刚才申请的那一块内存声明为 GL_ARRAY_BUFFER ,就是以什么类型的内存来使用;

3) glBufferData 把存放在程序内存的顶点数据 (CPU 内存) 关联到刚才申请的内存区中;

注: 图片截自, RW. Beginning. OpenGL ES.and.GLKit Tutorials 教程;图片中的 “~~ 3) 拷贝顶点数据~~ ” 更正为 “ 3) 关联顶点数据 ”, 因为从 CPU 拷贝数据到 GPU 是在 OpenGL ES 触发绘制方法(后面会进到)的时候才会进行;

4. 配置 OpenGL ES Shader

1) 编写 Vertex Shader Code 文件

a. 这是文件形式的,建议使用这种, Xcode 会进行关键字提示

#version 100

attribute vec4 v_Position;

void main(void) {
    gl_Position = v_Position;
}

a 对应的图片

b. 这是直接 GLchar * 字符串形式

+ (GLchar *)vertexShaderCode {
    return  "#version 100 \n"
            "attribute vec4 v_Position; \n"
            "void main(void) { \n"
                "gl_Position = v_Position;\n"
            "}";
}

b 对应的图片

非常明显地看出,a 不管编写和阅读都很轻松,而 b 就是一堆红,不知道是什么鬼,看久了眼睛会很累;

 代码解释:
a. #version 100 ,首先 OpenGL ES 2 使用的 GLSL ES 版本是 100, 这个没什么好解释的。《OpenGL ES 2 programming Guide》有提及

同时也说明了,我们编写 GLSL Code 的时候,要使用 《OpenGL ES Shading Language》的语言版本;

b. attribute vec4 v_Position;
b-1. attribute 存储类型限定符,表示链接,链接 OpenGL ES 的每一个顶点数据到顶点着色器(一个一个地);

注:
1) attribute 只能定义 float, vec2, vec3, vec4, mat2, mat3,mat4 这几种类型的变量,不能是结构体或数组;
2) 只能用在顶点着色器中,不能在片元着色器中使用,不然会编译错误;

补充:其它的存储类型限定符

限定符 描述
none (默认) 表示本地的可读写的内存  输入的参数
const 表示编译期固定的内容  只读的函数参数
attribute 表示链接,链接 OpenGL ES 的每一个顶点数据到顶点着色器(一个一个地)
uniform 表示一旦正在被处理的时候就不能改变的变量,链接程序、OpenGL ES 、着色器的变量
varying 表示链接顶点着色器和片元着色器的内部数据

b-2. [ vec4 ],基本的数据类型,直接上图

注: 图片截自,OpenGL ES Shading Language 1.0 Quick Reference Card - Page 3

c. gl_Position 内建变量
因为顶点数据里面

只是用到了 Position 顶点数据;

2) 编写 Fragment Shader Code 文件

a. 文件形式

#version 100

void main(void) {
    gl_FragColor = vec4(1, 1, 1, 1); // 填充色,白色
}

b. 字符串形式

+ (GLchar *)fragmentShaderCode {
    return  "#version 100 \n"
            "void main(void) { \n"
                "gl_FragColor = vec4(1, 1, 1, 1); \n"
            "}";
}

3) 配置 Vertex Shader

- (GLuint)createShaderWithType:(GLenum)type {

    GLuint shaderID = glCreateShader(type);

    const GLchar * code = (type == GL_VERTEX_SHADER) ? [[self class] vertexShaderCode] : [[self class] fragmentShaderCode];
    glShaderSource(shaderID,
                   ShaderMemoryBlock,
                   &code,
                   NULL);

    return shaderID;
}

- (void)compileVertexShaderWithShaderID:(GLuint)shaderID type:(GLenum)type {

    glCompileShader(shaderID);

    GLint compileStatus;
    glGetShaderiv(shaderID, GL_COMPILE_STATUS, &compileStatus);
    if (compileStatus == GL_FALSE) {
        GLint infoLength;
        glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLength);
        if (infoLength > 0) {
            GLchar *infoLog = malloc(sizeof(GLchar) * infoLength);
            glGetShaderInfoLog(shaderID, infoLength, NULL, infoLog);
            NSLog(@"%s -> %s", (type == GL_VERTEX_SHADER) ? "vertex shader" : "fragment shader", infoLog);
            free(infoLog);
        }
    }

}
函数 描述
glCreateShader 创建一个着色器对象
glShaderSource 关联顶点、片元着色器的代码
glCompileShader 编译着色器代码
glGetShaderiv 获取着色器对象的相关信息
glGetShaderInfoLog 获取着色器的打印消息
glCreateShader
GLuint glCreateShader (GLenum type)
type _只能是 GL_VERTEX_SHADER、GL_FRAGMENTSHADER 中的一个
return GLuint 返回着色器的内存标识符
glShaderSource
void glShaderSource (GLuint shader, GLsizei count, const GLchar * const_ _string, const GLint length)
shader 着色器的内存标识符
count 有多少块着色代码字符串资源
string 着色代码字符串首指针
length 着色代码字符串的长度
glCompileShader
void glCompileShader(GLuint shader)
shader 着色器的内存标识符
glGetShaderiv
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params)
shader 着色器的内存标识符
pname _指定获取信息的类型,有 GL_COMPILE_STATUS、GL_DELETE_STATUS、GL_INFO_LOG_LENGTH、GL_SHADER_SOURCE_LENGTH、GL_SHADERTYPE 五种
params 用于存储当前获取信息的变量内存地址
glGetShaderInfoLog
void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsei_ _length, GLchar infoLog)
shader 着色器的内存标识符
maxLength 指最大的信息长度
length 获取的信息长度,如果不知道可以是 NULL
infoLog 存储信息的变量的内存地址

4) 配置 Fragment Shader
与 3) 方法一样;

5) 创建 Shader Program

- (GLuint)createShaderProgram {

    return glCreateProgram();

}
函数 描述
glCreateProgram 创建 Shader Program 对象
glCreateProgram
GLuint glCreateProgram()
return GLuint 返回着色器程序的标识符

6) 装载 Vertex Shader 和 Fragment Shader

- (void)attachShaderToProgram:(GLuint)programID vertextShader:(GLuint)vertexShaderID fragmentShader:(GLuint)fragmentShaderID {

    glAttachShader(programID, vertexShaderID);
    glAttachShader(programID, fragmentShaderID);

}
函数 描述
glAttachShader 装载 Shader 对象
glAttachShader
void glAttachShader(GLuint program, GLuint shader)
program 着色器程序的标识符
shader 要装载的着色器对象标识符

7) 链接 Shader Program

- (void)linkProgramWithProgramID:(GLuint)programID {

    glLinkProgram(programID);

    GLint linkStatus;
    glGetProgramiv(programID, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE) {
        GLint infoLength;
        glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &infoLength);
        if (infoLength > 0) {
            GLchar *infoLog = malloc(sizeof(GLchar) * infoLength);
            glGetProgramInfoLog(programID, infoLength, NULL, infoLog);
            NSLog(@"%s", infoLog);
            free(infoLog);
        }
    }

}
函数 描述
glLinkProgram 链接 Shader Program 对象
glGetProgramiv 获取 着色器程序的相关信息
glGetProgramInfoLog 获取 着色器程序的打印信息
glLinkProgram
void glLinkProgram(GLuint program)
program 着色器程序的标识符
glGetProgramiv
void glGetProgramiv(GLuint program, GLenum pname,GLint *params)
program 着色器程序的标识符
pname _可以选择的消息类型有如下几个,GL_ACTIVE_ATTRIBUTES、GL_ACTIVE_ATTRIBUTE_MAX_LENGTH、GL_ACTIVE_UNIFORMS、GL_ACTIVE_UNIFORM_MAX_LENGTH、GL_ATTACHED_SHADERS、GL_DELETE_STATUS、GL_INFO_LOG_LENGTH、GL_LINK_STATUS、GL_VALIDATESTATUS
params 存储信息的变量的内存地址
glGetProgramInfoLog
void glGetProgramInfoLog(GLuint program,GLsizei maxLength, GLsizei_ _length, GLchar infoLog)
program 着色器程序的标识符
maxLength 指最大的信息长度
length 获取的信息长度,如果不知道可以是 NULL
infoLog 存储信息的变量的内存地址

5. 渲染绘制

1) 清空旧渲染缓存

- (void)clearRenderBuffer {

    glClear(GL_COLOR_BUFFER_BIT);

}
函数 描述
glClear 清空 渲染缓存的旧内容
glClear
void glClear (GLbitfield mask)
mask _三者中的一个 GL_COLOR_BUFFER_BIT (颜色缓存),GL_DEPTH_BUFFER_BIT ( 深度缓存 ), GL_STENCIL_BUFFERBIT (模板缓存)

2) 设置渲染窗口

- (void)setRenderViewPortWithCGRect:(CGRect)rect {

    glViewport(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);

}
函数 描述
glViewport 设置 渲染视窗的位置和尺寸
glViewport
void glViewport(GLint x, GLint y, GLsizei w, GLsizei h)
x,y 渲染窗口偏移屏幕坐标系左下角的像素个数
w,h 渲染窗口的宽高,其值必须要大于 0

3) 使用 Shder Program

- (void)userShaderWithProgramID:(GLuint)programID {

    glUseProgram(programID);

}
函数 描述
glUseProgram 使用 Shader Program
glUseProgram
void glUseProgram(GLuint program)
program 着色器程序的标识符

4) 关联数据

#define VertexAttributePosition     (0)
#define StrideCloser                (0)

- (void)attachTriangleVertexArrays {

    glEnableVertexAttribArray(VertexAttributePosition);

    if (self.vertexMode == VertexDataMode_VBO) {

        glVertexAttribPointer(VertexAttributePosition,
                              PositionCoordinateCount,
                              GL_FLOAT,
                              GL_FALSE,
                              sizeof(VFVertex),
                              (const GLvoid *) offsetof(VFVertex, position));

    } else {

        glVertexAttribPointer(VertexAttributePosition,
                              PositionCoordinateCount,
                              GL_FLOAT,
                              GL_FALSE,
                              StrideCloser,
                              vertices);

    }

}
函数 描述
glEnableVertexAttribArray 使能顶点数组数据
glVertexAttribPointer 关联顶点数据

a. 使能顶点缓存

glEnableVertexAttribArray
void glEnableVertexAttribArray(GLuint index)
index _attribute 变量的下标,范围是 [ 0, GL_MAX_VERTEXATTRIBS - 1]

b. 关联顶点数据

glVertexAttribPointer
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *ptr)
index _attribute 变量的下标,范围是 [ 0, GL_MAX_VERTEXATTRIBS - 1]
size 指顶点数组中,一个 attribute 元素变量的坐标分量是多少(如:position, 程序提供的就是 {x, y ,z} 点就是 3 个坐标分量 ),范围是 [1, 4]
type _数据的类型,只能是 GL_BYTE、GL_UNSIGNED_BYTE、GL_SHORT、GL_UNSIGNED_SHORT、GL_FLOAT、GL_FIXED、GL_HALF_FLOATOES *
normalized _指是否进行数据类型转换的意思,GL_TRUE 或 GLFALSE
stride 指每一个数据在内存中的偏移量,如果填 0(零) 就是每一个数据紧紧相挨着。
ptr 数据的内存首地址

知识扩展:
1) 获取最大 attribute 下标的方法

GLint maxVertexAttribs; // n will be >= 8
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxVertexAttribs);

2) 关于 size 补充

注, 图片截自,《OpenGL ES 2 Programming Guide》第 6 章

3) 使能顶点数组数据?
其实顶点着色器中处理的数据有两种输入类型,CVOs ( Constant
Vertex Objects )、VAOs (Vertex Array Objects);
 glEnableVertexAttribArrayglDisableVertexAttribArray 函数就是使用 CVOs 还是 VAOs 的一组开关,看图 :

注: 图片截自,《OpenGL ES 2 Programming Guide》第 6 章

若使用了 CVOs 作为输入数据的,要使用以下处理函数来替代 glVertexAttribPointer 函数:

4) OpenGL ES 只支持 float-pointer 类型的数据,所以才会有 normalized 参数;

5) 顶点着色器的数据传递图,

注: 图片截自,《OpenGL ES 2 Programming Guide》第 6 章

特别提醒,VBOs 只是一种为了加快数据访问和渲染调度的一种手段,而不是数据输入方式的一种;

强烈建议您去看一下 《OpenGL ES 2 Programming Guide》的 6. Vertex Attributes, Vertex Arrays, and Buffer Objects 这一章;

5) 绘制图形

#define PositionStartIndex          (0)
#define DrawIndicesCount            (3)

- (void)drawTriangle {

    glDrawArrays(GL_TRIANGLES,
                 PositionStartIndex,
                 DrawIndicesCount);

}
函数 描述
glDrawArrays 绘制所有图元
glDrawArrays
void glDrawArrays(GLenum mode, GLint first, GLsizei count)
mode _绘制的图元方式,只能是 GL_POINTS、GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP、GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLEFAN 的一种
first 从第几个顶点下标开始绘制
count 指有多少个顶点下标需要绘制

6) 渲染图形

- (void)render {

    [self.context presentRenderbuffer:GL_RENDERBUFFER];

}
函数 描述
presentRenderbuffer: 把 Renderbuffer 的内容显示到窗口系统 (CAEAGLLayer) 中
presentRenderbuffer:
- (BOOL)presentRenderbuffer:(NSUInteger)target
target _只能是 GLRENDERBUFFER
return BOOL 返回是否绑定成功

补充:同时,这个函数也说明了 kEAGLDrawablePropertyRetainedBacking 为什么要设为 YES 的原因:

如果要保存 Renderbuffer 的内容就要把 CARAGLLayer 的 drawableProperties 属性的 kEAGLDrawablePropertyRetainedBacking 设置为 YES 。

上面所有代码的工程文件, 在 Github 上 DrawTriangle_OneStep


面向对象的重新设计:

消息处理的主流程就是上面的信号流程图的步序。
面向对象,就是把所有的消息交给对象来处理咯,关注的就是消息的传递和处理。【可以按照你的喜好来设计,反正可扩展性和可维护性都比较好就行了,当然也不能把消息的传递变得很复杂咯】

OpenGL ES 2 iOS 渲染逻辑流程图_面向对象化

项目文件结构:

完整代码在 Github 上 DrawTriangle_OOP


第四步,练练手

建议按照自己的思路重新写一个项目

1. 修改背景色

提示:glClear 函数

2. 修改三角形的填充色:

提示:CVOs,三个顶点是统一的颜色数据

3. 修改三角形的三个顶点的颜色(填充色):

提示:VAOs / VBOs ,在三个顶点的基础上添加新的颜色数据

它们三个主要是为了 [学 + 习] 如何关联数据,对应的项目是:Github: DrawTriangle_OOP_Challenges_1

如果你发现文章有错误的地方,请在评论区指出,不胜感激!!!

目录

一、分析拉伸的原因

1、修复前后照片对比
2、从问题到目标,分析原因

二、准备知识,三维变换

1、4 x 4 方阵
2、线性变换(缩放与旋转)
3、平移
4、向量(四元数)
5、w 与 其它

三、OpenGL 下的三维变换

1、OpenGL 的坐标系
2、OpenGL 的 gl_Position 是行向量还是列向量
3、单次三维变换与多次三维变换问题
4、OpenGL 的变换是在那个阶段发生的,如何发生

四、修复拉伸问题

1、改写 Shader Code
2、应用 3D 变换知识,重新绑定数据
  1) 在 glLinkProgram 函数之后,利用 glGetUniformLocation 函数
     得到 uniform 变量的 location (内存标识符)
  2) 从 Render Buffer 得到屏幕的像素比(宽:高)值,即为缩小的值
  3) 使用 Shader Program , 调用 glUseProgram 函数
  4) 使用 3D 变换知识,得到一个缩放矩阵变量 scaleMat4
  5) 使用 glUniform* 函数把 scaleMat4 赋值给 uniform 变量
3、完整工程

一、分析拉伸的原因

1、修复前后照片对比

问题与目标

图片通过 sketch 制作

2、从问题到目标,分析原因

1、它们的顶点数据均为:

顶点数组

VFVertex

2、借助 Matlab 把顶点数据绘制出来:

分布图

从图可以看出,这三个数据形成的其实是一个等边直角三角形,而在 iOS 模拟器中通过 OpenGL ES 绘制出来的是直角三角形,所以是有问题的,三角形被拉伸了。
3、on-Screen (屏幕) 的像素分布情况:
1) iPhone6s Plus 屏幕:5.5 寸,1920 x 1080 像素分辨率,明显宽高比不是 1:1 的;
2) OpenGL ES 的屏幕坐标系 与 物理屏幕的坐标系对比:

OpenGL ES 的屏幕坐标系

物理屏幕的坐标系

分析:前者是正方体,后者长方体,不拉伸才怪。
3) 首先,OpenGL 最后生成的都是像素信息,再显示在物理屏幕上;通过 1) 和 2) 可以知道 Y 方向的像素数量大于 X 方向的像素数量,导致真实屏幕所生成的 Y 轴与 X 轴的刻度不一致(就是 Y=0.5 > X=0.5),从而引起了最后渲染绘制出来的图形是向 Y 方向拉伸了的。
动画演示修复:

FixTriangle.gif

所以要做的事情是,把顶点坐标的 Y 坐标变小,而且是要根据当前显示屏幕的像素比来进行缩小。

Gif 图片,由 C4D 制作,PS 最终导出;

4) 在 Shader 里面,v_Position 的数据类型是 vec4 ,即为 4 分量的向量数据 {x,y,z,w}; 就是说,要把这个向量通过数学运算变成适应当前屏幕的向量。


二、准备知识,三维变换

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
-- 建议 --:如果向量、矩阵知识不熟悉的可以看看《线性代数》一书;如果已经有相应的基础了,可以直接看《3D 数学基础:图形与游戏开发》,了解 3D 的世界是如何用向量和矩阵知识描述的;若对 3D 知识有一定的认识,可以直接看《OpenGL Programming Guide》8th 的变换知识, 或 《OpenGL Superblble》7th 的矩阵与变换知识,明确 OpenGL 是如何应用这些知识进行图形渲染的。
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

注:以下核心知识均来源于,《3D 数学基础:图形与游戏开发》,建议看一下第 8 章;

4x4 整体

图片通过 sketch 制作,请放大看

1、4 x 4 方阵

4X4 方阵

  • 1) 它其实就是一个齐次矩阵,是对 3D 运算的一种简便记法;
  • 2) 3x3 矩阵并没有包含平移,所以扩展到 4x4 矩阵,从而可以引入平移的运算;

2、线性变换(缩放与旋转)

线性变换

  • n,是标准化向量,而向量标准化就是指单位化:

normalied

-----> a、 v 不能是零向量,即零向量为 {0,0,0};
-----> b、||v|| 是向量的模,即向量的长度;
-----> c、例子是 2D 向量的,3D/4D 向量都是一样的
-------->【 sqrt(pow(x,2)+pow(y,2)+pow(w,2)...) 】

图片来源于《3D 数学基础:图形与游戏开发》5.7

  • k,是一个常数;

  • a,是一个弧度角;

1) 线性缩放

线性缩放

  • XYZ 方向的缩放:

X 方向,就是 {1,0,0};Y 方向,就是 {0,1,0};Z 方向,就是 {0,0,1};分别代入上面的公式即可得到。

图片来源于《3D 数学基础:图形与游戏开发》8.3.1

2) 线性旋转

线性旋转

  • X 方向 {1,0,0} 的旋转:

  • Y 方向 {0,1,0} 的旋转:

  • Z 方向 {0,0,1} 的旋转:

图片来源于《3D 数学基础:图形与游戏开发》8.2.2

3、平移

平移

直接把平移向量,按分量 {x, y, z} 依次代入齐次矩阵即可;

图片来源于《3D 数学基础:图形与游戏开发》9.4.2

4、向量(四元数)

四元数

a. 向量,即 4D 向量,也称齐次坐标 {x, y, z, w}; 4D->3D,{x/w, y/w, z/w};
b. 四元数,[ w, v ]或 [w, (x,y,z) ] 两种记法,其中 w 就是一个标量,即一个实数;
c. 点乘

矩阵乘法,点乘

c.1 上面两种是合法的,而下面两种是不合法的,就是没有意义的;
c.2 第一个为 A(1x3) 行向量 (矩阵) 与 B(3x3)方阵的点乘,第二个是 A(3x3) 的方阵与 A(3x1) 的列向量 (矩阵) 的点乘;

图片来源于《3D 数学基础:图形与游戏开发》7.1.7

5、w 与 其它

这块内容现在先不深究,不影响对本文内容的理解。

  • W

w

w,与平移向量 {x, y, z} 组成齐次坐标;一般情况下,都是 1;

  • 投影

投影

这里主要是控制投影,如透视投影;如:

图片来源于《3D 数学基础:图形与游戏开发》9.4.6


三、OpenGL 下的三维变换

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
这里主要讨论第一阶段 Vertex 的 3D 变换,对于视图变换、投影变换,不作过多讨论;如果要完全掌握后面两个变换,还需要掌握 OpenGL 下的多坐标系系统,以及摄像机系统的相关知识。
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

1、OpenGL 的坐标系

  • 坐标系方向定义分两种:

图片来源于,《3D 数学基础:图形与游戏开发》8.1;左右手坐标系是用来定义方向的。

  • 旋转的正方向

右手坐标

图片来源于,Diney Bomfim 的《Cameras on OpenGL ES 2.x - The ModelViewProjection Matrix》;这个就是 OpenGL 使用的坐标系,右手坐标系;其中白色小手演示了在各轴上旋转的正方向(黑色箭头所绕方向);

2、OpenGL 的 gl_Position 是行向量还是列向量

  • 这里讨论的核心是,gl_Position 接收的是 行向量,还是列向量?

行向量

列向量

  • 讨论行列向量的目的是明确,3D 矩阵变换在做乘法的时候是使用左乘还是右乘;

图片来源于,《线性代数》矩阵及其运算一节

从图中的结果就可以看出,左乘和右乘运算后是完全不一样的结果;虽然图片中的矩阵是 2 x 2 方阵,但是扩展到 n x n 也是一样的结果;

  • 那么 OpenGL 使用的是什么向量?

图 1,列向量

* **英文大意**:矩阵和矩阵乘法在处理坐标系显示模型方面是一个非常有用的途径,而且对于处理线性变换而言也是非常方便的机制。  

图 2

红框处的向量就是 v_Position 顶点数据;即 OpenGL 用的是列向量;(木有找到更有力的证据,只能这样了)

  • 左乘右乘问题?

图 3

* **英文大意**:在我们的视图模型中,我们想通过一个向量来与矩阵变换进行乘法运算,这里描述了一个矩阵乘法,向量先乘以 A 矩阵再乘以 B 矩阵:  

很明显,例子使用的就是左乘,即 OpenGL 用的是左乘;

图 1、3 来源于,《OpenGL Programming Guide 8th》第 5 章第二节
图 2 来源于,《3D 数学基础:图形与游戏开发》7.1.8

3、单次三维变换与多次三维变换问题

多次变换

1) OpenGL 的三维变换整体图:

4x4 整体 OpenGL

因为列向量的影响,在做点乘的时候,平移放在下方与右侧是完全不一样的结果,所以进行了适应性修改

  • 平移部分的内容:

4X4 方阵 OpenGL

平移 OpenGL

* 矩阵平移公式  

等式左侧:A(4x4)方阵点乘 {v.x, v.y, v.z, 1.0} 是顶点数据列向量;右侧就是一个 xyz 均增加一定偏移的列向量

图片来源于,《OpenGL Superblble》7th, Part 1, Chapter 4. Math for 3D Graphics

  • 投影(就是零)

投影 OpenGL

2) 所有的变换图例演示
物体的坐标是否与屏幕坐标原点重叠

Linaer Transforms

  • 单次变换(原点重叠)

Identity

无变换,即此矩阵与任一向量相乘,不改变向量的所有分量值,能做到这种效果的就是单位矩阵,而我们使用的向量是齐次坐标 {x, y, z, w},所以使用 4 x 4 方阵;{w === 1}.

  • 缩放

Scale

单一的线性变换——缩放,缩放变换是作用在蓝色区域的 R(3x3) 方阵的正对角线(从 m11(x)->m22(y)->m33(z))中; 例子是 X、Y、Z 均放大 3 倍。

  • 旋转

Rotate

单一的线性变换——旋转,旋转变换是作用在蓝色区域的 R(3x3) 方阵中; 例子是绕 Z 轴旋转 50 度。

  • 平移

Translation

单一的线性变换——平移,平移变换是作用在绿色区域的 R(3x1) 矩阵中({m11, m21, m31} 对应 {x, y, z}); 例子是沿 X 正方向平移 2.5 个单位。

  • 单次变换(原点不重叠)

Translation&Scale

Translation&Rotate

以上图片内容来源于《OpenGL Programming Guide》8th, Linear Transformations and Matrices 一小节,使用 skecth 重新排版并导出

3) 多次变换

连续变换

这里的问题就是先旋转还是后旋转。旋转前后,变化的是物体的坐标系(虚线(变换后),实线(变换前)),主要是看你要什么效果,而不是去评论它的对错。
图片来源于,《OpenGL Superblble》7th, Matrix Construction and Operators 一节;

4、OpenGL 的变换是在那个阶段发生的,如何发生

3D 变换

ES 主要看红框处的顶点着色阶段即可,所以我们的变换代码是写在 Vertex Shader 的文件中。

变换转换

这里描述了三个变换阶段,第一个阶段是模型变换,第二个是视图变换阶段,第三个是投影变换阶段,最后出来的才是变换后的图形。本文讨论的是第一个阶段。

详细过程

作为了解即可
以上图片均来源于,《OpenGL Programming Guide》8th, 5. Viewing Transformations, Clipping, and Feedback 的 User Transformations 一节;


四、修复拉伸问题

1、改写 Shader Code

增加了一个 uniform 变量,而且是 mat4 的矩阵类型,同时左乘于顶点数据;

  • 为什么使用 uniform 变量?
    • 首先, Vertex Shader 的输入量可以是 : attribute、unforms、samplers、temporary 四种;
    • 其次,我们的目的是把每一个顶点都缩小一个倍数,也就是它是一个固定的变量,即常量,所以排除 arrribute、temporary ;
    • 同时,既然是一个常量数据,那么 samplers 可以排除,所以最后使用的是 uniforms 变量;
  • 为什么使用 mat4 类型?
    v_Position 是 {x, y, z, w} 的列向量,即为 4 x 1 的矩阵,如果要最终生成 gl_Position 也是 4 x 1 的列向量,那么就要左乘一个 4 x 4 方阵;而 mat4 就是 4 x 4 方阵。

补充:n x m · 4 x 1 -> 4 x 1,如果要出现最终 4 x 1 那么,n 必须要是 4;如果矩阵点乘成立,那么 m 必须要是 4; 所以最终结果是 n x m = 4 x 4 ;

2、应用 3D 变换知识,重新绑定数据

这里主要解决,如何给 uniform 变量赋值,而且在什么时候进行赋值的问题

::::::::::::::::::::::::::::::::::::::::::: 核心步骤::::::::::::::::::::::::::::::::::::::::::::::::::::::
*
》》》 1、在 glLinkProgram 函数之后,利用 glGetUniformLocation 函数
》》》 得到 uniform 变量的 location (内存标识符)
》》》 2、从 Render Buffer 得到屏幕的像素比(宽:高)值,即为缩小的值
》》》 3、使用 Shader Program , 调用 glUseProgram 函数
》》》 4、使用 3D 变换知识,得到一个缩放矩阵变量 scaleMat4
》》》 5、使用 glUniform
 函数把 scaleMat4 赋值给 uniform 变量 **

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

  • 如何给 uniform 变量赋值?

》》》1、得到 uniform 的内存标识符

要在 glLinkProgram 后,再获取 location 值,因为只有链接后 Program 才会 location 的值

- (BOOL)linkShaderWithProgramID:(GLuint)programID {
    // 绑定 attribute 变量的下标
    // 如果使用了两个或以上个 attribute 一定要绑定属性的下标,不然会找不到数据源的
    // 因为使用了一个的时候,默认访问的就是 0 位置的变量,必然存在的,所以才不会出错
    [self bindShaderAttributeValuesWithShaderProgramID:programID];
    // 链接 Shader 到 Program
    glLinkProgram(programID);
    // 获取 Link 信息
    GLint linkSuccess;
    glGetProgramiv(programID, GL_LINK_STATUS, &linkSuccess);
    if (linkSuccess == GL_FALSE) {
        GLint infoLength;
        glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &infoLength);
        if (infoLength > EmptyMessage) {
            GLchar *messages = malloc(sizeof(GLchar *) * infoLength);
            glGetProgramInfoLog(programID, infoLength, NULL, messages);
            NSString *messageString = [NSString stringWithUTF8String:messages];
            NSLog(@"Error: Link Fail %@ !", messageString);
            free(messages);
        }
        return Failure;
    }
    // 在这里
    [self.shaderCodeAnalyzer updateActiveUniformsLocationsWithShaderFileName:@"VFVertexShader"
                                                                   programID:programID];
    return Successfully;
}
- (void)updateActiveUniformsLocationsWithShaderFileName:(NSString *)fileName programID:(GLuint)programID {

    NSDictionary *vertexShaderValueInfos = self.shaderFileValueInfos[fileName];
    ValueInfo_Dict *uniforms = vertexShaderValueInfos[UNIFORM_VALUE_DICT_KEY];

    NSArray *keys = [uniforms allKeys];
    for (NSString *uniformName in keys) {
        const GLchar * uniformCharName = [uniformName UTF8String];
        // 在这里
        GLint location = glGetUniformLocation(programID, uniformCharName); 
        VFShaderValueInfo *info = uniforms[uniformName];
        info.location = location;
    }

}

补充:

glGetActiveUniform
void glGetActiveUniform(GLuint program, GLuint index, GLsizei bufSize, GLsizei length, GLint size, GLenum type, char name)
program 指 Shader Program 的内存标识符
index 指下标,第几个 uniform 变量,[0, activeUniformCount]
bufSize _所有变量名的字符个数,如:v_Projection , 就有 12 个,如果还定义了 vTranslation 那么就是 12 + 13 = 25 个
length NULL 即可
size 数量,uniform 的数量,如果不是 uniform 数组,就写 1,如果是数组就写数组的长度
type _uniform 变量的类型,GL_FLOAT, GL_FLOAT_VEC2,GL_FLOAT_VEC3, GL_FLOAT_VEC4,GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4, GL_BOOL,GL_BOOL_VEC2, GL_BOOL_VEC3, GL_BOOL_VEC4,GL_FLOAT_MAT2, GL_FLOAT_MAT3, GL_FLOAT_MAT4,GL_SAMPLER_2D, GL_SAMPLERCUBE
name uniform 变量的变量名
// 这个函数可以得到,正在使用的 uniform 个数,即可以知道 index 是从 0 到几;
// 还有可以得到,bufSize 的长度
glGetProgramiv(progObj, GL_ACTIVE_UNIFORMS, &numUniforms);
glGetProgramiv(progObj, GL_ACTIVE_UNIFORM_MAX_LENGTH,
&maxUniformLen);

》》》》》》》》》》》》》》》

注:VFShaderValueRexAnalyzer 类就是一个方便进行调用的一种封装而已,你可以使用你喜欢的方式进行封装;

图片来源于,《OpenGL ES 2.0 Programming Guide》4. Shaders and Programs,Uniforms and Attributes 一节

  • 在什么时候进行赋值操作?
    一定要在 glUseProgram 后再进行赋值操作,不然无效
- (void)drawTriangle {

    [self.shaderManager useShader];
    [self.vertexManager makeScaleToFitCurrentWindowWithScale:[self.rboManager windowScaleFactor]];
    [self.vertexManager draw];
    [self.renderContext render];

}

》》》2、得到屏幕的像素比

- (CGFloat)windowScaleFactor {

    CGSize renderSize = [self renderBufferSize];
    float scaleFactor = (renderSize.width / renderSize.height);

    return scaleFactor;

}

补充:renderBufferSize

- (CGSize)renderBufferSize {
    GLint renderbufferWidth, renderbufferHeight;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &renderbufferWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &renderbufferHeight);
    return CGSizeMake(renderbufferWidth, renderbufferHeight);
}

》》》3、使用 Shader Program

- (void)useShader {

    glUseProgram(self.shaderProgramID);

}```

》》》**4、使用 3D 变换知识,得到一个缩放矩阵变量 scaleMat4**

VFMatrix4 scaleMat4 = VFMatrix4MakeScaleY(scale);```

扩展 1:

    VFMatrix4 VFMatrix4MakeXYZScale(float sx, float sy, float sz) {
        VFMatrix4 r4 = VFMatrix4Identity;
        VFMatrix4 _mat4 = {
              sx  , r4.m12, r4.m13, r4.m14,
            r4.m21,   sy  , r4.m23, r4.m24,
            r4.m31, r4.m32,   sz  , r4.m34,
            r4.m41, r4.m42, r4.m43, r4.m44,
        };
        return _mat4;
    };
    VFMatrix4 VFMatrix4MakeScaleX(float sx) {
        return VFMatrix4MakeXYZScale(sx, 1.f, 1.f);
    };
    VFMatrix4 VFMatrix4MakeScaleY(float sy) {
        return VFMatrix4MakeXYZScale(1.f, sy, 1.f);
    };
    VFMatrix4 VFMatrix4MakeScaleZ(float sz) {
        return VFMatrix4MakeXYZScale(1.f, 1.f, sz);
    };

它们都定义在:

VFMath

注:如果不想自己去写这些函数,那么可以直接使用 GLKit 提供的

数学函数

》》》》》》 个人建议,自己去尝试写一下会更好

》》》*5、使用 glUniform 函数把 scaleMat4 赋值给 uniform 变量 **

- (void)makeScaleToFitCurrentWindowWithScale:(float)scale {

    NSDictionary *vertexShaderValueInfos = self.shaderCodeAnalyzer.shaderFileValueInfos[@"VFVertexShader"];
    ValueInfo_Dict *uniforms = vertexShaderValueInfos[UNIFORM_VALUE_DICT_KEY];
//    NSLog(@"uniforms %@", [uniforms allKeys]);

    // v_Projection 投影
//    VFMatrix4 scaleMat4 = VFMatrix4Identity;
    VFMatrix4 scaleMat4 = VFMatrix4MakeScaleY(scale);
    VFMatrix4 transMat4 = VFMatrix4Identity; //VFMatrix4MakeTranslationX(0.3)
    glUniformMatrix4fv((GLint)uniforms[@"v_Projection"].location,   // 定义的 uniform 变量的内存标识符
                       1,                                           // 不是 uniform 数组,只是一个 uniform -> 1
                       GL_FALSE,                                    // ES 下 只能是 False
                       (const GLfloat *)scaleMat4.m1D);             // 数据的首指针

    glUniformMatrix4fv((GLint)uniforms[@"v_Translation"].location,   // 定义的 uniform 变量的内存标识符
                       1,                                           // 不是 uniform 数组,只是一个 uniform -> 1
                       GL_FALSE,                                    // ES 下 只能是 False
                       (const GLfloat *)transMat4.m1D);             // 数据的首指针

}

扩展 2:

  • 赋值函数有那些?
    它们分别是针对不同的 uniform 变量进行的赋值函数

3、完整工程:Github: [DrawTriangle_Fix](https://github.com/huangwenfei/OpenGLES2Learning/tree/master/02-DrawTriangle_Fix/DrawTriangle_Fix)

> glsl 代码分析类  
> 
> 
> ![](/usr/uploads/2017/10/4046991714.png)  
> 
> 
>   
> 核心的知识是**正则表达式**,主要是把代码中的变量解析出来,可以对它们做大规模的处理。有兴趣可以看一下,没有兴趣的可以忽略它完全不影响学习和练习本文的内容。

* * *

#### **学习这篇文章的大前提是,你得有[《OpenGL ES 2.0 (iOS): 一步从一个小三角开始》](http://www.jianshu.com/p/d22cf555de47)的基础知识。**

* * *

# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

# **本文核心目的就是熟练图形的分析与绘制**

# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

* * *

> ## 目录
> 
> ### 零、目标+准备
> 
> ### 一、图元绘制之线

0. 工程目录

  1. 绘制单一、交叉的线
  2. 绘制折线
  3. 绘制几何图形
  4. 绘制三角化的几何图形
  5. 绘制曲线、圆形

二、图元绘制之三角形

0.工程目录
1. 绘制基本几何图形

三、图元绘制之点精灵(内容为空)

四、练练手

0.工程目录
1. 绘制一棵卡通树
2. 绘制一张卡片
3. 绘制一棵草

零、目标 + 准备

1) 目标

Geometries

2) 准备

  • 观察所有图形,发现它们都是点与点之间的连线(直线或曲线),组成一个几何形状( ^_^ 好像有点废话);
  • 除了点线的问题外,还可以知道几何形状,有交叠、闭环、开环三种情况;
  • 除此之外,还有填充色有无的问题;

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

  • A、根据 OpenGL ES 的特点,归纳总结:
    • a. 要绘制这些图形,需要控制顶点的数量
    • b. 控制顶点与顶点之间的连接情况,Strip 或 Loop(Fan) 或 没关系
    • c. 控制图形的填充色,即 Fragment Shader 与 Vertex Shader 之间的颜色传递问题;
  • B、OpenGL ES 下控制数据源与绘制方式的函数有那些?(VBO 模式)
    • a. 绑定 VBO 数据 glBufferData
    • b. 绘制数据 glDrawArrays/glDrawElements
    • c. 绘制模式有:
      • GL_POINTS (点)
      • GL_LINES/GL_LINE_STRIP/GL_LINE_LOOP (线)
      • GL_TRIANGLES/GL_TRIANGLE_STRIP/GL_TRIANGLE_FAN (面)

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

所以本文就是根据图形的形态,选择适当的绘制方式,去绘制图形;核心目的就是熟练图形的分析与绘制;
因为是练习图元,所以学习的重点在,数据绑定和图形绘制这一块;


一、图元绘制之线

Lines,多条线的意思;
Line Strip , 指首尾相接的线段,第一条线和最后一条线没有连接在一起;
Line Loops, 指首尾相接的线段,第一条线和最后一条线连接在一起,即闭合的曲线;

模式 线与点的数量关系
GL_LINES nPoints = 2 * mLines
GL_LINE_STRIP nPoints = mLines + 1
GL_LINE_LOOP nPoints = mLines

ep: 上图中的图形

模式 线与点的数量关系
GL_LINES v0~v5(6) = 2 * 3
GL_LINE_STRIP v0~v3(4) = 3 + 1
GL_LINE_LOOP v0~v4(5) = 5

0. 工程目录

完整的线元工程在,这一章的结尾;

工程目录

图中红色箭头所指的就是要修改的类,其中 VFVertexDatasManager 类是核心,它是负责整个工程的数据绑定和图形绘制的;
蓝色框所指的都是工程中的静态顶点数据(当然你也可以动态生成并进行绑定绘制);

1. 绘制单一、交叉的线

LINES

  • 图形分析

    • 首先它们都是线,所以选择的是 线模式;
    • 左侧就是一条线 -> GL_LINES,有两个顶点坐标,而且坐标是左底右高
    • 右侧是两条交叉线 -> GL_LINES,有四个顶点坐标

nPoints = 2 * mLines

  • 开始写代码

    • 数据源准备
// 位于 VFBaseGeometricVertexData.h
// 单线段
static const VFVertex singleLineVertices[] = {
  { 0.5f,  0.5f, 0.0f},
  {-0.5f, -0.5f, 0.0f},
};
// 交叉线
static const VFVertex crossLinesVertices[] = {
  // Line one
  { 0.5f,  0.5f, 0.0f},
  {-0.5f, -0.5f, 0.0f},
  // Line Two
  {-0.53f, 0.48f, 0.0f},
  { 0.55f, -0.4f, 0.0f},
};
* 修改数据绑定方法
/**
*  装载数据
*/
- (void)attachVertexDatas {
  self.currentVBOIdentifier = [self createVBO];
  self.drawInfo = [self drawInfoMaker];
  if (self.drawInfo.elementDataPtr) {
      self.currentElementVBOIdentifier = [self createVBO];
      [self bindVertexDatasWithVertexBufferID:self.currentElementVBOIdentifier
                                   bufferType:GL_ELEMENT_ARRAY_BUFFER
                                 verticesSize:self.drawInfo.elementDataSize
                                     datasPtr:self.drawInfo.elementDataPtr];
  }
  [self bindVertexDatasWithVertexBufferID:self.currentVBOIdentifier
                               bufferType:GL_ARRAY_BUFFER
                             verticesSize:self.drawInfo.dataSize
                                 datasPtr:self.drawInfo.dataPtr]; // CPU 内存首地址
  [self attachVertexArrays];
}

关键的方法是- (void)bindVertexDatasWithVertexBufferID: bufferType: verticesSize: datasPtr:,如下:

/**
*  使用顶点缓存对象
*
*  @param vertexBufferID 顶点缓存对象标识
*/
- (void)bindVertexDatasWithVertexBufferID:(GLuint)vertexBufferID
                               bufferType:(GLenum)bufferType
                             verticesSize:(GLsizeiptr)size
                                 datasPtr:(const GLvoid*)dataPtr {

  glBindBuffer(bufferType, vertexBufferID);
  // 创建 资源 ( context )
  glBufferData(bufferType,        // 缓存块 类型
               size,              // 创建的 缓存块 尺寸
               dataPtr,           // 要绑定的顶点数据
               GL_STATIC_DRAW);   // 缓存块 用途
}

还有- (VFLineDrawInfo)drawLineInfoMaker 方法,生成相应图形的数据源信息,如下:

// 位于 VFVertexDatasManager 类的
// - (VFLineDrawInfo)drawLineInfoMaker; 方法中
    case VFDrawGeometryType_SingleLine: {

        dataSize                = sizeof(singleLineVertices);
        dataPtr                 = singleLineVertices;
        verticesIndicesCount    = (GLsizei)(sizeof(singleLineVertices) /
                                            sizeof(singleLineVertices[0]));
        primitiveMode           = VFPrimitiveModeLines;

        break;
    }
    case VFDrawGeometryType_CrossLines: {

        dataSize                = sizeof(crossLinesVertices);
        dataPtr                 = crossLinesVertices;
        verticesIndicesCount    = (GLsizei)(sizeof(crossLinesVertices) /
                                            sizeof(crossLinesVertices[0]));
        primitiveMode           = VFPrimitiveModeLines;

        break;
    }

其中 @property (assign, nonatomic) VFDrawInfo drawInfo; 是定义的数据源信息结构体,具体信息如下:

// 位于 VFVertexDatasManager 类中
typedef struct {
  // 数据所占的内存大小
  GLsizeiptr dataSize;
  // 数据的内存首地址
  const GLvoid *dataPtr;
  // 需要绘制的点数量
  GLsizei verticesIndicesCount;
  // 图元的绘制类型
  VFPrimitiveMode primitiveMode;
  // 下标数据所占的内存大小
  GLsizeiptr elementDataSize;
  // 下标内存首地址
  const GLvoid *elementDataPtr;
  // 下标个数
  GLsizei elementIndicesCount;
} VFDrawInfo;
* 修改绘制方法,直接获取信息即可
// 位于 VFVertexDatasManager 类中
#define GPUVBOMemoryPtr    (0)
/**
 *  绘制图形
 */
- (void)draw {
  glLineWidth(DEFAULT_LINE_WITH);
  if (self.drawInfo.elementIndicesCount) {
     glDrawElements(self.drawInfo.primitiveMode,
                    self.drawInfo.elementIndicesCount,
                    GL_UNSIGNED_BYTE,
                    GPUVBOMemoryPtr);  // GPU 内存中的首地址
    return;
}
  glDrawArrays(self.drawInfo.primitiveMode,
               StartIndex, // 就是 0
               self.drawInfo.verticesIndicesCount);
}

其中 glLineWidth函数是修改线的宽度的;
glDrawElements是绘制下标的方法;这里不需要用到,所以先不解释;

* 修改图形显示
// 位于 VFVertexDatasManager 类中
/**
*  绘制的几何图形类型
*/
@property (assign, nonatomic) VFDrawGeometryType drawGeometry;

// 位于 VFRenderWindow 类
// 位于 .m 文件的 263 行
/**
*  装载顶点数据
*/
- (void)prepareVertexDatas {
  [self.vertexManager setDrawGeometry:VFDrawGeometryType_CrossLines];
  [self.vertexManager attachVertexDatas];
}

这里新增了一个枚举类型的变量,drawGeometry ,目的是方便外部类进行操控,而进行何种类型图形的绘制渲染,VFDrawGeometryType 定义如下:

// VFVertexDatasManager .h 文件中
typedef NS_ENUM(NSUInteger, VFDrawGeometryType) {

  VFDrawGeometryType_SingleLine = 0,  // 单条线
  VFDrawGeometryType_CrossLines,      // 交叉线

  VFDrawGeometryType_MountainLines,   // 拆线山

  VFDrawGeometryType_TriangleLines,   // 线三角
  VFDrawGeometryType_RectangleLines,  // 线正方形
  VFDrawGeometryType_PentagonsLines,  // 线五边形
  VFDrawGeometryType_HexagonsLines,   // 线六边形
  VFDrawGeometryType_TrapezoidLines,  // 线梯形
  VFDrawGeometryType_PentagramLines,  // 线五角星
  VFDrawGeometryType_RoundLines,      // 线圆

  VFDrawGeometryType_LowPolyRectLines,// LP 线正方形
  VFDrawGeometryType_LowPolyPentLines,// LP 线五边形
  VFDrawGeometryType_LowPolyHexLines, // LP 线六边形
  VFDrawGeometryType_LowPolyTrazLines,// LP 线梯形
  VFDrawGeometryType_LowPolyStarLines,// LP 线五角星

  VFDrawGeometryType_BezierMountain,  // Bezier 山
  VFDrawGeometryType_BezierRound,     // Bezier 圆
  VFDrawGeometryType_BezierOval,      // Beizer 椭圆
};

这一节只是,单线与交叉线的绘制;

  • 程序运行结果

2. 绘制折线

LINE STRIP MOUN

  • 图形分析
    • 首先这是一条线,所以选择的是 线模式;
    • 但是它是一条折线,即多段线首尾相接组成的线,而且没有闭合,GL_LINES_STRIP 模式;
    • 有 7 个顶点,6 条线 (nPoints = mLines + 1)
  • 开始写代码

    • 数据源
// 位于 VFBaseGeometricVertexData.h
// 折线(山丘)
static const VFVertex mountainLinesVertices[] = {
// Point one
{-0.9f, -0.8f, 0.0f},

// Point Two
{-0.6f, -0.4f, 0.0f},

// Point Three
{-0.4f, -0.6f, 0.0f},

// Point Four
{ 0.05f, -0.05f, 0.0f},

// Point Five
{0.45f, -0.65f, 0.0f},

// Point Six
{ 0.55f,  -0.345f, 0.0f},

// Point Seven
{ 0.95f, -0.95f, 0.0f},
};
* 修改数据绑定方法  

在 drawLineInfoMaker 类中增加新的内容,其它不变;

// 位于 VFVertexDatasManager 类的
// - (VFLineDrawInfo)drawLineInfoMaker; 方法中
    case VFDrawGeometryType_MountainLines: {

        dataSize                = sizeof(mountainLinesVertices);
        dataPtr                 = mountainLinesVertices;
        verticesIndicesCount    = (GLsizei)(sizeof(mountainLinesVertices) /
                                            sizeof(mountainLinesVertices[0]));
        primitiveMode           = VFPrimitiveModeLineStrip;

        break;
    }
* 修改图形的显示
// 位于 VFRenderWindow 类
// 位于 .m 文件的 263 行
/**
*  装载顶点数据
*/
- (void)prepareVertexDatas {
  [self.vertexManager  setDrawGeometry:VFDrawGeometryType_MountainLines];
  [self.vertexManager attachVertexDatas];
}
  • 程序运行结果

3. 绘制几何图形

Triangle2Round.gif

LINE LOOP

  • 图形分析
    多段线首尾相接组成的几何形状,GL_LINES_LOOP 模式;

nPoints = mLines

  • 开始写代码

    • 数据源(从左至右),其中五角星这个数据,可以利用内五边形与外五边形相结合的方法(当然内五边形的点要做一个角度旋转),生成相应的点;

所有的点,都通过程序动态生成,如下:

这个类的计算原理是,建立极坐标系,确定起始点,再循环增加旋转角度,就可以得到所有的点,包括圆的点(圆即正多边形,不过它的边数已经多到细到人眼无法识别,而出现曲线的效果,就像这一小节开始的动态图一样的原理,当然椭圆的点集也可以通过这种方式得到)

这两个类在另外的工程里面, Github: 动态计算点

它的小应用,你可以按照自己的想法尽情改写......

红框处的,就是点的生成方法;箭头所指的函数是把生成的点数据按照一定的格式写入文件的方法(文件会自动创建);

下面是具体的数据:

// 三角形
static const VFVertex triangleLinesVertices[] = {
// Point one
  {0.000000, 0.500000, 0.000000},

// Point Two
  {-0.433013, -0.250000, 0.000000},

// Point Three
  {0.433013, -0.250000, 0.000000},
};
// 四边形
static const VFVertex rectangleLinesVertices[] = {
// Point one
  {-0.353553, 0.353553, 0.000000},

// Point Two
  {-0.353553, -0.353553, 0.000000},

// Point Three
  {0.353553, -0.353553, 0.000000},

// Point Four
  {0.353553, 0.353553, 0.000000},
};
// 五边形
static const VFVertex pentagonsLinesVertices[] = {
// Line one
  {0.000000, 0.500000, 0.000000},

// Line Two
  {-0.475528, 0.154509, 0.000000},

// Line Three
  {-0.293893, -0.404509, 0.000000},

// Line Four
  {0.293893, -0.404509, 0.000000},

// Line Five
  {0.475528, 0.154509, 0.000000},
};
// 六边形
static const VFVertex hexagonsLinesVertices[] = {
// Point one
  {0.000000, 0.500000, 0.000000},

// Point Two
  {-0.433013, 0.250000, 0.000000},

// Point Three
  {-0.433013, -0.250000, 0.000000},

// Point Four
  {-0.000000, -0.500000, 0.000000},

// Point Five
  {0.433013, -0.250000, 0.000000},

// Point Six
  {0.433013, 0.250000, 0.000000},
};
// 梯形
static const VFVertex trapezoidLinesVertices[] = {
// Point one
  {0.430057, 0.350000, 0.000000},

// Point Two
  {-0.430057, 0.350000, 0.000000},

// Point Three
  {-0.180057, -0.350000, 0.000000},

// Point Four
  {0.180057, -0.350000, 0.000000},
};
// 五角星形
static const VFVertex pentagramLinesVertices[] = {
// Point one
    {0.000000, 0.500000, 0.000000},

// Point Two
  {-0.176336, 0.242705, 0.000000},

// Point Three
  {-0.475528, 0.154509, 0.000000},

// Point Four
  {-0.285317, -0.092705, 0.000000},

// Point Five
  {-0.293893, -0.404509, 0.000000},

// Point Six
  {-0.000000, -0.300000, 0.000000},

// Point Seven
  {0.293893, -0.404509, 0.000000},

// Point Eight
  {0.285317, -0.092705, 0.000000},

// Point Nine
  {0.475528, 0.154509, 0.000000},

// Point Ten
  {0.176336, 0.242705, 0.000000},
};

圆的顶点数据在单独的文件中, VFRound.h,也是通过动态点生成的【因为点太多,所以单独放在一个文件中进行管理】;

* 修改数据绑定方法,在 drawLineInfoMaker 方法中增加新的内容
//  位于 VFVertexDatasManager 类的
// - (VFLineDrawInfo)drawLineInfoMaker; 方法中
    case VFDrawGeometryType_TriangleLines: {

        dataSize                = sizeof(triangleLinesVertices);
        dataPtr                 = triangleLinesVertices;
        verticesIndicesCount    = (GLsizei)(sizeof(triangleLinesVertices) /
                                            sizeof(triangleLinesVertices[0]));
        primitiveMode           = VFPrimitiveModeLineLoop;

        break;
    }
    case VFDrawGeometryType_RectangleLines: {

        dataSize                = sizeof(rectangleLinesVertices);
        dataPtr                 = rectangleLinesVertices;
        verticesIndicesCount    = (GLsizei)(sizeof(rectangleLinesVertices) /
                                            sizeof(rectangleLinesVertices[0]));
        primitiveMode           = VFPrimitiveModeLineLoop;

        break;
    }
    case VFDrawGeometryType_PentagonsLines: {

        dataSize                = sizeof(pentagonsLinesVertices);
        dataPtr                 = pentagonsLinesVertices;
        verticesIndicesCount    = (GLsizei)(sizeof(pentagonsLinesVertices) /
                                            sizeof(pentagonsLinesVertices[0]));
        primitiveMode           = VFPrimitiveModeLineLoop;

        break;
    }
    case VFDrawGeometryType_HexagonsLines: {

        dataSize                = sizeof(hexagonsLinesVertices);
        dataPtr                 = hexagonsLinesVertices;
        verticesIndicesCount    = (GLsizei)(sizeof(hexagonsLinesVertices) /
                                            sizeof(hexagonsLinesVertices[0]));
        primitiveMode           = VFPrimitiveModeLineLoop;

        break;
    }
    case VFDrawGeometryType_TrapezoidLines: {

        dataSize                = sizeof(trapezoidLinesVertices);
        dataPtr                 = trapezoidLinesVertices;
        verticesIndicesCount    = (GLsizei)(sizeof(trapezoidLinesVertices) /
                                            sizeof(trapezoidLinesVertices[0]));
        primitiveMode           = VFPrimitiveModeLineLoop;

        break;
    }
    case VFDrawGeometryType_PentagramLines: {

        dataSize                = sizeof(pentagramLinesVertices);
        dataPtr                 = pentagramLinesVertices;
        verticesIndicesCount    = (GLsizei)(sizeof(pentagramLinesVertices) /
                                            sizeof(pentagramLinesVertices[0]));
        primitiveMode           = VFPrimitiveModeLineLoop;

        break;
    }
    case VFDrawGeometryType_RoundLines: {

        dataSize                = sizeof(roundGeometry);
        dataPtr                 = roundGeometry;
        verticesIndicesCount    = (GLsizei)(sizeof(roundGeometry) /
                                            sizeof(roundGeometry[0]));
        primitiveMode           = VFPrimitiveModeLineLoop;

        break;
    }
  • 图形显示类(VFRenderWindow )也做相应的修改即可,位于 .m 文件的 263 行;

  • 程序运行结果

TRI-ROUND

4. 绘制三角化的几何图形(Low Poly)

TRIANGLE STRIP FAN PLO

  • 图形分析
    • 首先它们都是由线组成,线模式
    • 其次,它们的线是闭合的,首尾相接?GL_LINES_LOOP ?
    • 所谓首尾相接,形成闭合图形,是起点直接到达终点,就是说起点只会被经过一次,就是最后闭合的那一次;观察图形,起点如果只被经过一次,能不能用线绘制出来,很难吧,特别是最后一个,所以这里直接用 GL_LINESSTRIP 模式,之后任意编排线经过点的顺序,即可。(当然,如果你有兴趣的话,也可以写一个算法去计算点被经过最少的次数下,图形可以完整绘制出来)_
    • 点可能会多次被经过,那么就是说,这个点要被程序调度多次,但是 glDrawArrays 只能一个顶点被调度一次啊。所以这里要用它的兄弟函数 glDrawElements 这个函数的意思就是绘制成员,顶点数据的下标就是它的成员,即通过顶点数据的成员来访问数据而进行灵活绘制。

glDrawElements 根据顶点数据在内存的下标进行绘制的方法

glDrawElements
void glDrawElements(GLenum mode, GLsizei count,GLenum type, **const GLvoid*** indices)
mode _只能是以下几种:GL_POINTS、GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP、GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLEFAN
count indices 的数量
type _下标的数据类型:GL_UNSIGNED_BYTE、GL_UNSIGNED_SHORT、GL_UNSIGNED_INT(它只能在使用了 OES_element_indexuint 才能使用)
indices 下标在内存中的首地址 (如果使用了 VBO,就是 GPU 内存中的首地址,若不是,则为 CPU 内存中的首地址)
  • 开始写代码

    • VFLineDrawInfo 增加了对下标绘制的支持
typedef struct {
  // 数据所占的内存大小
  GLsizeiptr dataSize;
  // 数据的内存首地址
  const GLvoid *dataPtr;
  // 需要绘制的点数量
  GLsizei verticesIndicesCount;
  // 图元的绘制类型
  VFPrimitiveMode primitiveMode;
  // 下标数据所占的内存大小
  GLsizeiptr elementDataSize; // 在这.....
  // 下标内存首地址
  const GLvoid *elementDataPtr; // 在这.....
  // 下标个数
  GLsizei elementIndicesCount; // 在这.....
} VFLineDrawInfo;
* 在原来的线数据基础下,增加对应图形的下标数据  

这里选取下标的原则是,让每一个点都尽可能少地被经过,从而完成图形的绘制,目的就是为了节省资源。

// 四边形的下标数据
static const GLubyte rectangleElementIndeices[] = {
  0, 1, 2,
  3, 0, 2,
};
// 五边形的下标数据
static const GLubyte pentagonsElementIndeices[] = {
  4, 1, 0, 4,
  3, 1, 2, 3,
};
// 六边形的下标数据
static const GLubyte hexagonsElementIndeices[] = {
  5, 1, 0, 5,
  4, 1, 2, 4,
  3, 2,
};
// 梯形的下标数据
static const GLubyte trapezoidElementIndeices[] = {
1, 2, 3, 0,
1, 3,
};
//五角星形的下标数据
static const GLubyte pentagramElementIndeices[] = {
  1, 2, 3, 4,
  5, 6, 7, 8,
  9, 0, 1,
  9, 7, 5, 3, 1,
  5, 7, 1 
};
  • 修改数据绑定方法
    绑定新增加的下标数据支持,使用 VBO 的方式(虽然前面已经写过,这里重温一下,因为这里都是真正的应用)
// 核心方法
/**
*  装载数据
*/
- (void)attachVertexDatas {
  self.currentVBOIdentifier = [self createVBO];

  self.lineInfo = [self drawLineInfoMaker];

  if (self.lineInfo.elementDataPtr) {
      self.currentElementVBOIdentifier = [self createVBO];
      [self bindVertexDatasWithVertexBufferID:self.currentElementVBOIdentifier
                                   bufferType:GL_ELEMENT_ARRAY_BUFFER
                                 verticesSize:self.lineInfo.elementDataSize
                                     datasPtr:self.lineInfo.elementDataPtr];
  }

  [self bindVertexDatasWithVertexBufferID:self.currentVBOIdentifier
                               bufferType:GL_ARRAY_BUFFER
                             verticesSize:self.lineInfo.dataSize
                                 datasPtr:self.lineInfo.dataPtr]; // CPU 内存首地址

  [self attachVertexArrays];
}

在 drawLineInfoMaker 方法中新增内容:

// drawLineInfoMaker 里面的新增内容
      case VFDrawGeometryType_LowPolyRectLines: {

          dataSize                = sizeof(rectangleLinesVertices);
          dataPtr                 = rectangleLinesVertices;
          elementDataSize         = sizeof(rectangleElementIndeices);
          elementDataPtr          = rectangleElementIndeices;
          elementIndicesCount     = (GLsizei)(sizeof(rectangleElementIndeices) /
                                              sizeof(rectangleElementIndeices[0]));
          primitiveMode           = VFPrimitiveModeLineStrip;

          break;
      }
      case VFDrawGeometryType_LowPolyPentLines: {

          dataSize                = sizeof(pentagonsLinesVertices);
          dataPtr                 = pentagonsLinesVertices;
          elementDataSize         = sizeof(pentagonsElementIndeices);
          elementDataPtr          = pentagonsElementIndeices;
          elementIndicesCount     = (GLsizei)(sizeof(pentagonsElementIndeices) /
                                              sizeof(pentagonsElementIndeices[0]));
          primitiveMode           = VFPrimitiveModeLineStrip;

          break;
      }
      case VFDrawGeometryType_LowPolyHexLines: {

          dataSize                = sizeof(hexagonsLinesVertices);
          dataPtr                 = hexagonsLinesVertices;
          elementDataSize         = sizeof(hexagonsElementIndeices);
          elementDataPtr          = hexagonsElementIndeices;
          elementIndicesCount     = (GLsizei)(sizeof(hexagonsElementIndeices) /
                                              sizeof(hexagonsElementIndeices[0]));
          primitiveMode           = VFPrimitiveModeLineStrip;

          break;
      }
      case VFDrawGeometryType_LowPolyTrazLines: {

          dataSize                = sizeof(trapezoidLinesVertices);
          dataPtr                 = trapezoidLinesVertices;
          elementDataSize         = sizeof(trapezoidElementIndeices);
          elementDataPtr          = trapezoidElementIndeices;
          elementIndicesCount     = (GLsizei)(sizeof(trapezoidElementIndeices) /
                                              sizeof(trapezoidElementIndeices[0]));
          primitiveMode           = VFPrimitiveModeLineStrip;

          break;
      }
      case VFDrawGeometryType_LowPolyStarLines: {

          dataSize                = sizeof(pentagramLinesVertices);
          dataPtr                 = pentagramLinesVertices;
          elementDataSize         = sizeof(pentagramElementIndeices);
          elementDataPtr          = pentagramElementIndeices;
          elementIndicesCount     = (GLsizei)(sizeof(pentagramElementIndeices) /
                                              sizeof(pentagramElementIndeices[0]));
          primitiveMode           = VFPrimitiveModeLineStrip;

          break;
      }
// 修改的数据绑定方法
/**
*  使用顶点缓存对象
*
*  @param vertexBufferID 顶点缓存对象标识
*/
- (void)bindVertexDatasWithVertexBufferID:(GLuint)vertexBufferID
                             bufferType:(GLenum)bufferType
                           verticesSize:(GLsizeiptr)size
                               datasPtr:(const GLvoid*)dataPtr {

  glBindBuffer(bufferType, vertexBufferID);

  // 创建 资源 ( context )
  glBufferData(bufferType,        // 缓存块 类型
               size,              // 创建的 缓存块 尺寸
               dataPtr,           // 要绑定的顶点数据
               GL_STATIC_DRAW);   // 缓存块 用途
}
  • 数据绘制方法中的下标绘制支持
// 修改的绘制方法
#define GPUVBOMemoryPtr    (0)
/**
*  绘制图形
*/
- (void)draw {

  glLineWidth(DEFAULT_LINE_WITH);

  if (self.lineInfo.elementIndicesCount) {
      glDrawElements(self.lineInfo.primitiveMode,
                     self.lineInfo.elementIndicesCount,
                     GL_UNSIGNED_BYTE,
                     GPUVBOMemoryPtr);  // GPU 内存中的首地址
      return;
  }

  glDrawArrays(self.lineInfo.primitiveMode,
               0,
               self.lineInfo.verticesIndicesCount);
}
  • 程序运行结果

Rect-Star

5. 绘制曲线、圆形

BAISER

  • 图形分析

    • 首先,它们都是曲线,它们都可以通过 GL_LINE_STRIP 条带来进行绘制,而且后者也可能通过 GL_LINE_LOOP 进行绘制;
    • 根据上一节的圆可以知道,只要线足够短,以致人眼无法分辨,那么折线就可以形成曲线,但是有个问题?左边的,折线怎么控制它的方向呢,第一个点与第二个点之间的折线弯曲程度,要怎么才能生成它的点集呢?
    • OpenGL 是以点为基础进行图元的绘制的,那么只要有一个方法动态地根据固定点去控制之间曲线点的生成,问题就解决了。坐标与点,那么肯定是函数,要生成曲线,贝塞尔曲线函数就可以了(如果想不到,回忆你所见过的任一个图形绘制软件,就秒懂了,如:PS 的钢笔工具, skecth 的钢笔工具......)。
  • 知识补充 (贝塞尔曲线)
    请看下面的 word/pdf 文档《贝塞尔曲线推导》
    书写贝塞尔曲线函数如下,具体实现也在 Github: 动态计算点 这里

文件

应用

  • 开始写代码

    • 数据源都在 文件中,红框处

* 增加 VFDrawGeometryType 内容
VFDrawGeometryType_BezierMountain,
VFDrawGeometryType_BezierRound,
VFDrawGeometryType_BezierOval,
* drawLineInfoMaker 里面的新增内容
case VFDrawGeometryType_BezierMountain: {

       dataSize                = sizeof(_BEZMountain);
       dataPtr                 = _BEZMountain;
       verticesIndicesCount    = (GLsizei)(sizeof(_BEZMountain) /
                                           sizeof(_BEZMountain[0]));
       primitiveMode           = VFPrimitiveModeLineStrip;

       break;
   }
   case VFDrawGeometryType_BezierRound: {

       dataSize                = sizeof(_BEZRound);
       dataPtr                 = _BEZRound;
       verticesIndicesCount    = (GLsizei)(sizeof(_BEZRound) /
                                           sizeof(_BEZRound[0]));
       primitiveMode           = VFPrimitiveModeLineStrip;

       break;
   }
   case VFDrawGeometryType_BezierOval: {

       dataSize                = sizeof(_BEZOval);
       dataPtr                 = _BEZOval;
       verticesIndicesCount    = (GLsizei)(sizeof(_BEZOval) /
                                           sizeof(_BEZOval[0]));
       primitiveMode           = VFPrimitiveModeLineStrip;

       break;
   }
  • 当然图形显示类,也要改咯!

  • 程序运行结果

Bezier


二、图元绘制之三角形

Triangles,就是多个三角形;
Triangle Strip, 指条带,相互连接的三角形;
Triangle Fan, 指扇面,相互连接的三角形;

图 1:三角形模式

图 2:STRIP

图 3:FAN

模式 三角形与点的数量关系
GL_TRIANGLES nPoints = 3 * mTriangles
GL_TRIANGLE_STRIP nPoints = mTriangles + 2
GL_TRIANGLE_FAN nPoints = mTriangles + 2

ep: 图 1 中的图形

模式 三角形与点的数量关系
GL_TRIANGLES v0~v5(6) = 3 * 2
GL_TRIANGLE_STRIP v0~v4(5) = 3+ 2
GL_TRIANGLE_FAN v0~v4(5) = 3+ 2

0. 工程目录

工程目录

这里没有什么太大的变化,只是数据的集合发生了一些变化而已;

1. 绘制基本几何图形

TRIANGLE STRIP FAN

  • 图形分析

    • 首先,第一张图片每一个图形都是一个面,但是 OpenGL 只能直接绘制三角面,所以必须把图形分解成三角面才能进行绘制;
    • 以下就是分解成三角面之后的图形:

TRIANGLE LINESON

当然你也可以按照自己的方式进行分解,一定要遵守这里的点、三角形关系

不然图形是不能正确地绘制出来的;

  • 这里容易出问题的是最后一个图形(五角星形),三角形与点的关系:10(点的数量) = 10(分割出来的三角形数量) + 2,很明显是不相等的,所以 10 个点是不可能绘制出来这个图形的,只能再增加两个点; 除了点的数量问题外,它还不是一个条带(或者说用条带来描述并不合适),它更适合用扇面来描述,即 GL_TRIANGLE_FAN;

    • 开始写代码
  • 数据源,它们都可以通过 FAN 或 STRIP 进行绘制,当然那个点用得少而且图形绘制完整,以及方便,就用那个;像五角星那个图形这么麻烦,当然不做两种试验了;STRIP 模式下的点的分布要特别注意,偶数下标在上面,奇数下标在下面【把图形压扁,你就能看出来了】
// 三角形
static const VFVertex triangleTrianglesVertices[] = {
  // Point V0
  {0.000000, 0.500000, 0.000000},

  // Point V1
  {-0.433013, -0.250000, 0.000000},

  // Point V2
  {0.433013, -0.250000, 0.000000},
};
// 四边形( 0,1,2,3,0,2 )
static const VFVertex rectangleTrianglesVertices[] = {

  // GL_TRIANGLE_FAN
  // Point V0
  {-0.353553, 0.353553, 0.000000},    // V0

  // Point V1
  {-0.353553, -0.353553, 0.000000},   // V1

  // Point V2
  {0.353553, -0.353553, 0.000000},    // V2

  // Point V3
   {0.353553, 0.353553, 0.000000},     // V3

// GL_TRIANGLE_STRIP
//    // Point V0
//    {-0.353553, 0.353553, 0.000000},    // V0
//    
//    // Point V1
//    {-0.353553, -0.353553, 0.000000},   // V1
//    
//    // Point V3
//    {0.353553, 0.353553, 0.000000},     // V3
//    
//    // Point V2
//    {0.353553, -0.353553, 0.000000},    // V2
};
// 五边形
static const VFVertex pentagonsTrianglesVertices[] = {

// GL_TRIANGLE_FAN
//    // Point V0
//    {0.000000, 0.500000, 0.000000},
//    
//    // Point V1
//    {-0.475528, 0.154509, 0.000000},
//    
//    // Point V2
//    {-0.293893, -0.404509, 0.000000},
//    
//    // Point V3
//    {0.293893, -0.404509, 0.000000},
//    
//    // Point V4
//    {0.475528, 0.154509, 0.000000},

  // GL_TRIANGLE_STRIP
  // Point V1
  {-0.475528, 0.154509, 0.000000},

  // Point V2
  {-0.293893, -0.404509, 0.000000},

  // Point V0
  {0.000000, 0.500000, 0.000000},

  // Point V3
  {0.293893, -0.404509, 0.000000},

  // Point V4
  {0.475528, 0.154509, 0.000000},
};
// 六边形
static const VFVertex hexagonsTrianglesVertices[] = {

  // GL_TRIANGLE_FAN
  // Point V0
  {0.000000, 0.500000, 0.000000},

  // Point V1
  {-0.433013, 0.250000, 0.000000},

  // Point V2
  {-0.433013, -0.250000, 0.000000},

  // Point V3
  {-0.000000, -0.500000, 0.000000},

  // Point V4
  {0.433013, -0.250000, 0.000000},

  // Point V5
  {0.433013, 0.250000, 0.000000},

// GL_TRIANGLE_STRIP
//    // Point V1
//    {-0.433013, 0.250000, 0.000000},
//    
//    // Point V2
//    {-0.433013, -0.250000, 0.000000},
//    
//    // Point V0
//    {0.000000, 0.500000, 0.000000},
//    
//    // Point V3
//    {-0.000000, -0.500000, 0.000000},
//    
//    // Point V4
//    {0.433013, -0.250000, 0.000000},
//    
//    // Point V5
//    {0.433013, 0.250000, 0.000000},
//    
//    // Point V0
//    {0.000000, 0.500000, 0.000000},
};
// 梯形
static const VFVertex trapezoidTrianglesVertices[] = {

  // GL_TRIANGLE_FAN
//    // Point V0
//    {0.430057, 0.350000, 0.000000},
//    
//    // Point V1
//    {-0.430057, 0.350000, 0.000000},
//    
//    // Point V2
//    {-0.180057, -0.350000, 0.000000},
//    
//    // Point V3
//    {0.180057, -0.350000, 0.000000},

  // GL_TRIANGLE_STRIP
  // Point V0
  {0.430057, 0.350000, 0.000000},

  // Point V1
  {-0.430057, 0.350000, 0.000000},

  // Point V3
  {0.180057, -0.350000, 0.000000},

  // Point V2
  {-0.180057, -0.350000, 0.000000},
};
// 五角星形 10 = (n - 2) -> n = 12
static const VFVertex pentagramTrianglesVertices[] = {

  // GL_TRIANGLE_FAN
  // Point V0
  {0.000000, 0.000000, 0.000000}, // 在原来的基础上,增加的起点

  // Point V1
  {0.000000, 0.500000, 0.000000},

  // Point V2
  {-0.176336, 0.242705, 0.000000},

  // Point V3
  {-0.475528, 0.154509, 0.000000},

  // Point V4
  {-0.285317, -0.092705, 0.000000},

  // Point V5
  {-0.293893, -0.404509, 0.000000},

  // Point V6
  {-0.000000, -0.300000, 0.000000},

  // Point V7
  {0.293893, -0.404509, 0.000000},

  // Point V8
  {0.285317, -0.092705, 0.000000},

  // Point V9
  {0.475528, 0.154509, 0.000000},

  // Point V10
  {0.176336, 0.242705, 0.000000},

  // Point V11
  {0.000000, 0.500000, 0.000000},// 在原来的基础上,增加的终点
};
* 数据的绑定(与线元一致),只是修改了 VFDrawGeometryType 枚举和 drawLineInfoMaker 方法而已;

  * attachVertexDatas 
/**
*  装载数据
*/
- (void)attachVertexDatas {
  self.currentVBOIdentifier = [self createVBO];
  self.lineInfo = [self drawLineInfoMaker];
  if (self.lineInfo.elementDataPtr) {
      self.currentElementVBOIdentifier = [self createVBO];
      [self bindVertexDatasWithVertexBufferID:self.currentElementVBOIdentifier
                                   bufferType:GL_ELEMENT_ARRAY_BUFFER
                                 verticesSize:self.lineInfo.elementDataSize
                                     datasPtr:self.lineInfo.elementDataPtr];
}
  [self bindVertexDatasWithVertexBufferID:self.currentVBOIdentifier
                               bufferType:GL_ARRAY_BUFFER
                             verticesSize:self.lineInfo.dataSize
                                 datasPtr:self.lineInfo.dataPtr]; // CPU 内存首地址
  [self attachVertexArrays];
}
  * VFDrawGeometryType
// 在这呢......
typedef NS_ENUM(NSUInteger, VFDrawGeometryType) {
VFDrawGeometryType_TriangleTriangles = 0,
VFDrawGeometryType_RectangleTriangles,
VFDrawGeometryType_PentagonsTriangles,
VFDrawGeometryType_HexagonsTriangles,
VFDrawGeometryType_TrapezoidTriangles,
VFDrawGeometryType_PentagramTriangles,
VFDrawGeometryType_RoundTriangles,
};
  * drawInfoMaker 方法
// - (VFDrawInfo)drawInfoMaker 方法
// 在这呢......
switch (self.drawGeometry) {
  case VFDrawGeometryType_TriangleTriangles: {

      dataSize                = sizeof(triangleTrianglesVertices);
      dataPtr                 = triangleTrianglesVertices;
      verticesIndicesCount    = (GLsizei)(sizeof(triangleTrianglesVertices) /
                                          sizeof(triangleTrianglesVertices[0]));
      primitiveMode           = VFPrimitiveModeTriangles;

      break;
  }
  case VFDrawGeometryType_RectangleTriangles: {

      dataSize                = sizeof(rectangleTrianglesVertices);
      dataPtr                 = rectangleTrianglesVertices;
      verticesIndicesCount    = (GLsizei)(sizeof(rectangleTrianglesVertices) /
                                          sizeof(rectangleTrianglesVertices[0]));
      primitiveMode           = VFPrimitiveModeTriangleFan;

      break;
  }
  case VFDrawGeometryType_PentagonsTriangles: {

      dataSize                = sizeof(pentagonsTrianglesVertices);
      dataPtr                 = pentagonsTrianglesVertices;
      verticesIndicesCount    = (GLsizei)(sizeof(pentagonsTrianglesVertices) /
                                          sizeof(pentagonsTrianglesVertices[0]));
      primitiveMode           = VFPrimitiveModeTriangleStrip;

      break;
  }
  case VFDrawGeometryType_HexagonsTriangles: {

      dataSize                = sizeof(hexagonsTrianglesVertices);
      dataPtr                 = hexagonsTrianglesVertices;
      verticesIndicesCount    = (GLsizei)(sizeof(hexagonsTrianglesVertices) /
                                          sizeof(hexagonsTrianglesVertices[0]));
      primitiveMode           = VFPrimitiveModeTriangleFan;

      break;
  }
  case VFDrawGeometryType_TrapezoidTriangles: {

      dataSize                = sizeof(trapezoidTrianglesVertices);
      dataPtr                 = trapezoidTrianglesVertices;
      verticesIndicesCount    = (GLsizei)(sizeof(trapezoidTrianglesVertices) /
                                          sizeof(trapezoidTrianglesVertices[0]));
      primitiveMode           = VFPrimitiveModeTriangleStrip;

      break;
  }
  case VFDrawGeometryType_PentagramTriangles: {

      dataSize                = sizeof(pentagramTrianglesVertices);
      dataPtr                 = pentagramTrianglesVertices;
      verticesIndicesCount    = (GLsizei)(sizeof(pentagramTrianglesVertices) /
                                          sizeof(pentagramTrianglesVertices[0]));
      primitiveMode           = VFPrimitiveModeTriangleFan;

      break;
  }
  case VFDrawGeometryType_RoundTriangles: {

      dataSize                = sizeof(roundGeometry);
      dataPtr                 = roundGeometry;
      verticesIndicesCount    = (GLsizei)(sizeof(roundGeometry) /
                                          sizeof(roundGeometry[0]));
      primitiveMode           = VFPrimitiveModeTriangleFan;

      break;
  }
}
  * draw 方法
#define GPUVBOMemoryPtr    (0)
/**
*  绘制图形
*/
- (void)draw {

  if (self.lineInfo.elementIndicesCount) {
      glDrawElements(self.lineInfo.primitiveMode,
                     self.lineInfo.elementIndicesCount,
                     GL_UNSIGNED_BYTE,
                     GPUVBOMemoryPtr);  // GPU 内存中的首地址
    return;
}

  glDrawArrays(self.lineInfo.primitiveMode,
               StartIndex, // 0
               self.lineInfo.verticesIndicesCount);
}
  • 同样要修改图形显示类(VFRenderWindow).m 文件的 263 行;

  • 程序运行结果

TRI-ROUND Triangle

完整的程序代码: Github DrawGeometries_Triangles


三、图元绘制之点精灵

这里不进行详细讲解,个人感觉在这里讲没什么意思,还是放在 Texture 纹理部分进行详细讲解会比较有用,而且好玩;

如果只是学习 gl_PointSize 的话没意思,结合 gl_PointCoord 去学习反而更有趣,不过这里要有纹理的知识,所以先行不讲了;


四、练练手

Challenges

这里的目的不是为了绘制它们而进行绘制,而是针对图元绘制做一个深入的学习,要学习分析图形和寻找合适有效的绘制方式,而且还要做到判断数据的大致生成方法方式是什么,不然你永远都只是一个只会搞代码的搬运工而已;编程可不仅仅是搞代码;

0. 工程目录

取消了采用结构体存取数据的方式,改用 Model 类,方便 OC 处理和传输;

1. 绘制一棵卡通树

Tree

提示:进行两次的 glDraw* 调用,分别绘制外边的线和内部的填充图

2. 绘制一张卡片

Card

提示:把数据分成左、右、右中线,三种,原因是左边的数据是用贝塞尔曲线生成数据量非常大;主要是利用 glBufferSubData 与 glBufferData 的结合,以及 glVertexAttribPointer 的配合;

3. 绘制一棵草

Grass

注意:尽可以地用肉眼去判断线的走向,用 动态计算点 的类做实验,不断成长起来吧。

完整的挑战项目:Github DrawGeometries_Challenge


目录

一、多坐标系

1.  世界坐标系
2.  物体(模型)坐标系
3.  摄像机坐标系
4.  惯性坐标系

二、坐标空间

1.  世界空间
2.  模型空间
3.  摄像机空间
4.  裁剪空间
5.  屏幕空间

三、OpenGL ES 2 3D 空间

1.  变换发生的过程
2.  各个变换流程分解简述
3.  四次变换与编程应用

四、工程例子

五、参考书籍


一、多坐标系

1. 世界坐标系

  • 即物体存在的空间,以此空间某点为原点,建立的坐标系

  • 世界坐标系是最大的坐标系,世界坐标系不一定是指 “世界”,准确来说是一个空间或者区域,就是足以描述区域内所有物体的最大空间坐标,是我们关心的最大坐标空间;

  • 例子

    • ep1:
      比如我现在身处广州,要描述我现在所在的空间,对我而言最有意义就是,我身处广州的那里,而此时的广州就是我关心的 “世界坐标系”,而不用描述我现在的经纬坐标是多少,不需要知道我身处地球的那个经纬位置。
      这个例子是以物体的方向思考的最合适世界坐标系;(当然是排除我要与广州以外的区域进行行为交互的情况咯!)

    • ep2:
      如果现在要描述广州城的全貌,那么对于我们而言,最大的坐标系是不是就是广州这个世界坐标系,也就是所谓的我们最关心的坐标系;
      这个例子是以全局的方向思考的最合适世界坐标系;
  • 世界坐标系主要研究的问题:
    1) 每个物体的位置和方向
    2) 摄像机的位置和方向
    3) 世界的环境(如:地形)
    4) 物体的运动(从哪到哪)

2. 物体(模型)坐标系

  • 模型自身的坐标系,坐标原点在模型的某一点上,一般是几何中心位置为原点

  • 模型坐标系是会跟随模型的运动而运动,因为它是模型本身的 “一部份” ;

  • 模型内部的构件都是以模型坐标系为参考进而描述的;

  • ep:
    比如有一架飞机,机翼位于飞机的两侧,那么描述机翼最合适的坐标系,当然是相对于飞机本身,机翼位于那里;飞机在飞行的时候,飞机本身的坐标系是不是在跟随运动,机翼是不是在飞机的坐标中同时运动着。

3. 摄像机坐标系

  • 摄像机坐标系就是以摄像机本身为原点建立的坐标系,摄像机本身并不可见,它表示的是有多少区域可以被显示(渲染)

  • 白色线所围成的空间,就是摄像机所能捕捉到的最大空间,而物体则位于空间内部;

  • 位于摄像机捕捉空间外的图形会直接被剔除掉;

4. 惯性坐标系

  • 它的 X 轴与世界坐标系的 X 轴平行且方向相同,Y 轴亦然,它的原点与模型坐标系相同

  • 它的存在的核心价值是,简化坐标系的转换,即简化模型坐标系到世界坐标系的转换;

二、坐标空间

  • 坐标空间就是坐标系形成的空间


1. 世界空间

世界坐标系形成的空间,光线计算一般是在此空间统一进行;

2. 模型空间

模型坐标系形成的空间,这里主要包含模型顶点坐标和表面法向量的信息;


第一次变换
模型变换(Model Transforms):就是指从模型空间转换到世界空间的过程


3. 摄像机空间

摄像机空间

摄像机空间,就是黄色区域所包围的空间;
摄像机空间在这里就是透视投影,透视投影用于 3D 图形显示,反映真实世界的物体状态;

透视知识扩展 《透视》


第二次变换
视变换(View Transforms):就是指从世界空间转换到摄像机空间的过程


  • 摄像机空间,也被称为眼睛空间,即可视区域;
  • 其中,LookAt(摄像机的位置) 和 Perspective(摄像机的空间) 都是在调整摄像空间;

4. 裁剪空间

图形属于裁剪空间则保留,图形在裁剪空间外,则剔除(Culled)

摄像机 带注解

标号(3)[视景体] ,所指的空间即为裁剪空间,这个空间就由 Left、Right、Top、Bottom、Near、Far 六个面组成的四棱台,即视景体。

视景体

图中紫色区域为视场角

fov & zoom

从而引出,视场缩放为:

zoom

  • 其次,顶点是用齐次坐标表示 {x, y, z, w}, 3D 坐标则为{x/w, y/w, z/w} 而 w 就是判断图形是否属于裁剪空间的关键:
锥面 关系
Near z < -w
Far z > w
Bottom y < -w
Top y > w
Left x < -w
Right x > w

即坐标值,不符合这个范围的,都会被裁剪掉

坐标 值范围
x [-w , w]
y [-w, w]
z [-w, w]

第三次变换
投影变换(Projection Transforms): 当然包括正交、透视投影了,就是指从摄影机空间到视景体空间的变换过程


5. 屏幕空间

它就是显示设备的物理屏幕所在的坐标系形成的空间,它是 2D 的且以像素为单位,原点在屏幕的几何中心点

屏幕坐标空间. jpg


第四次变换(最后一次)
视口变换(ViewPort Transforms): 指从裁剪空间到屏幕空间的过程,即从 3D 到 2D


这里主要是关注像素的分布,即像素纵横比;因为图形要从裁剪空间投影映射到屏幕空间中,需要知道真实的环境的像素分布情况,不然图形就会出现变形;

《OpenGL ES 2.0 (iOS)[02]:修复三角形的显示》这篇文章就是为了修复屏幕像素比例不是 1 : 1 引起的拉伸问题,而它也就是视中变换中的一个组成部分。

  • 像素纵横比计算公式

像素缩放比

三、OpenGL ES 2 3D 空间

1. 变换发生的过程

OpenGL ES 2 变换流程图

  • 这个过程表明的是 GPU 处理过程(渲染管线);
  • 变换过程发生在,顶点着色与光栅化之间,即图元装配阶段;
  • 编写程序的时候,变换的操作是放在顶点着色器中进行处理;
  • 右下角写明了,总共就是四个变换过程:模型变换、视变换、投影变换、视口变换,经过这四个变换后,图形的点就可以正确并如愿地显示在用户屏幕上了;
  • 侧面反应,要正确地渲染图形,就要掌握这四种变换;

2. 各个变换流程分解简述

  • 阶段一:追加 w 分量为 1.0 (第一个蓝框)

这个阶段不需要程序员操作

这里的原因是,OpenGL 需要利用齐次坐标去进行矩阵的运算,核心原因当然就是方便矩阵做乘法咯(R(4x4) 点乘 R(4x1) 嘛)!

  • 阶段二:用户变换 (第二个蓝框)

这个阶段需要程序员操作,在 Vertex Shader Code 中进行操作

这个阶段主要是把模型正确地通过 3D 变换 (旋转、缩放、平移) 放置于摄像机的可视区域(视景体)中,包括处理摄像机的位置、摄像机的可视区域占整个摄像机空间的大小。

这个阶段过后,w 就不在是 1.0 了

  • 阶段三:重新把齐次坐标转换成 3D 坐标 (第三个蓝框)

这个阶段不需要程序员操作

要重新转换回来的原因,也很简单 ---- 齐次坐标只是为了方便做矩阵运算而引入的,而 3D 坐标点才是模型真正需要的点位置信息。

这个阶段过后,所有的点坐标都会标准化(所谓标准化,就是单位为 1),x 和 y 值范围均在 [-1.0, 1.0] 之间,z 就在 [ 0.0, 1.0 ] 之间;

x 和 y 值范围均在 [-1.0, 1.0] 之间,才能正确显示,原因是 OpenGL 的正方体值范围就是 [ -1.0, 1.0 ] 不存在其它范围的值;而 z 的值范围是由摄像机决定的,摄像机所处的位置就是 z = 0,的位置,所以 0 是指无限近,摄像机可视区的最远处就是 z = 1, 所以 1 是指无限远;

  • 阶段四:重新把齐次坐标转换成 3D 坐标 (第四个蓝框)

* 这个阶段需要程序员操作,在图形渲染前要进行操作,即在 gldraw 前 **

这个阶段核心的就是 ViewPort 和 DepthRange 两个,前者是指视口,后者是深度,分别对应的 OpenGL ES 2 的 API 是:

函数 描述
glViewport 调整视窗位置和尺寸
glDepthRange 调整视景体的 near 和 far 两个面的位置 (z)
glViewport
void glViewport(GLint x, GLint y, GLsizei w, GLsizei h)
x, y 以渲染的屏幕坐标系为参考的视口原点坐标值(如:苹果的移动设备都是是以左上角为坐标原点)
w, h 要渲染的视口尺寸,单位是像素
glDepthRange
void glDepthRange(GLclampf n, GLclampf f)
n, f n, f 分别指视景体的 near 和 far ,前者的默认值为 0 ,后者的默认值为 1.0, 它们的值范围均为 [0.0, 1.0], 其实就是 z 值

3. 四次变换与编程应用

  • 下面这两张图片就是 Vertex Shader Code 中的最终代码
#version 100

attribute vec4 v_Position;
uniform mat4 v_Projection, v_ModelView;

attribute vec4 v_Color;
varying mediump vec4 f_color;

void main(void) {
    f_color = v_Color;
    gl_Position  = v_Projection * v_ModelView * v_Position;
}
 v_Projection 表示投影变换;v_ModelView 表示模型变换和视变换;
  • 第一次变换:模型变换,模型空间到世界空间 ( 1 -> 2 )

请看《OpenGL ES 2.0 (iOS)[02]:修复三角形的显示》 这篇文章,专门讲模型变换的。

  • 余下的几次变换,都是和摄像机模型在打交道
    摄像机里面的模型

Camera Model

要完成摄像机正确地显示模型,要设置摄像机位置、摄像机的焦距:

1. 设置摄像机的位置、方向 --&gt; (视变换) gluLookAt (ES 没有这个函数),使要渲染的模型位于摄像机可视区域中;【完成图中 1 和 2】
2. 选择摄像机的焦距去适应整个可视区域 --&gt; (投影变换) glFrustum(视景体的六个面)、gluPerspective(透视) 、glOrtho(正交)( ES 没有这三个函数) 【完成图中 3】
3. 设置图形的视图区域,对于 3D 图形还可以设置 depth- range --&gt; glViewport 、glDepthRange
  • 第二次变换:视变换,世界空间到摄像机空间 ( 2 -> 3 )

上面提到, ES 版本没有 gluLookAt 这个函数,但是我们知道,这里做的都是矩阵运算,所以可以自己写一个功能一样的矩阵函数即可;

// 我不想写,所以可以用 GLKit 提供给我们的函数
/*
 Equivalent to gluLookAt.
 */
GLK_INLINE GLKMatrix4 GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ,
                                                  float centerX, float centerY, float centerZ,
                                                  float upX, float upY, float upZ);

Frustum

函数的 eye x、y、z 就是对应图片中的 Eye at ,即摄像机的位置;
函数的 center x、y、z 就是对应图片中的 z-axis 可视区域的中心点;
函数的 up x、y、z 就是对应图片中的 up 指摄像机上下的位置(就是角度);

  • 第三次变换:投影变换,摄像机空间到裁剪空间 ( 3 -> 4 )

view frustum

当模型处于视景体外时会被剔除掉,如果模型有一部分在视景体内时,模型的点信息只会剩下在视景体内的,其它的点信息不渲染;

/*
 Equivalent to glFrustum.
 */
GLK_INLINE GLKMatrix4 GLKMatrix4MakeFrustum(float left, float right,
                                            float bottom, float top,
                                            float nearZ, float farZ);

这个是设置视景体六个面的大小的;

  • 透视投影

透视投影

对应的投影公式 :

完整的透视投影公式

使用 GLKit 提供的函数:

/*
Equivalent to gluPerspective.
*/
GLK_INLINE GLKMatrix4 GLKMatrix4MakePerspective(float fovyRadians, // 视场角
                                                float aspect,  // 屏幕像素纵横比
                                                float nearZ, // 近平面距摄像机位置的距离
                                                float farZ); // 远平面摄像机位的距离
  • 正交投影

Orthographic projection

对应的投影公式 :

完整的正交投影公式

/*
 Equivalent to glOrtho.
 */
GLK_INLINE GLKMatrix4 GLKMatrix4MakeOrtho(float left, float right,
                                          float bottom, float top,
                                          float nearZ, float farZ);
  • 第四次变换:视口变换,裁剪空间到屏幕空间 ( 4 -> 5 )

这里就是设置 glViewPort 和 glDepthRange 当然 2D 图形不用设置 glDepthRange ;

  • 实际编程过程中的使用过程

  • 第一步,如果是 3D 图形的渲染,那么要绑定深度渲染缓存(DepthRenderBuffer),若是 2D 可以跳过,因为它的顶点信息中没有 z 信息 ( z 就是顶点坐标的深度信息 );

    1. Generate ,请求 depth buffer ,生成相应的内存标识符
    2. Bind,绑定申请的内存标识符
    3. Configure Storage,配置储存 depth buffer 的尺寸
    4. Attach,装载 depth buffer 到 Frame Buffer 中
      具体的程序代码:

  • 第二步,缩写 Vertex Shader Code
#version 100

attribute vec4 v_Position;
uniform mat4 v_Projection, v_ModelView; // 投影变换、模型视图变换

attribute vec4 v_Color;
varying mediump vec4 f_color;

void main(void) {
     f_color = v_Color;
     gl_Position = v_Projection * v_ModelView * v_Position;
}

一般是把四次变换写成这两个,当然也可以写成一个;因为它们是一矩阵,等同于一个常量,所以使用的是 uniform 变量,变量类型就是 mat4 四乘四方阵(齐次矩阵);

  • 第三步,就是外部程序赋值这两个变量

* 注意,要在 glUseProgram 函数后,再使用 glUniform 函数来赋值变量,不然是无效的;**

依次完成 模型变换、视变换、投影变换,即可;它们两两用矩阵乘法进行连接即可;

如:modelMatrix 点乘 viewMatrix , 它们的结果再与 projectionMatrix 点乘,即为 ModelViewMatrix ;

GLKit 点乘函数,
GLK_INLINE GLKMatrix4 GLKMatrix4Multiply(GLKMatrix4 matrixLeft, GLKMatrix4 matrixRight);

  • 第四步,如果是 3D 图形,有 depth buffer ,那么要清除深度渲染缓存

使用 glClear(GL_DEPTH_BUFFER_BIT); 进行清除,当然之后就是要使能深度测试 glEnable(GL_DEPTH_TEST); 不然图形会变形;

最好,也使能 glEnable(GL_CULL_FACE); 这里的意思就是,把在屏幕后面的点剔除掉,就是不渲染;判断是前还是后,是利用提供的模型顶点信息中点与点依次连接形成的基本图元的时钟方向进行判断的,这个 OpenGL 会自行判断;

ClockWise & Counterclockwise

左为顺时针,右为逆时针;

  • 第五步,设置 glViewPort 和 glDepthRange

使用 OpenGL ES 提供的 glViewPort 和 glDepthRange 函数即可;


四、工程例子

Github: 《DrawSquare_3DFix》


五、参考书籍

《OpenGL ES 2.0 Programming Guide》
《OpenGL Programming Guide 8th》
《3D 数学基础:图形与游戏开发》
《OpenGL 超级宝典 第五版》
《Learning OpenGL ES For iOS》

目录

一、目标

1. 基础知识准备
2. 图形分析

二、编写程序

0. 工程结构与整体渲染管线
1. Depth Render Buffer
2. 数据源的编写与绑定
3. 深度测试与绘制
4. 让正方体动起来

三、参考书籍、文章


一、目标

正方体. gif

1. 基础知识准备

a. 渲染管线的基础知识
《OpenGL ES 2.0 (iOS)[01]: 一步从一个小三角开始》

b. 3D 变换
《OpenGL ES 2.0 (iOS)[04]:坐标空间 与 OpenGL ES 2 3D 空间》

2. 图形分析

a. 它是一个正方体,由六个正方形面组成,有 8 个顶点;

b. 正方体并不是二维图形,而是三维图形,即顶点坐标应为 {x, y, z},而且 z 不可能一直为 0;

c. 若由 OpenGL ES 绘制,z 坐标表示深度(depth)信息;

d. 六个面均有不一样的颜色,即 8 个顶点都带有颜色信息,即渲染的顶点要提供相应的颜色信息;

e. 六个正方形面,若由 OpenGL ES 绘制,需要由两个三角面组合而成,即绘制模式为 GL_TRIANGLE*;

f. 正方体的每一个顶点都包含在三个面中,即一个顶点都会被使用多次,即绘制的时候应该使用 glDrawElements 方法而不是 glDrawArrays 方法,所以除 8 个顶点的数据外还需增加下标数据才有可能高效地绘制出正方体;

g. 正方体在不断地旋转运动,即可能要实时改变顶点的信息并进行重新绘制以达到运动的效果(思路:动图就是静态图的快速连续变化,只要变化的速度大于人眼可以辨别的速度,就会产生自然流畅的动图)

分析可程序化:
1) 结合 a、b、c、d 四点可以知道,顶点的数据格式可以为:

#define PositionCoordinateCount         (3)
#define ColorCoordinateCount            (4)
typedef struct {
    GLfloat position[PositionCoordinateCount];
    GLfloat color[ColorCoordinateCount];
} VFVertex;
static const VFVertex vertices[] = {
    {{...}, {...}}
    ......
};

当然你也可以把 position 和 color 分开来,只不过我认为放在一起更好管理罢了。

2) 从 e、f 两点可以知道,增加的数据及绘制的方式:

因为使用 element 方式,所以增加下标信息;

static const GLubyte indices[] = {
    ......
};
    glDrawElements(GL_TRIANGLES,
                   sizeof(indices) / sizeof(indices[0]),
                   GL_UNSIGNED_BYTE,
                   indices);

3) 从 g 点可以知道:

图形的运动,表明图形在一定时间内不断地进行更新(重新绘制并渲染),即只要使用具有定时功能的方法即可处理图形的运动,NSTimer 就可以胜任这个工作,不过 iOS 提供了一个 CADisplayLink 类来专门做定时更新的工作,所以可以选用它进行运动更新;


二、编写程序

0. 工程结构与整体渲染管线

结构目录简述
1) 蓝框是包含 CADisplayLink 子类的类,用于更新渲染,就是让图形动起来;

2) 红框就是整体的渲染管线,所有的绘制渲染工作均在此处;

渲染管线 + Depth
Render Buffer 有三种缓存,Color 、Depth 、Stencil 三种;而单纯绘制 2D 图形的时候因为没有引入 z 坐标(z != 0)而只使用了 Render Buffer 的 Color Render Buffer ;
而如今要进行渲染的正方体,是带有 z 坐标,即深度信息,所以自然要引入 Depth Render Buffer 了;
引入 Depth Render Buffer 并使其工作的步骤:

Depth Render Buffer

ViewController 的程序调度

#import "ViewController.h"

#import "VFGLCubeView.h"

@interface ViewController ()
@property (strong, nonatomic) VFGLCubeView *cubeView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    CGRect rect = CGRectOffset(self.view.frame, 0, 0);
    self.cubeView = [[VFGLCubeView alloc] initWithFrame:rect];

    [_cubeView prepareDisplay];
    [_cubeView drawAndRender];

    [self.view addSubview:_cubeView];

}

- (void)viewDidAppear:(BOOL)animated {

    [super viewDidAppear:animated];

    [self.cubeView update];

}

- (void)viewWillDisappear:(BOOL)animated {

    [super viewWillDisappear:animated];

    [self.cubeView pauseUpdate];

}

@end

内容并不复杂,所以此处不进行赘述;

渲染管线
prepareDisplay + drawAndRender

prepareDisplay 渲染管线的准备部分

- (void)prepareDisplay {

    // 1. Context
    [self settingContext];

    // 2 要在 Render Context setCurrent 后, 再进行 OpenGL ES 的操作
    // [UIColor colorWithRed:0.423 green:0.046 blue:0.875 alpha:1.000]
    // [UIColor colorWithRed:0.423 green:0.431 blue:0.875 alpha:1.000]
    [self setRenderBackgroundColor:RGBAColorMake(0.423, 0.431, 0.875, 1.000)];

    // 2.? Vertex Buffer Object
    self.vboBufferID = [self createVBO];
    [self bindVertexDatasWithVertexBufferID:_vboBufferID
                               bufferTarget:GL_ARRAY_BUFFER
                                   dataSize:sizeof(vertices)
                                       data:vertices
                                   elements:NO];

    [self bindVertexDatasWithVertexBufferID:kInvaildBufferID
                               bufferTarget:GL_ELEMENT_ARRAY_BUFFER
                                   dataSize:sizeof(indices)
                                       data:indices
                                   elements:YES];

    // 3. Shader
    GLuint vertexShaderID = [self createShaderWithType:GL_VERTEX_SHADER];
    [self compileVertexShaderWithShaderID:vertexShaderID type:GL_VERTEX_SHADER];

    GLuint fragmentShaderID = [self createShaderWithType:GL_FRAGMENT_SHADER];
    [self compileVertexShaderWithShaderID:fragmentShaderID type:GL_FRAGMENT_SHADER];

    self.programID = [self createShaderProgram];
    [self attachShaderToProgram:_programID
                  vertextShader:vertexShaderID
                 fragmentShader:fragmentShaderID];

    [self linkProgramWithProgramID:_programID];

    [self updateUniformsLocationsWithProgramID:_programID];

    // 4. Attach VBOs
    [self attachCubeVertexArrays];

}

基于这部分,本文的工作在以下两处进行:

    // 1. Context
    [self settingContext];

它负责确定渲染上下文,以及 Render Buffer 与 Frame Buffer 的资源绑定处理;
[self settingContext]; 详见 本章 1.Depth Render Buffer 一节

    // 2.? Vertex Buffer Object
    self.vboBufferID = [self createVBO];
    [self bindVertexDatasWithVertexBufferID:_vboBufferID
                               bufferTarget:GL_ARRAY_BUFFER
                                   dataSize:sizeof(vertices)
                                       data:vertices
                                   elements:NO];

    [self bindVertexDatasWithVertexBufferID:kInvaildBufferID
                               bufferTarget:GL_ELEMENT_ARRAY_BUFFER
                                   dataSize:sizeof(indices)
                                       data:indices
                                   elements:YES];

它是处理顶点缓存数据的;
VBO 与 数据源 详见 本章 2. 数据源的编写与绑定

drawAndRender 渲染管线的余下部分

- (void)drawAndRender {

    // 5. Draw Cube
    // 5.0 使用 Shader
    [self userShaderWithProgramID:_programID];

    // 5.1 应用 3D 变换
    self.modelPosition = GLKVector3Make(0, -0.5, -5);
    [self transforms];

    // 5.2 清除旧渲染缓存
    [self clearColorRenderBuffer:YES depth:YES stencil:NO];

    // 5.3 开启深度测试
    [self enableDepthTesting];

    // 5.4 绘制图形
    [self drawCube];

    // 5.5 渲染图形
    [self render];

}

基于这部分,本文的工作在此处进行:

    // 5.2 清除旧渲染缓存
    [self clearColorRenderBuffer:YES depth:YES stencil:NO];

    // 5.3 开启深度测试
    [self enableDepthTesting];

    // 5.4 绘制图形
    [self drawCube];

详见 本章 3. 深度测试与绘制 一节

关于实时更新的内容

    [self.cubeView update];
    [self.cubeView pauseUpdate];

详见 本章 4. 让正方体动起来

1. Depth Render Buffer

[self settingContext];
它的内容为:

- (void)setContext:(EAGLContext *)context {

    if (_context != context) {

        [EAGLContext setCurrentContext:_context];

        [self deleteFrameBuffer:@[@(self.frameBufferID)]];
        self.frameBufferID = kInvaildBufferID;

        [self deleteRenderBuffer:@[@(self.colorRenderBufferID), @(self.depthRenderBufferID)]];
        self.colorRenderBufferID = self.depthRenderBufferID = kInvaildBufferID;

        _context = context;

        if (context != nil) {

            _context = context;
            [EAGLContext setCurrentContext:_context];

            // 2. Render / Frame Buffer

            // 2.0 创建 Frame Buffer
            [self deleteFrameBuffer:@[@(self.frameBufferID)]];

            self.frameBufferID = [self createFrameBuffer];

            // 2.1 Color & Depth Render Buffer
            [self deleteRenderBuffer:@[@(self.colorRenderBufferID)]];

            self.colorRenderBufferID = [self createRenderBuffer];

            [self renderBufferStrogeWithRenderID:self.colorRenderBufferID];

            [self attachRenderBufferToFrameBufferWithRenderBufferID:self.colorRenderBufferID
                                                         attachment:GL_COLOR_ATTACHMENT0];

            // 2.2 检查 Frame 装载 Render Buffer 的问题
            [self checkFrameBufferStatus];

            // 2.3 Add Depth Render Buffer
            [self enableDepthRenderBuffer];

            [self deleteRenderBuffer:@[@(self.depthRenderBufferID)]];

            if ( ! CGSizeEqualToSize(self.renderBufferSize, CGSizeZero) &&
                self.depthMode != VFDrawableDepthMode_None) {

                self.depthRenderBufferID = [self createRenderBuffer];

                if (self.depthRenderBufferID == kInvaildBufferID) {
                    return;
                }

                [self renderBufferStrogeWithRenderID:self.depthRenderBufferID];

                [self attachRenderBufferToFrameBufferWithRenderBufferID:self.depthRenderBufferID
                                                             attachment:GL_DEPTH_ATTACHMENT];

            }

            // 2.4 检查 Frame 装载 Render Buffer 的问题
            [self checkFrameBufferStatus];

        }

    }

}

- (void)settingContext {

    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

}

这里重写了 setContext: 方法,核心内容是
// 2.3 Add Depth Render Buffer

    // 2.3 Add Depth Render Buffer
    [self enableDepthRenderBuffer];

    [self deleteRenderBuffer:@[@(self.depthRenderBufferID)]];

    if ( ! CGSizeEqualToSize(self.renderBufferSize, CGSizeZero) &&
        self.depthMode != VFDrawableDepthMode_None) {

        self.depthRenderBufferID = [self createRenderBuffer];

        if (self.depthRenderBufferID == kInvaildBufferID) {
            return;
        }

        [self renderBufferStrogeWithRenderID:self.depthRenderBufferID];

        [self attachRenderBufferToFrameBufferWithRenderBufferID:self.depthRenderBufferID
                                                     attachment:GL_DEPTH_ATTACHMENT];

    }

步骤分解:

Step One

第一步,创建并绑定深度渲染缓存,对应程序代码为:

self.depthRenderBufferID = [self createRenderBuffer];
- (GLuint)createRenderBuffer {

    GLuint ID = kInvaildBufferID;
    glGenRenderbuffers(RenderMemoryBlock, &ID);  // 申请 Render Buffer
    glBindRenderbuffer(GL_RENDERBUFFER, ID); // 创建 Render Buffer

    return ID;

}

第二步,存储新创建的渲染缓存,对应程序代码为:

[self renderBufferStrogeWithRenderID:self.depthRenderBufferID];
- (void)renderBufferStrogeWithRenderID:(GLuint)renderBufferID {

    if (renderBufferID == self.colorRenderBufferID) {

        // 必须要在 glbindRenderBuffer 之后 (就是使用 Render Buffer 之后), 再绑定渲染的图层
        [self bindDrawableObjectToRenderBuffer];

        self.renderBufferSize = [self getRenderBufferSize];

    }

    if (renderBufferID == self.depthRenderBufferID) {

        glRenderbufferStorage(GL_RENDERBUFFER,
                              GL_DEPTH_COMPONENT16,
                              self.renderBufferSize.width,
                              self.renderBufferSize.height);

    }

}

核心函数:存储渲染信息

glRenderbufferStorage
void glRenderbufferStorage(GLenum target,GLenum internalformat,GLsizei width, GLsizei height)
target _只能是 GLRENDERBUFFER
internalformat 可用选项见下表
width 渲染缓存的宽度(像素单位)
height 渲染缓存的高度(像素单位)
internalformat 存储格式(位 = bit)
颜色方面 GL_RGB565(5 + 6 + 5 = 16 位)、GL_RGBA4(4 x 4 = 16)、GL_RGB5_A1(5 + 5 + 5 + 1 = 16)、GL_RGB8_OES(3 x 8 = 24 )、GL_RGBA8_OES(4 x 8 = 32)
深度方面 GL_DEPTH_COMPONENT16(16 位)、GL_DEPTH_COMPONENT24_OES(24 位)、GL_DEPTH_COMPONENT32_OES(32 位)
模板方面 GL_STENCIL_INDEX8、GL_STENCIL_INDEX1_OES、GL_STENCIL_INDEX4_OES
深度与模板 GL_DEPTH24_STENCIL8_OES

第三步,装载渲染缓存到帧缓存中,对应程序代码为:

[self attachRenderBufferToFrameBufferWithRenderBufferID:self.depthRenderBufferID
                                             attachment:GL_DEPTH_ATTACHMENT];
- (void)attachRenderBufferToFrameBufferWithRenderBufferID:(GLuint)renderBufferID attachment:(GLenum)attachment {

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, renderBufferID);

}
2. 数据源的编写与绑定

数据源的书写
从 2D 到 3D :

右下方,线框正方体的 8 个顶点坐标分布,其实 0~7 的编号是你决定的,也就是说 0 放在那里开始都是可以的,只要是 8 个点即可;

Cube

static const VFVertex vertices[] = {
    // Front
    // 0 [UIColor colorWithRed:0.438 green:0.786 blue:1.000 alpha:1.000]
    {{ 1.0, -1.0,  1.0}, {0.438, 0.786, 1.000, 1.000}}, // 淡(蓝) -- 0

    // 1 [UIColor colorWithRed:1.000 green:0.557 blue:0.246 alpha:1.000]
    {{ 1.0,  1.0,  1.0}, {1.000, 0.557, 0.246, 1.000}}, // 淡(橙) -- 1

    // 2 [UIColor colorWithRed:0.357 green:0.927 blue:0.690 alpha:1.000]
    {{-1.0,  1.0,  1.0}, {0.357, 0.927, 0.690, 1.000}}, // 蓝(绿) -- 2

    // 3 [UIColor colorWithRed:0.860 green:0.890 blue:0.897 alpha:1.000]
    {{-1.0, -1.0,  1.0}, {0.860, 0.890, 0.897, 1.000}}, // 超淡蓝 偏(白) -- 3

    // Back
    // 4 [UIColor colorWithRed:0.860 green:0.890 blue:0.897 alpha:1.000]
    {{-1.0, -1.0, -1.0}, {0.860, 0.890, 0.897, 1.000}}, // 超淡蓝 偏(白) -- 4

    // 5 [UIColor colorWithRed:0.357 green:0.927 blue:0.690 alpha:1.000]
    {{-1.0,  1.0, -1.0}, {0.357, 0.927, 0.690, 1.000}}, // 蓝(绿) -- 5

    // 6 [UIColor colorWithRed:1.000 green:0.557 blue:0.246 alpha:1.000]
    {{ 1.0,  1.0, -1.0}, {1.000, 0.557, 0.246, 1.000}}, // 淡(橙) -- 6

    // 7 [UIColor colorWithRed:0.438 green:0.786 blue:1.000 alpha:1.000]
    {{ 1.0, -1.0, -1.0}, {0.438, 0.786, 1.000, 1.000}}, // 淡(蓝) -- 7
};

只要你空间想像不是特别差,估计能看出每个点的坐标吧!你可以把这样的点 {1.0, -1.0, -1.0} 改成你喜欢的数值亦可,只要最终是正方体即可;

真正重要的数据其实是下标数据:

static const GLubyte indices[] = {
    // Front  ------------- 蓝橙绿白 中间线(蓝绿)
    0, 1, 2, // 蓝橙绿
    2, 3, 0, // 绿白蓝
    // Back   ------------- 蓝橙绿白 中间线(白橙)
    4, 5, 6, // 白绿橙
    6, 7, 4, // 橙蓝白
    // Left   ------------- 白绿
    3, 2, 5, // 白绿绿
    5, 4, 3, // 绿白白
    // Right  ------------- 蓝橙
    7, 6, 1, // 蓝橙橙
    1, 0, 7, // 橙蓝蓝
    // Top    ------------- 橙绿
    1, 6, 5, // 橙橙绿
    5, 2, 1, // 绿绿橙
    // Bottom ------------- 白蓝
    3, 4, 7, // 白白蓝
    7, 0, 3  // 蓝蓝白
};

这些下标的值由两个因素决定,第一个因素是上面 8 个顶点数据的下标;第二个因素是时钟方向;

现在看看时钟方向:

有没有发现,每一个正方形的两个小三角,都是逆时针方向的;当然你也可以换成顺时针方向,相应的下标数据就要发生改变;

EP: 如 Front 这个面,如果使用顺时针来写数据为:

    // Front  ------------- 白绿橙蓝 中间线(白橙)
    3, 2, 1, // 白绿橙
    1, 0, 2, // 橙蓝绿

你也可以从 2 或 1 开始,看你的喜好咯;

方向只有两个:

资源绑定
这里主要是 VBO 的数据绑定,增加 Element 的支持而已;

    [self bindVertexDatasWithVertexBufferID:kInvaildBufferID
                               bufferTarget:GL_ELEMENT_ARRAY_BUFFER
                                   dataSize:sizeof(indices)
                                       data:indices
                                   elements:YES];
- (void)bindVertexDatasWithVertexBufferID:(GLuint)vertexBufferID bufferTarget:(GLenum)target dataSize:(GLsizeiptr)size data:(const GLvoid *)data elements:(BOOL)isElement {

    if ( ! isElement) {
        glBindBuffer(target, vertexBufferID);
    }

    // 创建 资源 ( context )
    glBufferData(target,            // 缓存块 类型
                 size,              // 创建的 缓存块 尺寸
                 data,              // 要绑定的顶点数据
                 GL_STATIC_DRAW);   // 缓存块 用途

}

此处不再赘述;
如果实在不懂,请移步至
《OpenGL ES 2.0 (iOS)[03]:熟练图元绘制,玩转二维图形》练习练习;

3. 深度测试与绘制

Step Two

清除旧的深度缓存信息

[self clearColorRenderBuffer:YES depth:YES stencil:NO];
- (void)clearColorRenderBuffer:(BOOL)color depth:(BOOL)depth stencil:(BOOL)stencil {

    GLbitfield colorBit     = 0;
    GLbitfield depthBit     = 0;
    GLbitfield stencilBit   = 0;

    if (color)      { colorBit      = GL_COLOR_BUFFER_BIT;     }
    if (depth)      { depthBit      = GL_DEPTH_BUFFER_BIT;     }
    if (stencil)    { stencilBit    = GL_STENCIL_BUFFER_BIT;   }

    glClear(colorBit | depthBit | stencilBit);

}

启用深度测试

[self enableDepthTesting];
- (void)enableDepthTesting {

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);

}

这里多了一个 GL_CULL_FACE 的启用,它的意思就是,把看不见的像素信息剔除掉,只保留能看见的信息(留前去后);
如果没有启用 GL_DEPTH_TEST 程序运行后是这样的:

关掉 GL_DEPTH_TEST.gif

很明显图形是有穿透性的,如果去掉 GL_DEPTH_TEST 就不是实体的正方体了;当然如果你喜欢这种效果,也可以关掉 GL_DEPTH_TEST (反正我个人觉得关掉也蛮好看的);

重新绑定 Color Render Buffer
原因,因为当绑定 Depth Render Buffer 之后,渲染管线从原来的绑定(激活)的 Color Render Buffer 切换成了,绑定(激活)Depth Render Buffer ,从而导致渲染出来的结果,不是期望中的那样;所以在绘制前要重新绑定(激活)Color Render Buffer .

Step Three

- (void)drawCube {

    // 失败的核心原因
    // 因为 depth buffer 是最后一个绑定的,所以当前渲染的 buffer 变成了 depth 而不是 color
    // 所以 渲染的图形没有任何变化,无法产生深度效果
    // Make the Color Render Buffer the current buffer for display
    [self rebindRenderBuffer:@[@(self.colorRenderBufferID)]];

    [self rebindVertexBuffer:@[@(self.vboBufferID)]];

    glDrawElements(GL_TRIANGLES,
                   sizeof(indices) / sizeof(indices[0]),
                   GL_UNSIGNED_BYTE,
                   indices);

}

这是注释了代码中,[self rebindRenderBuffer:@[@(self.colorRenderBufferID)]]; 的运行结果;

4. 让正方体动起来

ViewController 的调度
其实就是,view 显示的时候更新,不显示的时候停止更新;

- (void)viewDidAppear:(BOOL)animated {

    [super viewDidAppear:animated];

    [self.cubeView update];

}

- (void)viewWillDisappear:(BOOL)animated {

    [super viewWillDisappear:animated];

    [self.cubeView pauseUpdate];

}

CubeView 的应用

#pragma mark - DisplayLink Update

- (void)preferTransformsWithTimes:(NSTimeInterval)time {

    GLfloat rotateX = self.modelRotate.x;
//    rotateX += M_PI_4 * time;

    GLfloat rotateY = self.modelRotate.y;
    rotateY += M_PI_2 * time;

    GLfloat rotateZ = self.modelRotate.z;
    rotateZ += M_PI * time;

    self.modelRotate = GLKVector3Make(rotateX, rotateY, rotateZ);

}

本类提供的改变参数有:

@property (assign, nonatomic) GLKVector3 modelPosition, modelRotate, modelScale;
@property (assign, nonatomic) GLKVector3 viewPosition , viewRotate , viewScale ;
@property (assign, nonatomic) GLfloat projectionFov, projectionScaleFix, projectionNearZ, projectionFarZ;

已经包含了所有的变换操作;

以下的几个方法均是处理 VFRedisplay 类的实时更新问题;

// <VFRedisplayDelegate>
- (void)updateContentsWithTimes:(NSTimeInterval)times {

    [self preferTransformsWithTimes:times];
    [self drawAndRender];

}

#pragma mark - Update

- (void)update {

    self.displayUpdate = [[VFRedisplay alloc] init];
    self.displayUpdate.delegate = self;
    self.displayUpdate.preferredFramesPerSecond = 25;
    self.displayUpdate.updateContentTimes = arc4random_uniform(650) / 10000.0;
    [self.displayUpdate startUpdate];

}

- (void)pauseUpdate {

    [self.displayUpdate pauseUpdate];

}

#pragma mark - Dealloc

- (void)dealloc {

    [self.displayUpdate endUpdate];

}
    self.displayUpdate.preferredFramesPerSecond = 25; //更新频率
    self.displayUpdate.updateContentTimes = arc4random_uniform(650) / 10000.0; // 控制变化率(快慢)

核心是 - (void)updateContentsWithTimes:(NSTimeInterval)times 方法,这个方法是用于更新时,实时调用的方法;由VFRedisplay 类提供的协议 @interface VFGLCubeView ()<VFRedisplayDelegate> 方法;

VFRedisplay.h 主要内容

@protocol VFRedisplayDelegate <NSObject>

- (void)updateContentsWithTimes:(NSTimeInterval)times;

@end

......

- (void)startUpdate;
- (void)pauseUpdate;
- (void)endUpdate;

VFRedisplay.m 主要内容
开始更新的方法:

- (void)startUpdate {

    if ( ! self.delegate ) {
        return;
    }

    self.displayLink = [CADisplayLink displayLinkWithTarget:self
                                                   selector:@selector(displayContents:)];

    self.displayLink.frameInterval = (NSUInteger)MAX(kLeastSeconds,
                                                     (kTotalSeconds / self.preferredFramesPerSecond));

    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop]
                           forMode:NSDefaultRunLoopMode];

    self.displayPause = kDefaultDisplayPause;

}

- (void)displayContents:(CADisplayLink *)sender {

    if ([self.delegate respondsToSelector:@selector(updateContentsWithTimes:)]) {

        [self.delegate updateContentsWithTimes:self.updateContentTimes];

    }

}

四步走:
第一步,创建相应的更新调度方法- (void)displayContents:(CADisplayLink *)sender,这个方法必须是- (void)selector:(CADisplayLink *)sender这种类型的;
第二步,指定一个更新频率(就是一秒更新多少次)frameInterval 一般是 24、25、30,默认是 30 的;
第三步,把 CADisplayLink 的子类添加到当前的 RunLoop [NSRunLoop currentRunLoop] 上,不然程序是无法调度指定的方法的;
第四步,启动更新 static const BOOL kDefaultDisplayPause = NO; 

displayPause 属性

@property (assign, nonatomic) BOOL displayPause;
@dynamic displayPause;
- (void)setDisplayPause:(BOOL)displayPause {
    self.displayLink.paused = displayPause;
}
- (BOOL)displayPause {
    return self.displayLink.paused;
}

停止更新的方法:

- (void)pauseUpdate {

    self.displayPause = YES;

}

结束更新的方法:

- (void)endUpdate {

    self.displayPause = YES;
    [self.displayLink invalidate];
    [self.displayLink removeFromRunLoop:[NSRunLoop currentRunLoop]
                                forMode:NSDefaultRunLoopMode];

}

不用的时候,当然要先停止更新,再关掉时钟(CADisplayLink 就是一个时钟类),最后要从当前 RunLoop 中移除;

5. 工程文件

Github: DrawCube

Github:DrawCube_Onestep

增加魔方色开关,RubikCubeColor 宏定义;

开关

数据源

正方体_魔方色. gif


三、参考书籍、文章

《OpenGL ES 2 Programming Guide》
《OpenGL Programming Guide》8th
《Learning OpenGL ES For iOS》
RW.OpenGLES2.0

前言:如果你没有 OpenGL ES 2 的基础知识,请先移步 《OpenGL ES 2.0 (iOS) 笔记大纲》 学习一下基础的知识。

目录

一、软件运行效果演示
(一)、最终效果
(二)、信息提取
二、纹理处理的流程【核心】
(一)、Texture 是什么?
(二)、Texture
(三)、引入了 Texture 的 Shader 文件
(四)、Texture 正确的 “书写” 顺序
三、知识扩充:图片加载
使用 Quartz Core 的知识加载图片数据


一、软件运行效果演示

(一)、最终效果

工程地址:Github

Texture-Base.gif

(二)、信息提取

  1. 不同的模型【2D & 3D】,不同维度下,Texture 的处理区别
  2. 单一像素信息【pixelBuffer】 与 复杂像素信息【图片】的显示区别
  3. 正方图【单张或多张图片】 与 长方图,像素的显示控制区别

二、纹理处理的流程【核心】

(一)、Texture 是什么?

Texture 纹理,就是一堆被精心排列过的像素;

  1. 因为 OpenGL 就是图像处理库,所以 Texture 在 OpenGL 里面有多重要,可想而知;
  2. 其中间接地鉴明了一点,图片本身可以有多大变化,OpenGL 就可以有多少种变化。

学好 Texture 非常重要

(二)、Texture

Texture 在 OpenGL 里面有很多种类,但在 ES 版本中就两种——Texture_2D + Texture_CubeMap;

Texture_2D: 就是 {x, y} 二维空间下的像素呈现,也就是说,由效果图上演示可知,很难做到使正方体的六个面出现不同的像素组合;图片处理一般都使用这个模式;[x 、y 属于 [0, 1] 这个范围]

Texture_CubeMap: 就是 {x, y, z} 三维空间下的像素呈现,也就如效果图中演示的正方体的六个面可以出现不同的像素组合;它一般是用于做环境贴图——就是制作一个环境,让 3D 模型如同置身于真实环境中【卡通环境中也行】。[x、y、z 属于 [-1, 1] 这个范围,就是与 Vertex Position 的值范围一致]

注:上面提到的所有坐标范围是指有效渲染范围,也就是说你如果提供的纹理坐标超出了这个范围也没有问题,只不过超出的部分就不渲染了;

感受一下怎么具体表达:

// VYVertex
typedef struct {
    GLfloat position[3];
    GLfloat texCoord[2];
    GLfloat normalCoord[3];
}VYVertex;

Texture_2D:

// Square
static const VYVertex tex2DSquareDatas[] = {
    {{-1.0, -1.0, 0.0}, {0.0, 0.0}},
    {{ 1.0, -1.0, 0.0}, {1.0, 0.0}},
    {{ 1.0,  1.0, 0.0}, {1.0, 1.0}},
    {{-1.0,  1.0, 0.0}, {0.0, 1.0}},
};
// Cube
static const VYVertex tex2DCubeDatas[] = {

    // Front [Front 的 z 是正的]
    {{-1.0, -1.0,  1.0}, {0.0, 0.0}}, // 0
    {{ 1.0, -1.0,  1.0}, {1.0, 0.0}}, // 1
    {{ 1.0,  1.0,  1.0}, {1.0, 1.0}}, // 2
    {{-1.0,  1.0,  1.0}, {0.0, 1.0}}, // 3
    // Back [Back 的 z 是负的]
    {{-1.0,  1.0, -1.0}, {0.0, 0.0}}, //4[3: -Z]
    {{ 1.0,  1.0, -1.0}, {1.0, 0.0}}, //5[2: -Z]
    {{ 1.0, -1.0, -1.0}, {1.0, 1.0}}, //6[1: -Z]
    {{-1.0, -1.0, -1.0}, {0.0, 1.0}}, //7[0: -Z]
    // Left [Left 的 x 是负的]
    {{-1.0, -1.0,  1.0}, {0.0, 0.0}}, //8[0]
    {{-1.0,  1.0,  1.0}, {1.0, 0.0}}, //9[3]
    {{-1.0,  1.0, -1.0}, {1.0, 1.0}}, //10[4]
    {{-1.0, -1.0, -1.0}, {0.0, 1.0}}, //11[7]
    // Right [Right 的 x 是正的]
    {{ 1.0, -1.0,  1.0}, {0.0, 0.0}}, //12[1]
    {{ 1.0, -1.0, -1.0}, {1.0, 0.0}}, //13[6]
    {{ 1.0,  1.0, -1.0}, {1.0, 1.0}}, //14[5]
    {{ 1.0,  1.0,  1.0}, {0.0, 1.0}}, //15[2]
    // Top [Top 的 y 是正的]
    {{-1.0,  1.0,  1.0}, {0.0, 0.0}}, //16[3]
    {{ 1.0,  1.0,  1.0}, {1.0, 0.0}}, //17[2]
    {{ 1.0,  1.0, -1.0}, {1.0, 1.0}}, //18[5]
    {{-1.0,  1.0, -1.0}, {0.0, 1.0}}, //19[4]
    // Bottom [Bottom 的 y 是负的]
    {{-1.0, -1.0,  1.0}, {0.0, 0.0}}, //20[0]
    {{-1.0, -1.0, -1.0}, {1.0, 0.0}}, //21[7]
    {{ 1.0, -1.0, -1.0}, {1.0, 1.0}}, //22[6]
    {{ 1.0, -1.0,  1.0}, {0.0, 1.0}}, //23[1]

};

Texture_CubeMap:

// Cube Map
static const VYVertex texCubemapCubeDatas[] = {

    // Front [Front 的 z 是正的]
    {{-1.0, -1.0,  1.0}, {}, {-1.0, -1.0,  1.0}}, // 0
    {{ 1.0, -1.0,  1.0}, {}, { 1.0, -1.0,  1.0}}, // 1
    {{ 1.0,  1.0,  1.0}, {}, { 1.0,  1.0,  1.0}}, // 2
    {{-1.0,  1.0,  1.0}, {}, {-1.0,  1.0,  1.0}}, // 3
    // Back [Back 的 z 是负的]
    {{-1.0,  1.0, -1.0}, {}, {-1.0,  1.0, -1.0}}, //4[3: -Z]
    {{ 1.0,  1.0, -1.0}, {}, { 1.0,  1.0, -1.0}}, //5[2: -Z]
    {{ 1.0, -1.0, -1.0}, {}, { 1.0, -1.0, -1.0}}, //6[1: -Z]
    {{-1.0, -1.0, -1.0}, {}, {-1.0, -1.0, -1.0}}, //7[0: -Z]
    // Left [Left 的 x 是负的]
    {{-1.0, -1.0,  1.0}, {}, {-1.0, -1.0,  1.0}}, //8[0]
    {{-1.0,  1.0,  1.0}, {}, {-1.0,  1.0,  1.0}}, //9[3]
    {{-1.0,  1.0, -1.0}, {}, {-1.0,  1.0, -1.0}}, //10[4]
    {{-1.0, -1.0, -1.0}, {}, {-1.0, -1.0, -1.0}}, //11[7]
    // Right [Right 的 x 是正的]
    {{ 1.0, -1.0,  1.0}, {}, { 1.0, -1.0,  1.0}}, //12[1]
    {{ 1.0, -1.0, -1.0}, {}, { 1.0, -1.0, -1.0}}, //13[6]
    {{ 1.0,  1.0, -1.0}, {}, { 1.0,  1.0, -1.0}}, //14[5]
    {{ 1.0,  1.0,  1.0}, {}, { 1.0,  1.0,  1.0}}, //15[2]
    // Top [Top 的 y 是正的]
    {{-1.0,  1.0,  1.0}, {}, {-1.0,  1.0,  1.0}}, //16[3]
    {{ 1.0,  1.0,  1.0}, {}, { 1.0,  1.0,  1.0}}, //17[2]
    {{ 1.0,  1.0, -1.0}, {}, { 1.0,  1.0, -1.0}}, //18[5]
    {{-1.0,  1.0, -1.0}, {}, {-1.0,  1.0, -1.0}}, //19[4]
    // Bottom [Bottom 的 y 是负的]
    {{-1.0, -1.0,  1.0}, {}, {-1.0, -1.0,  1.0}}, //20[0]
    {{-1.0, -1.0, -1.0}, {}, { 1.0, -1.0,  1.0}}, //21[7]
    {{ 1.0, -1.0, -1.0}, {}, { 1.0, -1.0, -1.0}}, //22[6]
    {{ 1.0, -1.0,  1.0}, {}, {-1.0, -1.0, -1.0}}, //23[1]

};

这种坐标,是刚好贴合【完全覆盖】的状态;

数据特点:一个顶点数据绑定一个纹理数据;

【有没有注意到,CubeMap 里面就是直接拷贝顶点数据到纹理坐标上,就行了。(CubeMap 中间那个空的 {} 是结构体中的 2D 纹理数据(就是空的))】

其它的数据形态【对于不是正方的图片】,
【希望大一点,或小一点,即只显示某一部分】:

都是类似图中的分割一样,划分成多个小图片【小的 TexCoord】,最终的数据形态是:

static const VYVertex tex2DElongatedDDCubeDatas[] = {

    // Front [Front 的 z 是正的]
    {{-1.0, -1.0,  1.0}, {0.000, 0.000}}, // 0
    {{ 1.0, -1.0,  1.0}, {0.250, 0.000}}, // 1
    {{ 1.0,  1.0,  1.0}, {0.250, 0.500}}, // 2
    {{-1.0,  1.0,  1.0}, {0.000, 0.500}}, // 3
    // Back [Back 的 z 是负的]
    {{-1.0,  1.0, -1.0}, {0.000, 0.500}}, //4[3: -Z]
    {{ 1.0,  1.0, -1.0}, {0.250, 0.500}}, //5[2: -Z]
    {{ 1.0, -1.0, -1.0}, {0.250, 1.000}}, //6[1: -Z]
    {{-1.0, -1.0, -1.0}, {0.000, 1.000}}, //7[0: -Z]
    // Left [Left 的 x 是负的]
    {{-1.0, -1.0,  1.0}, {0.250, 0.000}}, //8[0]
    {{-1.0,  1.0,  1.0}, {0.500, 0.000}}, //9[3]
    {{-1.0,  1.0, -1.0}, {0.500, 0.500}}, //10[4]
    {{-1.0, -1.0, -1.0}, {0.250, 0.500}}, //11[7]
    // Right [Right 的 x 是正的]
    {{ 1.0, -1.0,  1.0}, {0.250, 0.500}}, //12[1]
    {{ 1.0, -1.0, -1.0}, {0.500, 0.500}}, //13[6]
    {{ 1.0,  1.0, -1.0}, {0.500, 1.000}}, //14[5]
    {{ 1.0,  1.0,  1.0}, {0.250, 1.000}}, //15[2]
    // Top [Top 的 y 是正的]
    {{-1.0,  1.0,  1.0}, {0.500, 0.000}}, //16[3]
    {{ 1.0,  1.0,  1.0}, {0.750, 0.000}}, //17[2]
    {{ 1.0,  1.0, -1.0}, {0.750, 0.500}}, //18[5]
    {{-1.0,  1.0, -1.0}, {0.500, 0.500}}, //19[4]
    // Bottom [Bottom 的 y 是负的]
    {{-1.0, -1.0,  1.0}, {0.750, 0.000}}, //20[0]
    {{-1.0, -1.0, -1.0}, {1.000, 0.000}}, //21[7]
    {{ 1.0, -1.0, -1.0}, {1.000, 0.500}}, //22[6]
    {{ 1.0, -1.0,  1.0}, {0.750, 0.500}}, //23[1]

};

也可以是没有填充完整的图片,只取其中的一部分,数据形态也是上面的:

扩展:
CubeMap 用于做环境贴图,还需要 Light + Shadow 【光 + 阴影】的知识,为什么?环境,有物体 + 自然光 + 人造光 + 光与物体产生的阴影 + 光与物体作用后的颜色;【颜色和阴影是因为有光才产生的,OpenGL 本身默认有一个全局光,不然你没有写光的代码,为什么可以看到你渲染的模型体】
即只有在具备了 光 + 影 的知识,去学习 环境贴图才好理解;【贴图:HDR 图片 (效果中的那张蓝色森林就是 HDR 图,没有做 CubeMap) + CubeMap 格式】

CubeMap 图片格式,就是把下图中的 HDR 图片直接转换成,六个黄色框框的图像,框框之间的边缘是连接的哦:

连接

MipMapping: 根据不同的情形加载不同大小的图片进行渲染;【不同情形,指不同远近,不同光影环境下对图片 “看清”“看不清” 的程度,OpenGL 自动选择合适的图片大小】【不同大小的图片,程序员要事先加载一张图片的不同大小 ( 2^n , 2^m ) 的像素数据(0 ~ n level),又因为 ES 是基于移动端的,所以内存容易告急,即能不用则不用】

Fliter + 特效 : 我们天天看到的最多的东西,就是给图片像素加入各种 “想法” 变成你想要的效果【加雾、马赛克、调色、镜像、模糊、素描、液化、叠加、艺术化 ......】,它的核心知识在 Fragment Shader【重点】 + OpenGL ES 提供的基础混合模式【滤波 + Blend】,放在下一篇文章专门讲;

粒子系统:Texture + Point Sprites,制作雨水、下雪、飞舞的花瓣...... 只要渲染效果要求有多个相似点在那动来动去的,都可以用它们来实现;【数学中的分形理论好像也可以用上】【粒子,会用专门的一篇文章讲】

所有的 “花样” 特效,不管被称之为什么,都与 数学知识【算法】 和 颜色构成知识【光构成、色彩构成】 密不可分;

所以我就要怕了吗?
错,你应该兴奋;因为~~ 反正我也没有什么可以失去的了,上来不就是干了吗? ^ _ ^ + ~_~ + $-$

(三)、引入了 Texture 的 Shader 文件

Texture_2D:

2D Vertex:

#version 100

uniform mat4 u_modelViewMat4;
uniform mat4 u_projectionMat4;

attribute vec4 a_position;
attribute vec2 a_texCoord;

varying highp vec2 v_texCoord;

void main(void) {
    gl_Position = u_projectionMat4 * u_modelViewMat4 * a_position;
    v_texCoord  = a_texCoord;
}

纹理输入输出:

...
attribute vec2 a_texCoord;
varying highp vec2 v_texCoord;

void main(void) {
    ...
    v_texCoord  = a_texCoord;
}

输入:
vec2 a_texCoord,上面提到过它是 {x, y} 的坐标,所以使用的也是 vec2 ;

输出:
同样是 vec2 ,但是一定要记住加 highp 精度限定符,不然编译会报错哦;

不知道,你是否还记得渲染管线中的 Texture Memory ,看下图:

渲染管线

红色框框住的虚线,就是指代 Vertex Shader 中的纹理坐标信息;

直接给的,为什么是虚线?
看清楚 Shader 代码,这里是直接就赋值【输入 = 输出,经过其它变换也行】了,也就是 Vertex Shader 内部不需要使用到它,它只是为了传到 Fragment 里面使用的【varying 的作用】,所以就使用虚线来表示;

2D Fragment:

#version 100

uniform sampler2D us2d_texture;

varying highp vec2 v_texCoord;

void main(void) {
//    gl_FragColor = vec4(1, 1, 0.5, 1);
    gl_FragColor = texture2D(us2d_texture, v_texCoord);
}

上面的渲染管线图中,黄色框框住的实线,就是指代 Fragment Shader 中的像素数据【sampler2D】来源;

这里是核心,输入输出:

uniform sampler2D us2d_texture;
...

void main(void) {
    gl_FragColor = texture2D(us2d_texture, ...);
}

输入:
sampler2D 就是一堆静态数据的意思,像素信息就是一堆固定【不管是写死,还是程序自动生成,都一样】的颜色信息,所以要使用这种常量块的类型限定符;

输出:
这里要使用 texture2D 内置函数来处理像素信息生成 vec4 的颜色信息,原型 vec4 texture2D(sampler2D s, vec2 texCoord);

所以剩下的问题就是如何得到 sampler2D 数据,并如何将像素数据写入到 Shader 中

Texture_CubeMap:

#version 100

uniform mat4 u_modelViewMat4;
uniform mat4 u_projectionMat4;

attribute vec4 a_position;
attribute vec3 a_normalCoord;
varying highp vec3 v_normalCoord;

void main(void) {
    gl_Position = u_projectionMat4 * u_modelViewMat4 * a_position;
    v_normalCoord  = a_normalCoord;
}
#version 100

uniform samplerCube us2d_texture;
varying highp vec3 v_normalCoord;

void main(void) {
    gl_FragColor = textureCube(us2d_texture, v_normalCoord);
}

CubeMap 与 2D 的 Fragment 区别并不大,原理一样的;
CubeMap Vertex ,只要把 vec2 --> vec3 即可;
CubeMap Fragment , 只要把 sampler2D --> samplerCube , texture2D 函数改成 textureCube 即可;

(四)、Texture 正确的 “书写” 顺序

前提,假设基本的渲染管线已经配置完成了,这里只重点讲纹理相关的;

1、 绑定 Texture Coord 纹理坐标:

GLuint texCoordAttributeComCount = 2;

glEnableVertexAttribArray(texCoordAttributeIndex);
if ( texture2D ) {
    glVertexAttribPointer(texCoordAttributeIndex,
                          texCoordAttributeComCount,
                          GL_FLOAT, GL_FALSE,
                          sizeof(VYVertex),
                          (const GLvoid *) offsetof(VYVertex, texCoord));
 } else {
    texCoordAttributeComCount = 3;
    glVertexAttribPointer(texCoordAttributeIndex,
                          texCoordAttributeComCount,
                          GL_FLOAT, GL_FALSE,
                          sizeof(VYVertex),
                          (const GLvoid *) offsetof(VYVertex, normalCoord));
 }

【如果看不懂,请回去看看第一篇文章,里面有详细讲】

2、 请求 Texture 内存:

    GLuint texture = 0;
    glGenTextures(1, &texture);

    GLenum texMode = texture2D ? GL_TEXTURE_2D : GL_TEXTURE_CUBE_MAP;
    glBindTexture(texMode, texture);

glGenTextures(GLsizei n, GLuint* textures); 和 glGenBuffers 等的使用是一样的;它的意思就是,向 GPU 请求一块 Texture 内存;
glBindTexture (GLenum target, GLuint texture); 和其它的 glBind... 方法一样;它的意思是,告诉 GPU 请求一块 target 【只有 2D 和 CubeMap 两种】 类型的内存,只有当这个方法完成请求后,这块 Texture 内存才会生成【如果当前内存标识符指向的内存已经存在,则不会再创建,只会指向此处】;

3、 加载像素数据:

    glUseProgram(programObject);

    [self setTextureWithProgram:programObject 
                        texture:texture
                        texMode:texMode];

(1)一定要在 glUseProgram 函数后进行这个步骤,为什么?
因为 Fragment 使用的是 uniform samplerXXX 的数据,uniform 常量数据要在 glUseProgram 后再加载才有效,而且它的内存标识符【内存】要在 link Program 之后 OpenGL 才会分配;

(2)进入 setTextureWithProgram: texture: texMode:方法
先准备像素数据【pixelsDatas 或 ImageDatas】:
这里的是,Pixels 的数据,就是写死的数据

// 2 * 2 For Texture_2D
static const GLfloat tex2DPixelDatas[3*4] = {
    1.000, 1.000, 0.108,//[UIColor colorWithRed:1.000 green:1.000 blue:0.108 alpha:1.000]
    0.458, 1.000, 0.404,//[UIColor colorWithRed:0.458 green:1.000 blue:0.404 alpha:1.000]
    0.458, 1.000, 0.770,//[UIColor colorWithRed:0.458 green:1.000 blue:0.770 alpha:1.000]
    0.729, 0.350, 0.770,//[UIColor colorWithRed:0.729 green:0.350 blue:0.770 alpha:1.000]
};

// (2 * 2 * 6) For Texture_CubeMap
static const GLfloat texCubemapPixelDatas[6][3*4] = {
    1.000, 1.000, 0.108,//[UIColor colorWithRed:1.000 green:1.000 blue:0.108 alpha:1.000]
    0.458, 1.000, 0.404,//[UIColor colorWithRed:0.458 green:1.000 blue:0.404 alpha:1.000]
    0.458, 1.000, 0.770,//[UIColor colorWithRed:0.458 green:1.000 blue:0.770 alpha:1.000]
    0.729, 0.350, 0.770,//[UIColor colorWithRed:0.729 green:0.350 blue:0.770 alpha:1.000]

    0.145, 0.319, 0.308,//[UIColor colorWithRed:0.145 green:0.319 blue:0.308 alpha:1.000]
    0.732, 0.319, 0.308,//[UIColor colorWithRed:0.732 green:0.319 blue:0.308 alpha:1.000]
    0.732, 0.727, 0.308,//[UIColor colorWithRed:0.732 green:0.727 blue:0.308 alpha:1.000]
    0.732, 0.727, 0.889,//[UIColor colorWithRed:0.732 green:0.727 blue:0.889 alpha:1.000]

    0.633, 0.820, 0.058,//[UIColor colorWithRed:0.633 green:0.820 blue:0.058 alpha:1.000]
    0.936, 0.820, 0.994,//[UIColor colorWithRed:0.936 green:0.820 blue:0.994 alpha:1.000]
    0.017, 0.029, 0.994,//[UIColor colorWithRed:0.017 green:0.029 blue:0.994 alpha:1.000]
    0.000, 0.000, 0.000,//[UIColor colorWithWhite:0.000 alpha:1.000]

    0.593, 0.854, 0.000,//[UIColor colorWithRed:0.593 green:0.854 blue:0.000 alpha:1.000]
    0.593, 0.337, 0.000,//[UIColor colorWithRed:0.593 green:0.337 blue:0.000 alpha:1.000]
    1.000, 0.407, 0.709,//[UIColor colorWithRed:1.000 green:0.407 blue:0.709 alpha:1.000]
    0.337, 0.407, 0.709,//[UIColor colorWithRed:0.337 green:0.407 blue:0.709 alpha:1.000]

    0.337, 0.738, 0.709,//[UIColor colorWithRed:0.337 green:0.738 blue:0.709 alpha:1.000]
    0.337, 0.994, 0.709,//[UIColor colorWithRed:0.337 green:0.994 blue:0.709 alpha:1.000]
    0.186, 0.105, 0.290,//[UIColor colorWithRed:0.186 green:0.105 blue:0.290 alpha:1.000]
    0.633, 0.872, 0.500,//[UIColor colorWithRed:0.633 green:0.872 blue:0.500 alpha:1.000]

    0.290, 0.924, 0.680,//[UIColor colorWithRed:0.290 green:0.924 blue:0.680 alpha:1.000]
    0.290, 0.924, 0.174,//[UIColor colorWithRed:0.290 green:0.924 blue:0.174 alpha:1.000]
    0.982, 0.163, 0.174,//[UIColor colorWithRed:0.982 green:0.163 blue:0.174 alpha:1.000]
    0.628, 0.970, 0.878,//[UIColor colorWithRed:0.628 green:0.970 blue:0.878 alpha:1.000]
};

因为 Texture_2D 状态下,只有 {x, y} 平面的数据需要填充,所以这里就只有一个面的颜色数据;

而在 Texture_CubeMap 状态下,是 {x, y, z} 三维坐标,即六个面需要填充,所以就是6 * 1(1 = 2 * 2) = 6个面的颜色数据;

注:图片类型的数据要自己写转换方法,生成像素数据;当然也可以使用 GLKit 提供的 TextureLoder 类来加载图片像素数据;

(3)【核心】glTexImage2D得到纹理像素的方法,就是加载纹理像素到 GPU 的方法:

glTexImage2D
void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid* pixels)
target _指 如果是 2D ,就是 GL_Texture_2D,如果是 CubeMap 就是 GL_TEXTURE_CUBE_MAPXXX [+-x, +-y, +-z, 六个面]
level 指 mipmapping level 没有做 mipmapping 则为 0 ;如果做了,则为 0 ~ levelMax [这个 max 是由你自己图片数据决定的]
internalformat _指 像素数据的格式是什么 GLRGB 等等
width 指 一块像素的宽 [2D 下只有一块,cubemap 会有多块(六个面)]
height 指 一块像素的高
border _指 ES 下是 GLFALSE
format 指 与 internalformat 格式一致
type _指 像素数据存储的类型,如:GL_FLOAT, GL_UNSIGNEDBYTE
pixels 指 一块像素的内存首地址

a. 像素模式下的使用:

if (texMode == GL_TEXTURE_2D) {
    glTexImage2D(texMode, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT, tex2DPixelDatas);
} else {

//                glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[0]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[1]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[2]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[3]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[4]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[5]);

    GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X;
    for (NSUInteger i = 0; i < 6; i++) {
        glTexImage2D(target, 0, GL_RGB,
                     2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[i]);
        target++;
    }

}

上面在 GL_TEXTURE_2D 状态下的加载,只要理解了glTexImage2D函数参数的意思,也就会使用且明白了,这里就不再赘述了;

特别要注意的是在 GL_Texture_Cube_Map 状态下的使用,一定要六个面都进行像素数据加载;

#define GL_TEXTURE_CUBE_MAP_POSITIVE_X                   0x8515
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X                   0x8516
#define GL_TEXTURE_CUBE_MAP_POSITIVE_Y                   0x8517
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y                   0x8518
#define GL_TEXTURE_CUBE_MAP_POSITIVE_Z                   0x8519
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z                   0x851A

看看GL_TEXTURE_CUBE_MAP_POSITIVE_X它们的定义,因为定义是连续的,所以我们才可以用 for 循环来 “偷懒”;

b. 图片像素模式下的使用:

if (texMode == GL_TEXTURE_2D) {

    UIImage *img = // img;

    [self.loadTexture textureDataWithResizedCGImageBytes:img.CGImage completion:^(NSData *imageData, size_t newWidth, size_t newHeight) {
        glTexImage2D(texMode, 0, GL_RGBA,
                     (GLsizei)newWidth, (GLsizei)newHeight,
                     GL_FALSE, GL_RGBA, GL_UNSIGNED_BYTE,
                     imageData.bytes);
    }];

} else {

    NSArray<UIImage *> *imgs = // imgs;

    GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X;
    [self.loadTexture textureDatasWithResizedUIImages:imgs completion:^(NSArray<NSData *> *imageDatas, size_t newWidth, size_t newHeight) {
        [imageDatas enumerateObjectsUsingBlock:^(NSData * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            glTexImage2D((GLenum)(target + idx), 0, GL_RGBA,
                         (GLsizei)newWidth, (GLsizei)newHeight,
                         GL_FALSE, GL_RGBA, GL_UNSIGNED_BYTE,
                         obj.bytes);
        }];
    }];

}

这里的核心就是,self.loadTexture 的图片加载方法,这是自己写的加载方法,使用的技术是 Quartz Core ;具体的在下一节【三、知识扩充:图片加载】会讲到;

两者的使用并不会有什么区别,这只是两种像素数据提供的方式不同罢了

(4)指定滤波设置【下一篇会重点讲】 + 像素绑定 + 激活纹理

glTexParameteri(texMode, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(texMode, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

GLuint textureSourceLoc = glGetUniformLocation(programObject, "us2d_texture");
glUniform1i(textureSourceLoc, 0);

glEnable(texMode);
glActiveTexture(GL_TEXTURE0);
glBindTexture(texMode, texObj);

a. 设置滤波模式,函数就是glTexparameteri方法
原型:glTexParameteri (GLenum target, GLenum pname, GLint param);

ES 2 有四个这种滤波函数,下图的参数已经说得很明白了,我就不一一解释了:

MIN / MAG ?:

magnification【MAG】:放大的意思,指显示在屏幕上的一个像素是纹理像素放大后的结果;
【只有 x、y 方向都进行放大,才需要这个参数,也就是说它是可选的】

minification【MIN】: 缩小的意思,指显示在屏幕上的一个像素是一个纹理像素集缩小后的结果;
【一定要做的设置,如上述代码中的glTexParameteri(xxx, GL_TEXTURE_MIN_FILTER, xxx);
【MipMapping 发挥作用的地方就是在缩小的时候,OpenGL 会自动选择合适大小的像素数据】

如果纹理像素在 x、y 方向上是做同一个动作【拉伸或压缩】,则需要放大或缩小像素;如果纹理像素在 x、y 方向上是做不同的动作,则需要放大或者缩小,不确定【由 OpenGL 自己选择】;

WRAP_S / WRAP_T ? : 就是 x 或 y 方向填充覆盖的意思;

LINEAR / NEAREST ? :

前者是指启用线性滤波【就是平滑过渡】,后者是禁用线性滤波;

平滑过滤使用的技术——信号采样,先看看一维的信号采样:

意思就是,采样提供的纹理像素,在放大、缩小的时候,使相邻的像素进行 “一定程度的融合” 产生新的像素信息,使最终显示在屏幕在的图片更加平滑;上图【猴子】中的效果就是利用这项技术来的,对于二维、三维,就相应地做多次采样【二维,两次;三维,三次......】;

b. 像素绑定【就是告诉 GPU Shader 的像素数据在那】+ 激活纹理

GLuint textureSourceLoc = glGetUniformLocation(programObject, 
                                               "us2d_texture");
glUniform1i(textureSourceLoc, 0);

glEnable(texMode);
glActiveTexture(GL_TEXTURE0);

glBindTexture(texMode, texObj);

glUniform1i类函数,可以理解成绑定一块内存【像素块内存】,也可以理解成绑定一个内存空间【一般常量】;
函数原型:void glUniform1i(GLint location, GLint x)

glEnable函数,就是打开一些什么东西,这里是打开 GL_TEXTURE_XXX ,不写也行,这里和其它地方的默认一样, 0 这个位置的纹理就是打开的;【为了良好习惯,还是写吧】

glActiveTexture函数,名字已经告诉是激活纹理的意思,不用多说了;

重点:glUniform1i 的第二个参数是和 glActiveTexture 的第二个参数是对应的,前者使用的是 0,那么后者就是对应 GL_TEXTURE0 【0~31,共 32 个】,依此类推

为什么还要做glBindTexture(texMode, texObj);重新绑定像素内存,其实就是防止中途有什么地方把它给改了【如,bind 了其它的纹理】,所以是为了保险起见,就最好写上;但是因为这里很明显地,只有 layoutSubviews 函数【此渲染代码都是写在这个函数内运行的】会绑定它,而且都是同一个的,所以也可以不写;


三、知识扩充:图片加载

使用 Quartz Core 技术 加载图片数据,Bitmap Context :

本来它不属于 OpenGL 的内容,但是它本身也是图像处理的技术,包括 Core Image、 Accelerate 等图像处理的框架,如果可以,请尽量去了解或去掌握或去熟练。

核心代码:

#define kBitsPerComponent   8

#define kBytesPerPixels     4
#define kBytesPerRow(width)         ((width) * kBytesPerPixels)

- (NSData *)textureDataWithResizedCGImageBytes:(CGImageRef)cgImage
                                      widthPtr:(size_t *)widthPtr
                                     heightPtr:(size_t *)heightPtr {

    if (cgImage == nil) {
        NSLog(@"Error: CGImage 不能是 nil ! ");
        return [NSData data];
    }

    if (widthPtr == NULL || heightPtr == NULL) {
        NSLog(@"Error: 宽度或高度不能为空。");
        return [NSData data];
    }

    size_t originalWidth  = CGImageGetWidth(cgImage);
    size_t originalHeight = CGImageGetHeight(cgImage);

    // Calculate the width and height of the new texture buffer
    // The new texture buffer will have power of 2 dimensions.
    size_t width  = [self aspectSizeWithDataDimension:originalWidth];
    size_t height = [self aspectSizeWithDataDimension:originalHeight];

    // Allocate sufficient storage for RGBA pixel color data with
    // the power of 2 sizes specified
    NSMutableData *imageData =
    [NSMutableData dataWithLength:height * width * kBytesPerPixels]; // 4 bytes per RGBA pixel

    // Create a Core Graphics context that draws into the
    // allocated bytes
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef cgContext = CGBitmapContextCreate([imageData mutableBytes],
                                                   width, height,
                                                   kBitsPerComponent,
                                                   kBytesPerRow(width),
                                                   colorSpace,
                                                   kCGImageAlphaPremultipliedLast); // RGBA
    CGColorSpaceRelease(colorSpace);
    // Flip the Core Graphics Y-axis for future drawing
    CGContextTranslateCTM (cgContext, 0, height);
    CGContextScaleCTM (cgContext, 1.0, -1.0);
    // Draw the loaded image into the Core Graphics context
    // resizing as necessary
    CGContextDrawImage(cgContext, CGRectMake(0, 0, width, height), cgImage);
    CGContextRelease(cgContext);

    *widthPtr  = width;
    *heightPtr = height;

    return imageData;
}

主流程:
1、规格化图片尺寸,让其符合 (2^n, 2^m)[n,m 均为自然数]
为什么?
(1)因为 CGBitmapContextCreate支持的是 size_t ((long) unsigned int) 的【来个 0.25 个像素也是醉了】;
(2)而且 OpenGL ES 支持的最大像素尺寸也是有限制的,当前环境支持的最大值是 (4096, 4096),这个值由以下两个 xx_MAX_xx 得到【就在 aspectSizeWithDataDimension: 方法内】:

    GLint _2dTextureSize;
    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &_2dTextureSize);

    GLint cubeMapTextureSize;
    glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &cubeMapTextureSize);

glGetIntegerv函数是可以获取当前环境下所有的默认常量的方法;

2、确定图片像素最终输出的颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();,这个最容易出错,它的颜色格式要和你使用glTexImage2D函数指名的颜色格式要一致,不然不可能显示正常【如,你这里定义成 CYMK, 指名了 GL_RGB 那么肯定不对的】

3、确定最终像素的位深与位数
这里是明确用多少位来表示一个像素位【如:R 用 8 位表示】,一个像素由多少个成员组成【如:RGBA 就是 4 个】

4、创建上下文环境
Bitmap 图就是像素图,包含所有的像素信息,没有什么 jpg / png 容器什么的;
CGBitmapContextCreate函数的各个参数都很明显了,所以就不废话了;

5、变换像素的坐标空间
为什么?
Texture 纹理坐标空间的坐标原点在,左下角,而苹果设备显示的图形的坐标系的坐标原点在左上角,刚好是反的;

6、绘制生成最终的像素数据


谢谢看完,如果有描述不清或讲述错误的地方,请评论指出!!!

猜你喜欢

转载自blog.csdn.net/yangzm/article/details/79730834