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 模拟器 或真机上 的坐标是怎样构成的:

opengl es 好文_第1张图片

三维坐标 + 坐标值 演示图

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

二维就是长这样的了:

opengl es 好文_第2张图片

二维坐标( z = 0 )

2) 三条线?

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

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

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

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

opengl es 好文_第3张图片

1) 线元

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

Line

2) 三角图元

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

Triangle

扇面

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

3)填充色?

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


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

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

OpenGL ES 2 的渲染管线

opengl es 好文_第4张图片

图形管线(Graphics Pipeline)

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

opengl es 好文_第5张图片

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 (函数查询)在线

opengl es 好文_第6张图片

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

离线的函数 Card

opengl es 好文_第7张图片

红框处单击打开

opengl es 好文_第8张图片

红箭头处选择保存即可

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

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 好文_第9张图片

注:图片截自:《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 (片元着色器) :

片元着色器信号图:

opengl es 好文_第10张图片

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 : 保存了渲染过程中的所有数据和状态信息;
图示解释:

opengl es 好文_第11张图片

图片截自, 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 类;

opengl es 好文_第12张图片

GLKit APIs

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

4. GLSL ES (也称 ESSL) ?

简单流程图:

opengl es 好文_第13张图片

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,而是可以有多个,需要使用多少个,由具体项目决定。


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

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

opengl es 好文_第14张图片

e981fd1c1e0c35f7e91735fb473b2bec.gif

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

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

1. 配置环境:

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

opengl es 好文_第15张图片

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)drawable
target _只能填 GLRENDERBUFFER
drawable 只能是 CAEAGLLayer 对象

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

opengl es 好文_第16张图片

函数 描述
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 内存) 关联到刚才申请的内存区中;

opengl es 好文_第17张图片

注: 图片截自, 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 好文_第18张图片

注, 图片截自,《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 的原因:

opengl es 好文_第19张图片

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

opengl es 好文_第20张图片

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


面向对象的重新设计:

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

opengl es 好文_第21张图片

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

项目文件结构:

完整代码在 Github 上 DrawTriangle_OOP


第四步,练练手

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

1. 修改背景色

opengl es 好文_第22张图片

提示:glClear 函数

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

opengl es 好文_第23张图片

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

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

opengl es 好文_第24张图片

提示: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 把顶点数据绘制出来:

opengl es 好文_第25张图片

分布图

从图可以看出,这三个数据形成的其实是一个等边直角三角形,而在 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 章;

opengl es 好文_第26张图片

4x4 整体

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

1、4 x 4 方阵

opengl es 好文_第27张图片

4X4 方阵

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

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

opengl es 好文_第28张图片

线性变换

  • 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} 的旋转:

opengl es 好文_第29张图片

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

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

3、平移

opengl es 好文_第30张图片

平移

直接把平移向量,按分量 {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

opengl es 好文_第31张图片

w

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

  • 投影

投影

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

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


三、OpenGL 下的三维变换

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

1、OpenGL 的坐标系

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

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

  • 旋转的正方向

opengl es 好文_第32张图片

右手坐标

图片来源于,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、单次三维变换与多次三维变换问题

opengl es 好文_第33张图片

多次变换

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

opengl es 好文_第34张图片

4x4 整体 OpenGL

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

  • 平移部分的内容:

opengl es 好文_第35张图片

4X4 方阵 OpenGL

平移 OpenGL

* 矩阵平移公式  

opengl es 好文_第36张图片

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

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

  • 投影(就是零)

投影 OpenGL

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

opengl es 好文_第37张图片

Linaer Transforms

  • 单次变换(原点重叠)

Identity

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

  • 缩放

opengl es 好文_第38张图片

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 个单位。

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

opengl es 好文_第39张图片

Translation&Scale

opengl es 好文_第40张图片

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 es 好文_第41张图片

详细过程

作为了解即可
以上图片均来源于,《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);
    };

它们都定义在:

opengl es 好文_第42张图片

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 变量进行的赋值函数

opengl es 好文_第43张图片

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) 目标

opengl es 好文_第44张图片

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: 动态计算点

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

opengl es 好文_第45张图片

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

下面是具体的数据:

// 三角形
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)

opengl es 好文_第46张图片

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);
}
  • 程序运行结果

opengl es 好文_第47张图片

Rect-Star

5. 绘制曲线、圆形

BAISER

  • 图形分析

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

文件

opengl es 好文_第48张图片

应用

  • 开始写代码

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

* 增加 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

  • 完整的线元工程, Github:DrawGeometries_Lines

二、图元绘制之三角形

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

opengl es 好文_第49张图片

图 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. 工程目录

opengl es 好文_第50张图片

工程目录

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

1. 绘制基本几何图形

TRIANGLE STRIP FAN

  • 图形分析

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

opengl es 好文_第51张图片

TRIANGLE LINESON

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

opengl es 好文_第52张图片

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

  • 这里容易出问题的是最后一个图形(五角星形),三角形与点的关系: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 去学习反而更有趣,不过这里要有纹理的知识,所以先行不讲了;


四、练练手

opengl es 好文_第53张图片

Challenges

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

0. 工程目录

opengl es 好文_第54张图片

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

1. 绘制一棵卡通树

Tree

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

2. 绘制一张卡片

opengl es 好文_第55张图片

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)

opengl es 好文_第56张图片

摄像机 带注解

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

视景体

图中紫色区域为视场角

fov & zoom

从而引出,视场缩放为:

opengl es 好文_第57张图片

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 好文_第58张图片

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. 设置摄像机的位置、方向 --> (视变换) gluLookAt (ES 没有这个函数),使要渲染的模型位于摄像机可视区域中;【完成图中 1 和 2】
2. 选择摄像机的焦距去适应整个可视区域 --> (投影变换) glFrustum(视景体的六个面)、gluPerspective(透视) 、glOrtho(正交)( ES 没有这三个函数) 【完成图中 3】
3. 设置图形的视图区域,对于 3D 图形还可以设置 depth- range --> 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);

opengl es 好文_第59张图片

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);

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

  • 透视投影

透视投影

对应的投影公式 :

opengl es 好文_第60张图片

完整的透视投影公式

使用 GLKit 提供的函数:

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

Orthographic projection

对应的投影公式 :

opengl es 好文_第61张图片

完整的正交投影公式

/*
 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 并使其工作的步骤:

opengl es 好文_第62张图片

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 :

opengl es 好文_第63张图片

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

opengl es 好文_第64张图片

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 个顶点数据的下标;第二个因素是时钟方向;

现在看看时钟方向:

opengl es 好文_第65张图片

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

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

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

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

方向只有两个:

opengl es 好文_第66张图片

资源绑定
这里主要是 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. 深度测试与绘制

opengl es 好文_第67张图片

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)]]; 的运行结果;

opengl es 好文_第68张图片

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 类的实时更新问题;

// 
- (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<

你可能感兴趣的:(ios)