opengl es 好文

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

目錄結構:

第一步,明確要幹嘛

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

第二步,怎麼去畫(純理論)

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

第三步,怎麼去畫(實戰)

 a.OpenGL ES 2 的渲染流程 細化
    1) 配置環境
    2) 初始化數據
    3) 配置 OpenGL ES Shader
    4) 渲染繪製
 b.流程代碼化
  一、配置渲染環境
    1) 配置渲染窗口 [ 繼承自 UIView ]
    2) 配置渲染上下文
    3) 配置幀渲染
    4) 配置渲染緩存
    5) 幀緩存裝載渲染緩存的內容
    6) 渲染上下文綁定渲染窗口(圖層)
  二、修改背景色
  三、 初始化數據
  四、 配置 OpenGL ES Shader
    1) 編寫 Vertex Shader Code 文件
    2) 編寫 Fragment Shader Code 文件
    3) 配置 Vertex Shader
    4) 配置 Fragment Shader
    5) 創建 Shader Program
    6) 裝載 Vertex ShaderFragment Shader
    7) 鏈接 Shader Program
  五、渲染繪製
    1) 清空舊渲染緩存
    2) 設置渲染窗口
    3) 使用 Shder Program
    4) **關聯數據**
    5) 繪製圖形
 c.面向對象的重新設計

第四步,練練手

 a.修改背景色
 b.修改三角形的填充色
 c.修改三角形的三個頂點的顏色(填充色)


第一步,明確要幹嘛

1. 目標:

使用 OpenGL ES 2.0 在 iOS 模擬器中繪製一個三角形。

2. 效果:

3. 分析圖形:

1) 背景顏色是藍色
--> 修改背景顏色

2) 直角三角形
--> 繪製三角形

4. 繪製三角形?三角形由什麼組成?

--> 三個端點 + 三條線 + 中間的填充色,即三個點連成線形成一個三角面。

1). 三個什麼端點(屏幕座標點)?
要回答這個問題要先了解 OpenGL ES 的座標系在屏幕上是怎樣分佈的:

OpenGL ES 的座標系 {x, y, z}

注:圖片截自 《Learning OpenGL ES For iOS》一書

a. 通過圖片的三維座標系可以知道:

- 它是一個三維座標系 {x, y, z}
- 三維座標中心在正方體的幾何中心 {0, 0, 0}
- 整個座標系是 [0, 1] 的點,也就是說 OpenGL 中只支持 0 ~ 1 的點

注意,這裏所講的 0 和 1 ,最好理解成 0 --> 無限小, 1 --> 無限大 ,它並不是指 0 個單位的長度,或 1 個單位的長度。

b. 再來看看我們繪製的三角形,在 iOS 模擬器 或真機上 的座標是怎樣構成的:

三維座標 + 座標值 演示圖

注:圖片通過 CINEMA4D (c4d)三維軟件繪製

二維就是長這樣的了:

二維座標( z = 0 )

2) 三條線?

a. 連接三個端點形成封閉的三角面,那麼 OpenGL ES 能不能直接繪製三角形 ? --> 答案是能。

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

c. 那 OpenGL 能直接繪製什麼?
--> 答案是:點精靈、線、三角形,它們統稱爲 圖元(Primitive)。

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

1) 線元

Line Strip , 指首尾相接的線段,第一條線和最後一條線沒有連接在一起;
Line Loops, 指首尾相接的線段,第一條線和最後一條線連接在一起,即閉合的曲線;

Line

2) 三角圖元

Triangle Strip, 指條帶,相互連接的三角形
Triangle Fan, 指扇面,相互連接的三角形

Triangle

扇面

3) 點精靈 【主要應用在 紋理 方面】

3)填充色?

就是指 RGBA 的顏色值;( ^_^ 感覺好廢但還是要說)


第二步,怎麼去畫(純理論)

怎麼去畫,就是通過多少個步驟完成一個完整的繪製渲染流程,當然這裏指 OpenGL ES 2 的渲染管線流程)

OpenGL ES 2 的渲染管線

圖形管線(Graphics Pipeline)

因爲這裏是 iOS 端的圖,所以重新繪製了一下:

OpenGL ES 2 渲染流程圖

注:此圖根據 《OpenGL ES 2.0 programming guide》的 Graphics Pipeline 和 Diney Bomfim [All about OpenGL ES 2.x - (part 2/3)] 的管線圖進行重新繪製。【繪製的軟件爲:Visio 2016】

1. 簡述繪製流程的每一個單元【至左向右】

OpenGL ES 2.0 API :

iOS 環境下

gltypes.h 是包含了 OpenGL ES 2.0 的基本數據類型的定義;
glext.h 是包含各種宏定義,以及矩陣運算等常用的函數;
gl.h 是 OpenGL ES 2.0 所有的核心函數(命令);

擴展
OpenGL ES 2.0 Reference (函數查詢)在線

左邊選擇要查詢的函數即可

離線的函數 Card

紅框處單擊打開

紅箭頭處選擇保存即可

本人推薦使用離線的卡,不受網絡影響,而且一目瞭然。配合官方的編程指南使用就最佳了。

2. Vertex Arrays / Buffer Objects :

1) Vertex Arrays Objects (簡稱:VAOs),頂點數組對象,就是一個數組,包含頂點座標、顏色值、紋理座標等數據;通過 CPU 內存關聯到 GPU 的內存區被 GPU 所使用;

【官方解釋:Vertex data may be sourced from arrays that are stored in application memory (via a pointer) or faster GPU memory (in a buffer object).(意指:頂點數組保存在程序內存或快速 GPU 內存中,前者通過數組指針訪問數據,後者直接通過 Buffer Objects 訪問。【就是指 VAOs 或 VBOs 方式訪問】)】

繪製的三角形的數組(三個頂(端)點座標)如下圖:

頂點數組

VFVertex

這是 C 語言的知識,應該不難理解。

2) Vertex Buffer Objects , (簡稱:VBOs [ Vertex Buffer Objects]),緩存對象,就是持有頂點數組數據或數據下標的對象【並不是指面向對象裏面的對象哦,其實一塊 GPU 內存塊】。

【官方解釋:Buffer objects hold vertex array data or indices in high-performance server memory. (意指:VBOs 是持有保存在 GPU 快速內存區的頂點數據或頂點數據下標的緩存對象。)】

a. 爲什麼是 server ?
--> 答,OpenGL 是基於 CS 模式的設計而成,客戶端操作就相當於我們寫的 OpenGL API ( OpenGL commands ) 的各種操作,服務器就是圖形處理相關的硬件。( ES 當然也是這意思咯。)

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

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

3. Vertex Shader (頂點着色器) :

處理頂點相關的數據,包括頂點在屏幕的位置(矩陣變換),頂點處的光照計算,紋理座標等。

頂點着色器的信號圖:

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

1) 輸入信號:Attributes、Uniforms、Samplers (optional)

a. Attributes : 屬性的意思,指每一個頂點數據;

b. Uniforms :

b-1. 統一的意思 , 是一個只讀全局常量,存儲在程序的常量區;
b-2. 當 Vertex Shader 和 Fragment Shader 定義了同名同類型的 Uniform 常量時,此時的 Uniform 常量就變成了全局常量(指向同一塊內存區的常量);

c. Samplers (可選的) : 
是一個特殊的 Uniforms 保存的是 Texteures(紋理) 數據;

2) 輸出信號: Varying

Varying : 
a. 它是 Vertex Shader 與 Fragment Shader 的接口,是爲了解決功能性問題(兩個 Shader 的信息交互);

b. 儲存 Vertex Shader 的輸出信息;

c. Vertex Shader 與 Fragment Shader 中必須要有必須要同名同類型的 Varying 變量,不然會編譯錯誤;(因爲它是兩個 Shader 的信息接口啊,不一樣還接什麼口啊。)

3) 交互信息: Temporary Variables

Temporary Variables :
a. 指臨時變量;
b. 儲存 Shader 處理過程中的中間值用的;
c. 聲明在 Funtions(函數) 或 Variable(變量) 內部;

4) 輸出的內建變量:gl_Position、gl_FrontFacing、gl_PointSize

a. gl_Position (highp vec4 變量) :
就是 Vertex Position,Vertex Shader 的輸出值,而且是必須要賦值的變量;只有在 Vertex Shader 中使用纔會有效

注:highp vec4, highp (high precision) 高精度的意思,是精度限定符;vec4 ( Floating Point Vector ) 浮點向量 , OpenGL ES 的數據類型。

b. gl_PointSize (mediump float 變量) :
告訴 Vertex Shader 柵格化點的尺寸(pixels, 像素化),想要改變繪製點的大小就是要用這個變量 只有在 Vertex Shader 中使用纔會有效

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

c. gl_FrontFacing (bool 變量) : 
改變渲染物體的 Front Facing 和 Back Facing , 是用於處理物體光照問題的變量,雙面光照(3D 物體裏外光照)問題的時候纔會使用的變量,只能在 Vertex Shader 中進行設置, Fragment Shader 是隻讀的

4. Primitive Assembly (圖元裝配) :

1) 第一步,把 Vertex Shader 處理後的頂點數據組織成 OpenGL ES 可以直接渲染的基本圖元:點、線、三角形;

2) 第二步,裁剪 (Clipping) ,只保留在渲染區域(視錐體,視覺區域)內的圖元;

3) 第二步,剔除 (Culling),可通過編程決定剔除前面、後面、還是全部;

注:
視錐體,實際上是一個三維錐體包含的空間區域,由攝影機和物體的捕捉關係形成;

視錐體

圖片來源 《透視投影詳解》一文

5. Rasterization (光柵化) :

光柵化的信號圖:

作用是,將基本圖元(點、線、三角形)轉換成二維的片元(Fragment, 包含二維座標、顏色值、紋理座標等等屬性), 像素化基本圖元使其可以在屏幕上進行繪製(顯示)。

6. Texture Memory (紋理內存) :

Texture 就是指保存了圖片(位圖)的所有顏色的緩存;Texture Memory 就是圖片的顏色(像素)內存;每一個嵌入式系統對 Texture Memory 的大小都是有限制的;

1) 完整的 iOS 渲染繪製管線圖中,向上指向 Vertex Shader 的虛線,意指 Texture Coordinate (紋理座標)信息是通過程序提供給它的;

2) 完整的 iOS 渲染繪製管線圖中,指向 Fragment Shader 的實線,因爲 Fragment Shader 處理的是光柵化後的數據,即像素數據,而 Texture 本身就是像素數據,所以 Texture Memory 可以直接當成 Fragment Shader 的輸入;

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

片元着色器信號圖:

1) 輸入信號: Varying、Uniforms、Samples
與 Vertex Shader 的輸入是同一個意思,具體請查看 Vertex Shader 處的解釋~~~;

2) 輸入的內建變量:gl_FragCoord、gl_FrontFacing、gl_PointCoord

a. gl_FragCoord (mediump vec4 只讀變量) :
是保存窗口相對座標的 {x, y, z, 1/w} 的變量,z 表示深度 (will be used for the fragment's depth), w 表示旋轉;

b. gl_PointCoord (mediump int 只讀變量) : 
是包含了當前片元原始點位置的二維座標;點的範圍是 [0, 1] ;

c. gl_FrontFacing 
請查看 Vertex Shader 處的解釋;

3) 輸出信號 (內建變量) : gl_FragColor、gl_FragData (圖上沒寫)

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

b. gl_FragData (mediump vec4) : 
是一個數組,片元顏色集;

注:兩個輸出信號只能同時存在一個,就是 寫了 gl_FragColor 就不要寫 gl_FragData , 反之亦然;【If a shader statically assigns a value to gl_FragColor, it may not assign a value to any element of gl_FragData. If a shader statically writes a value to any element of gl_FragData, it may not assign a value to gl_FragColor. That is, a shader may assign values to either gl_FragColor or gl_FragData, but not both.

-

補充知識 (For Shader)

8. Per-Fragment Operations :

信號圖:

1) Pixel ownership test (像素歸屬測試) :
判斷像素在 Framebuffer 中的位置是不是爲當前 OpenGL ES Context 所有,即測試某個像素是否屬於當前的 Context 或是否被展示(是否被用戶可見);

2) Scissor Test (裁剪測試) :
判斷像素是否在由 glScissor* 定義的裁剪區域內,不在該剪裁區域內的像素就會被丟棄掉;

3) Stencil Test (模版測試):
將模版緩存中的值與一個參考值進行比較,從而進行相應的處理;

4) Depth Test (深度測試) :
比較下一個片段與幀緩衝區中的片段的深度,從而決定哪一個像素在前面,哪一個像素被遮擋;

5) Blending (混合) :
將片段的顏色和幀緩存中已有的顏色值進行混合,並將混合所得的新值寫入幀緩存 (FrameBuffer) ;

6) Dithering (抖動) :
使用有限的色彩讓你看到比實際圖象更爲豐富的色彩顯示方式,以緩解表示顏色的值的精度不夠大而導致顏色劇變的問題。

9. Render Buffer & Frame Buffer:

關係圖:

1) Render Buffer (渲染緩存) :

a. 簡稱 RBO , Render Buffer Object;
b. 是由程序(Application)分配的 2D 圖片緩存;
c. Render Buffer 可以分配和存儲顏色(color)、深度(depth)、模版(stectil)值,也可以把這三種值裝載到 Frame Buffer 裏面;

2) Frame Buffer (幀緩存) :

a. 簡稱 FBO , Frame Buffer Object;
b. 是顏色、深度、模板緩存裝載在 FBO 上所有裝載點的合集;
c. 描述顏色、深度、模板的大小和類型的屬性狀態;
d. 描述 Texture 名稱的屬性狀態;
e. 描述裝載在 FBO 上的 Render Buffer Objects (渲染緩存對象) 的屬性狀態;

擴充知識(FBO):

FBO API 支持的操作如下:
1) 只能通過 OpenGL ES 命令 (API) 創建 FBO 對象;
2) 使用一個 EGL Context 去創建和使用多個 FBO , 即不要爲每一個 FBO 對象創建一個正在渲染的上下文(rendering context);
3) 創建 off-screen 的顏色、深度、模板渲染緩存和紋理需要裝載在 FBO 上;
4) 通過多個 FBO 來共享顏色、深度、模板緩存;
5) 正確地裝載紋理的顏色或深度到 FBO 中,避免複製操作;

10. EAGL API :

官方的是 EGL API 與平臺無關,因爲它本身是可以進行平臺定製的,所以 iOS 下就被 Apple 定製成了 EAGL API 。

EAGL.h : 裏面的核心類是 EAGLContext , 上下文環境;
EAGLDrawable.h : 用於渲染繪製輸出的 EAGLContext 分類;

注:除了上面的兩個外,還有一個類 CAEAGLLayer ,它就是 iOS 端的渲染窗口寄宿層;

【 看這裏:
1) EGL API 設計出來的目的就是爲了在 OpenGL ES 2 能在窗口系統 (屏幕 ,iOS 是 CAEAGLLayer 類爲寄宿層的 View)進行渲染繪製;

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

a. 可以進行顯示的設備( iOS 下當然是手機或模擬器 )
b. 創建渲染面(rendering surface), 設備的屏幕 (on-screen) 或 像素緩存 ( pixel Buffer ) ( off-screen )

注: pixel Buffer , 這種 buffer 是不能直接顯示的,只能成爲渲染面或通過其它 API 分享出去,如: pbuffers 經常被用於 Texture 的 maps , 因爲 Texture 本身也是像素嘛;

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

注:

OpenGL ES Context : 保存了渲染過程中的所有數據和狀態信息;
圖示解釋:

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

OpenGL ES Shader Language 簡述

流程圖中出現的 Vertex Shader 與 Fragment Shader 都是要使用 GLSL ES 語言來進行編程操作的

1. GLSL ES 版本:

OpenGL ES 2.0 對應的 GLSL ES 版本是 1.0,版本編號是 100;

2. iOS Shader 類:

iOS 環境下 GLKit 提供了一個簡單的 Shader 類——GLKBaseEffect 類;

GLKit APIs

3. OpenGL 本身是 C Base 的語言,可以適應多個平臺,而在 iOS 下的封裝就是 GLKit ;

4. GLSL ES (也稱 ESSL) ?

簡單流程圖:

OpenGL ES Shader 流程圖

1) 編寫 Shader 代碼:

a. 同時編寫 Vertex Code 和 Fragment Code
b. 建議以文件的形式來編寫,不建議使用 "......" 字符串的形式進行編寫,前者會有編譯器的提示作爲輔助防止一定的輸入錯誤,但後者不會,爲了不必要的麻煩,使用前者;
c. 文件的名稱使用應該要形如 xxxVertexShader.glsl / xxxFragmentShader.glsl;

注:(其實文件名和後綴都可以隨意的,但是你在編程的時候爲了可讀性,建議這樣寫,也是爲了防止不必要的麻煩);【 Xcode 只會在 glsl 的文件後綴的文件進行提示,當然有時候會抽一風也是正常的 】

d. 要掌握的知識點是 Shader 的 Data Typies(數據類型,如:GLfloat 等)、Build-in Variables(內置變量,如:attribute 等)、流程控制語句(if、while 等);

2) 除編寫 Shader Code 外,其它的流程都由一個對應的 GLSL ES 的 API (函數) 進行相應的操作;

注:此處只是做了一個 Program 的圖,不是只能有一個 Program,而是可以有多個,需要使用多少個,由具體項目決定。


第三步,怎麼去畫(實戰)

以本文的小三角爲例,開始浪吧~~~!

e981fd1c1e0c35f7e91735fb473b2bec.gif

OpenGL ES 2 的渲染流程 實際繪製環境,流程細化

OpenGL ES 2 iOS 渲染邏輯流程圖. png

1. 配置環境:

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

EAGL Class

2) 核心操作:

a. CAEAGLLayer 替換默認的 CALayer,配置繪製屬性;
b. EAGLContext,即 Render Context ,設置成 OpenGL ES 2 API 環境,並使其成爲當前活躍的上下文環境;
c. Frame Buffers / Render Buffer 的創建和使用,以及內容綁定;
d. EAGLContext 綁定渲染的窗口 (on-screen),CAEAGLLayer 

擴展:
CAEAGLLayer 
1) 繼承鏈:

CALayer 有的,當然 CAEAGLLayer 也有;

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

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

3) 使用注意:
a. 修改 opaque 屬性爲 YES (CAEAGLLayer.opaque = YES;);
b. 不要修改 Transform ;
c. 當橫豎屏切換的時候,不要去修改 CAEAGLLayer 的 Transform 而進行 Rotate, 而是要通過 OpenGL / OpenGL ES 來 Rotate 要渲染的內容。

EAGLContext 
是管理 OpenGL ES 渲染上下文(包含,信息的狀態、openGL ES 的命令(API)、OpenGL ES 需要繪製的資源)的對象,要使用 OpenGL ES 的 API (命令) 就要使該 Context 成爲當前活躍的渲染上下文。(原文: An EAGLContext object manages an OpenGL ES rendering context—the state information, commands, and resources needed to draw using OpenGL ES. To execute OpenGL ES commands, you need a current rendering context.)

2. 初始化數據

這裏主要是考慮是否使用 VBOs ,由於移動端對效率有所要求,所以一般採用 VBOs 快速緩存;

3. 配置 OpenGL ES Shader

1) 這裏的核心工作是 Shader Code ,即學習 GLSL ES 語言;
2) iOS 端採用 glsl 後綴的文件來編寫代碼;

4. 渲染繪製

1) 這裏要注意的是 清空舊緩存、設置窗口,雖然只是一句代碼的問題,但還是很重要的;
2) 核心是學習 glDraw* 繪製 API ;


流程代碼化

1. 配置渲染環境

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

a. 重寫 layerClass 類方法

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

b. 配置 drawableProperties ,就是繪製的屬性

- (void)commit {

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

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

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

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

}

2) 配置渲染上下文

// a. 定義 EAGLContext
@interface VFGLTriangleView ()
@property (assign, nonatomic) VertexDataMode vertexMode;
@property (strong, nonatomic) EAGLContext *context;
@end
// b. 使用 OpenGL ES 2 的 API,並使該 Context ,成爲當前活躍的 Context
- (void)settingContext {

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

}

3) 配置幀渲染

- (GLuint)createFrameBuffer {

    GLuint ID;

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

    return ID;

}
函數描述
glGenFramebuffers創建 幀緩存對象
glBindFramebuffer使用 幀緩存對象
glGenFramebuffers
void glGenFramebuffers (GLsizei n, GLuint * framebuffers)
n 指返回多少個 Frame Buffer 對象
framebuffers 指 Frame Buffer 對象的標識符的內存地址
glBindFramebuffer
void glBindFramebuffer (GLenum target, GLuint framebuffer)
target _只能填 GLFRAMEBUFFER
framebuffer 指 Frame Buffer 對象的標識符

4) 配置渲染緩存

- (GLuint)createRenderBuffer {

    GLuint ID;

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

    return ID;

}
函數描述
glGenRenderbuffers創建 渲染緩存對象
glBindRenderbuffer使用 渲染緩存對象
glGenRenderbuffers
void glGenRenderbuffers(GLsizei n, GLuint *renderbuffers)
n 指返回多少個 Render Buffer 對象
renderbuffers 指 Render Buffer 對象的標識符的內存地址
glBindRenderbuffer
void glBindRenderbuffer(GLenum target, GLuint renderbuffer)
target _只能填 GLRENDERBUFFER
renderbuffers 指 Render Buffer 對象的標識符

5) 幀緩存裝載渲染緩存的內容

- (void)attachRenderBufferToFrameBufferWithRenderID:(GLuint)renderBufferID {

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBufferID);

}
函數描述
glFramebufferRenderbuffer裝載 渲染緩存的內容到幀緩存對象中
glFramebufferRenderbuffer
void glFramebufferRenderbuffer (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer)
target _只能填 GLFRAMEBUFFER
attachment _只能是三個中的一個:GL_COLOR_ATTACHMENT0 (顏色緩存)、GL_DEPTH_ATTACHMENT ( 深度緩存 )、GL_STENCILATTACHMENT (模板緩存)
renderbuffertarget _只能填 GLRENDERBUFFER
renderbuffer 指 Render Buffer 對象的標識符,而且當前的 Render Buffer 對象一定要是可用的

6) 渲染上下文綁定渲染窗口(圖層)

- (void)bindDrawableObjectToRenderBuffer {

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

}
函數描述
renderbufferStorage: fromDrawable:關聯 當前渲染上下文和渲染窗口
renderbufferStorage: fromDrawable:
- (BOOL)renderbufferStorage:(NSUInteger)target fromDrawable:(id<EAGLDrawable>)drawable
target _只能填 GLRENDERBUFFER
drawable 只能是 CAEAGLLayer 對象

函數解釋:
1) 爲了使創建的 Render Buffer 的內容可以顯示在屏幕上,要使用這個函數綁定 Render Buffer 而且分配共享內存;
2) 要顯示 Render Buffer 的內容, 就要使用 presentRenderbuffer:來顯示內容;
3) 這個函數的功能等同於 OpenGL ES 中的它【內容太多,簡書不好排版】

函數描述
glRenderbufferStorage保存渲染緩存內容
glRenderbufferStorage
void glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height)
target _只能填 GLRENDERBUFFER
internalformat 分三種 color render buffer、 depth render buffer、stencil render buffer
width _像素單位,大小必須 <= GL_MAX_RENDERBUFFERSIZE
height _像素單位,大小必須 <= GL_MAX_RENDERBUFFERSIZE
internalformat
color render buffer [01]GL_RGB565, GL_RGBA4, GL_RGB5_A1,
color render buffer [02]GL_RGB8_OES, GL_RGBA8_OES
depth render buffer [01]GL_DEPTH_COMPONENT16,
depth render buffer [02]GL_DEPTH_COMPONENT24_OES, GL_DEPTH_COMPONENT32_OE
stencil render bufferGL_STENCIL_INDEX8, GL_STENCIL_INDEX4_OES, GL_STENCIL_INDEX1_OE

2. 修改背景色

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

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

    RGBAColor color = {

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

    };

    return color;

}

- (void)setRenderBackgroundColor:(RGBAColor)color {

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

}
函數描述
glClearColor清空 Render Buffer 的 Color Render Buffer 爲 RGBA 顏色
glClearColor
void glClearColor (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
red 指 [0, 1] 的紅色值
green 指 [0, 1] 的綠色值
blue 指 [0, 1] 的藍色值
alpha 指 [0, 1] 的透明度值

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

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

3. 初始化數據

如果要使用 VBOs 最好在這裏創建 VBOs 對象並綁定頂點數據,當然直接在關聯數據一步做也沒問題;

#define VertexBufferMemoryBlock    (1)

- (GLuint)createVBO {

    GLuint vertexBufferID;
    glGenBuffers(VertexBufferMemoryBlock, &vertexBufferID);

    return vertexBufferID;

}

#define PositionCoordinateCount      (3)

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

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

- (void)bindVertexDatasWithVertexBufferID:(GLuint)vertexBufferID {

    glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);

    // 創建 資源 ( context )
    glBufferData(GL_ARRAY_BUFFER,   // 緩存塊 類型
                 sizeof(vertices),  // 創建的 緩存塊 尺寸
                 vertices,          // 要綁定的頂點數據
                 GL_STATIC_DRAW);   // 緩存塊 用途

}
函數描述
glGenBuffers申請 VBOs 對象內存
glBindBuffer綁定 VBOs 對象
glBufferData關聯頂點數據,並創建內存
glGenBuffers
void glGenBuffers (GLsizei n, GLuint * buffers)
n 指返回多少個 VBO
buffers 指 VBO 的標識符內存地址
glBindBuffer
void glBindBuffer (GLenum target, GLuint buffer)
target _可以使用 GL_ARRAY_BUFFER 或 GL_ELEMENT_ARRAYBUFFER
buffer 指 VBO 的標識符
glBufferData
void glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage)
target _可以使用 GL_ARRAY_BUFFER 或 GL_ELEMENT_ARRAYBUFFER
size 字節單位,數據在內存中的大小(sizeof(...))
data 頂點數據的內存指針
usage 告訴程序怎麼去使用這些頂點數據
usage
GL_STATIC_DRAW程序只指定一次內存對象的數據(頂點數據),而且數據會被多次(非常頻繁地)用於繪製圖元。
GL_DYNAMIC_DRAW程序不斷地指定內存對象的數據(頂點數據),而且數據會被多次(非常頻繁地)用於繪製圖元。
GL_STREAM_DRAW程序只指定一次內存對象的數據(頂點數據),而且數據會被數次(不確定幾次)用於繪製圖元。

glGenBuffers 、glBindBuffer、glBufferData 都幹了什麼?

1) glGenBuffers 會在 OpenGL ES Context (GPU) 裏面,申請一塊指定大小的內存區;

2) glBindBuffer 會把剛纔申請的那一塊內存聲明爲 GL_ARRAY_BUFFER ,就是以什麼類型的內存來使用;

3) glBufferData 把存放在程序內存的頂點數據 (CPU 內存) 關聯到剛纔申請的內存區中;

注: 圖片截自, RW. Beginning. OpenGL ES.and.GLKit Tutorials 教程;圖片中的 “~~ 3) 拷貝頂點數據~~ ” 更正爲 “ 3) 關聯頂點數據 ”, 因爲從 CPU 拷貝數據到 GPU 是在 OpenGL ES 觸發繪製方法(後面會進到)的時候纔會進行;

4. 配置 OpenGL ES Shader

1) 編寫 Vertex Shader Code 文件

a. 這是文件形式的,建議使用這種, Xcode 會進行關鍵字提示

#version 100

attribute vec4 v_Position;

void main(void) {
    gl_Position = v_Position;
}

a 對應的圖片

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

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

b 對應的圖片

非常明顯地看出,a 不管編寫和閱讀都很輕鬆,而 b 就是一堆紅,不知道是什麼鬼,看久了眼睛會很累;

 代碼解釋:
a. #version 100 ,首先 OpenGL ES 2 使用的 GLSL ES 版本是 100, 這個沒什麼好解釋的。《OpenGL ES 2 programming Guide》有提及

同時也說明了,我們編寫 GLSL Code 的時候,要使用 《OpenGL ES Shading Language》的語言版本;

b. attribute vec4 v_Position;
b-1. attribute 存儲類型限定符,表示鏈接,鏈接 OpenGL ES 的每一個頂點數據到頂點着色器(一個一個地);

注:
1) attribute 只能定義 float, vec2, vec3, vec4, mat2, mat3,mat4 這幾種類型的變量,不能是結構體或數組;
2) 只能用在頂點着色器中,不能在片元着色器中使用,不然會編譯錯誤;

補充:其它的存儲類型限定符

限定符描述
none(默認) 表示本地的可讀寫的內存  輸入的參數
const表示編譯期固定的內容  只讀的函數參數
attribute表示鏈接,鏈接 OpenGL ES 的每一個頂點數據到頂點着色器(一個一個地)
uniform表示一旦正在被處理的時候就不能改變的變量,鏈接程序、OpenGL ES 、着色器的變量
varying表示鏈接頂點着色器和片元着色器的內部數據

b-2. [ vec4 ],基本的數據類型,直接上圖

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

c. gl_Position 內建變量
因爲頂點數據裏面

只是用到了 Position 頂點數據;

2) 編寫 Fragment Shader Code 文件

a. 文件形式

#version 100

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

b. 字符串形式

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

3) 配置 Vertex Shader

- (GLuint)createShaderWithType:(GLenum)type {

    GLuint shaderID = glCreateShader(type);

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

    return shaderID;
}

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

    glCompileShader(shaderID);

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

}
函數描述
glCreateShader創建一個着色器對象
glShaderSource關聯頂點、片元着色器的代碼
glCompileShader編譯着色器代碼
glGetShaderiv獲取着色器對象的相關信息
glGetShaderInfoLog獲取着色器的打印消息
glCreateShader
GLuint glCreateShader (GLenum type)
type _只能是 GL_VERTEX_SHADER、GL_FRAGMENTSHADER 中的一個
return GLuint 返回着色器的內存標識符
glShaderSource
void glShaderSource (GLuint shader, GLsizei count, const GLchar * const_ _string, const GLint length)
shader 着色器的內存標識符
count 有多少塊着色代碼字符串資源
string 着色代碼字符串首指針
length 着色代碼字符串的長度
glCompileShader
void glCompileShader(GLuint shader)
shader 着色器的內存標識符
glGetShaderiv
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params)
shader 着色器的內存標識符
pname _指定獲取信息的類型,有 GL_COMPILE_STATUS、GL_DELETE_STATUS、GL_INFO_LOG_LENGTH、GL_SHADER_SOURCE_LENGTH、GL_SHADERTYPE 五種
params 用於存儲當前獲取信息的變量內存地址
glGetShaderInfoLog
void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsei_ _length, GLchar infoLog)
shader 着色器的內存標識符
maxLength 指最大的信息長度
length 獲取的信息長度,如果不知道可以是 NULL
infoLog 存儲信息的變量的內存地址

4) 配置 Fragment Shader
與 3) 方法一樣;

5) 創建 Shader Program

- (GLuint)createShaderProgram {

    return glCreateProgram();

}
函數描述
glCreateProgram創建 Shader Program 對象
glCreateProgram
GLuint glCreateProgram()
return GLuint 返回着色器程序的標識符

6) 裝載 Vertex Shader 和 Fragment Shader

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

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

}
函數描述
glAttachShader裝載 Shader 對象
glAttachShader
void glAttachShader(GLuint program, GLuint shader)
program 着色器程序的標識符
shader 要裝載的着色器對象標識符

7) 鏈接 Shader Program

- (void)linkProgramWithProgramID:(GLuint)programID {

    glLinkProgram(programID);

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

}
函數描述
glLinkProgram鏈接 Shader Program 對象
glGetProgramiv獲取 着色器程序的相關信息
glGetProgramInfoLog獲取 着色器程序的打印信息
glLinkProgram
void glLinkProgram(GLuint program)
program 着色器程序的標識符
glGetProgramiv
void glGetProgramiv(GLuint program, GLenum pname,GLint *params)
program 着色器程序的標識符
pname _可以選擇的消息類型有如下幾個,GL_ACTIVE_ATTRIBUTES、GL_ACTIVE_ATTRIBUTE_MAX_LENGTH、GL_ACTIVE_UNIFORMS、GL_ACTIVE_UNIFORM_MAX_LENGTH、GL_ATTACHED_SHADERS、GL_DELETE_STATUS、GL_INFO_LOG_LENGTH、GL_LINK_STATUS、GL_VALIDATESTATUS
params 存儲信息的變量的內存地址
glGetProgramInfoLog
void glGetProgramInfoLog(GLuint program,GLsizei maxLength, GLsizei_ _length, GLchar infoLog)
program 着色器程序的標識符
maxLength 指最大的信息長度
length 獲取的信息長度,如果不知道可以是 NULL
infoLog 存儲信息的變量的內存地址

5. 渲染繪製

1) 清空舊渲染緩存

- (void)clearRenderBuffer {

    glClear(GL_COLOR_BUFFER_BIT);

}
函數描述
glClear清空 渲染緩存的舊內容
glClear
void glClear (GLbitfield mask)
mask _三者中的一個 GL_COLOR_BUFFER_BIT (顏色緩存),GL_DEPTH_BUFFER_BIT ( 深度緩存 ), GL_STENCIL_BUFFERBIT (模板緩存)

2) 設置渲染窗口

- (void)setRenderViewPortWithCGRect:(CGRect)rect {

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

}
函數描述
glViewport設置 渲染視窗的位置和尺寸
glViewport
void glViewport(GLint x, GLint y, GLsizei w, GLsizei h)
x,y 渲染窗口偏移屏幕座標系左下角的像素個數
w,h 渲染窗口的寬高,其值必須要大於 0

3) 使用 Shder Program

- (void)userShaderWithProgramID:(GLuint)programID {

    glUseProgram(programID);

}
函數描述
glUseProgram使用 Shader Program
glUseProgram
void glUseProgram(GLuint program)
program 着色器程序的標識符

4) 關聯數據

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

- (void)attachTriangleVertexArrays {

    glEnableVertexAttribArray(VertexAttributePosition);

    if (self.vertexMode == VertexDataMode_VBO) {

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

    } else {

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

    }

}
函數描述
glEnableVertexAttribArray使能頂點數組數據
glVertexAttribPointer關聯頂點數據

a. 使能頂點緩存

glEnableVertexAttribArray
void glEnableVertexAttribArray(GLuint index)
index _attribute 變量的下標,範圍是 [ 0, GL_MAX_VERTEXATTRIBS - 1]

b. 關聯頂點數據

glVertexAttribPointer
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *ptr)
index _attribute 變量的下標,範圍是 [ 0, GL_MAX_VERTEXATTRIBS - 1]
size 指頂點數組中,一個 attribute 元素變量的座標分量是多少(如:position, 程序提供的就是 {x, y ,z} 點就是 3 個座標分量 ),範圍是 [1, 4]
type _數據的類型,只能是 GL_BYTE、GL_UNSIGNED_BYTE、GL_SHORT、GL_UNSIGNED_SHORT、GL_FLOAT、GL_FIXED、GL_HALF_FLOATOES *
normalized _指是否進行數據類型轉換的意思,GL_TRUE 或 GLFALSE
stride 指每一個數據在內存中的偏移量,如果填 0(零) 就是每一個數據緊緊相挨着。
ptr 數據的內存首地址

知識擴展:
1) 獲取最大 attribute 下標的方法

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

2) 關於 size 補充

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

3) 使能頂點數組數據?
其實頂點着色器中處理的數據有兩種輸入類型,CVOs ( Constant
Vertex Objects )、VAOs (Vertex Array Objects);
 glEnableVertexAttribArrayglDisableVertexAttribArray 函數就是使用 CVOs 還是 VAOs 的一組開關,看圖 :

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

若使用了 CVOs 作爲輸入數據的,要使用以下處理函數來替代 glVertexAttribPointer 函數:

4) OpenGL ES 只支持 float-pointer 類型的數據,所以纔會有 normalized 參數;

5) 頂點着色器的數據傳遞圖,

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

特別提醒,VBOs 只是一種爲了加快數據訪問和渲染調度的一種手段,而不是數據輸入方式的一種;

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

5) 繪製圖形

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

- (void)drawTriangle {

    glDrawArrays(GL_TRIANGLES,
                 PositionStartIndex,
                 DrawIndicesCount);

}
函數描述
glDrawArrays繪製所有圖元
glDrawArrays
void glDrawArrays(GLenum mode, GLint first, GLsizei count)
mode _繪製的圖元方式,只能是 GL_POINTS、GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP、GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLEFAN 的一種
first 從第幾個頂點下標開始繪製
count 指有多少個頂點下標需要繪製

6) 渲染圖形

- (void)render {

    [self.context presentRenderbuffer:GL_RENDERBUFFER];

}
函數描述
presentRenderbuffer:把 Renderbuffer 的內容顯示到窗口系統 (CAEAGLLayer) 中
presentRenderbuffer:
- (BOOL)presentRenderbuffer:(NSUInteger)target
target _只能是 GLRENDERBUFFER
return BOOL 返回是否綁定成功

補充:同時,這個函數也說明了 kEAGLDrawablePropertyRetainedBacking 爲什麼要設爲 YES 的原因:

如果要保存 Renderbuffer 的內容就要把 CARAGLLayer 的 drawableProperties 屬性的 kEAGLDrawablePropertyRetainedBacking 設置爲 YES 。

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


面向對象的重新設計:

消息處理的主流程就是上面的信號流程圖的步序。
面向對象,就是把所有的消息交給對象來處理咯,關注的就是消息的傳遞和處理。【可以按照你的喜好來設計,反正可擴展性和可維護性都比較好就行了,當然也不能把消息的傳遞變得很複雜咯】

OpenGL ES 2 iOS 渲染邏輯流程圖_面向對象化

項目文件結構:

完整代碼在 Github 上 DrawTriangle_OOP


第四步,練練手

建議按照自己的思路重新寫一個項目

1. 修改背景色

提示:glClear 函數

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

提示:CVOs,三個頂點是統一的顏色數據

3. 修改三角形的三個頂點的顏色(填充色):

提示:VAOs / VBOs ,在三個頂點的基礎上添加新的顏色數據

它們三個主要是爲了 [學 + 習] 如何關聯數據,對應的項目是:Github: DrawTriangle_OOP_Challenges_1

如果你發現文章有錯誤的地方,請在評論區指出,不勝感激!!!

目錄

一、分析拉伸的原因

1、修復前後照片對比
2、從問題到目標,分析原因

二、準備知識,三維變換

1、4 x 4 方陣
2、線性變換(縮放與旋轉)
3、平移
4、向量(四元數)
5、w 與 其它

三、OpenGL 下的三維變換

1、OpenGL 的座標系
2、OpenGL 的 gl_Position 是行向量還是列向量
3、單次三維變換與多次三維變換問題
4、OpenGL 的變換是在那個階段發生的,如何發生

四、修復拉伸問題

1、改寫 Shader Code
2、應用 3D 變換知識,重新綁定數據
  1) 在 glLinkProgram 函數之後,利用 glGetUniformLocation 函數
     得到 uniform 變量的 location (內存標識符)
  2) 從 Render Buffer 得到屏幕的像素比(寬:高)值,即爲縮小的值
  3) 使用 Shader Program , 調用 glUseProgram 函數
  4) 使用 3D 變換知識,得到一個縮放矩陣變量 scaleMat4
  5) 使用 glUniform* 函數把 scaleMat4 賦值給 uniform 變量
3、完整工程

一、分析拉伸的原因

1、修復前後照片對比

問題與目標

圖片通過 sketch 製作

2、從問題到目標,分析原因

1、它們的頂點數據均爲:

頂點數組

VFVertex

2、藉助 Matlab 把頂點數據繪製出來:

分佈圖

從圖可以看出,這三個數據形成的其實是一個等邊直角三角形,而在 iOS 模擬器中通過 OpenGL ES 繪製出來的是直角三角形,所以是有問題的,三角形被拉伸了。
3、on-Screen (屏幕) 的像素分佈情況:
1) iPhone6s Plus 屏幕:5.5 寸,1920 x 1080 像素分辨率,明顯寬高比不是 1:1 的;
2) OpenGL ES 的屏幕座標系 與 物理屏幕的座標系對比:

OpenGL ES 的屏幕座標系

物理屏幕的座標系

分析:前者是正方體,後者長方體,不拉伸纔怪。
3) 首先,OpenGL 最後生成的都是像素信息,再顯示在物理屏幕上;通過 1) 和 2) 可以知道 Y 方向的像素數量大於 X 方向的像素數量,導致真實屏幕所生成的 Y 軸與 X 軸的刻度不一致(就是 Y=0.5 > X=0.5),從而引起了最後渲染繪製出來的圖形是向 Y 方向拉伸了的。
動畫演示修復:

FixTriangle.gif

所以要做的事情是,把頂點座標的 Y 座標變小,而且是要根據當前顯示屏幕的像素比來進行縮小。

Gif 圖片,由 C4D 製作,PS 最終導出;

4) 在 Shader 裏面,v_Position 的數據類型是 vec4 ,即爲 4 分量的向量數據 {x,y,z,w}; 就是說,要把這個向量通過數學運算變成適應當前屏幕的向量。


二、準備知識,三維變換

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
-- 建議 --:如果向量、矩陣知識不熟悉的可以看看《線性代數》一書;如果已經有相應的基礎了,可以直接看《3D 數學基礎:圖形與遊戲開發》,瞭解 3D 的世界是如何用向量和矩陣知識描述的;若對 3D 知識有一定的認識,可以直接看《OpenGL Programming Guide》8th 的變換知識, 或 《OpenGL Superblble》7th 的矩陣與變換知識,明確 OpenGL 是如何應用這些知識進行圖形渲染的。
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

注:以下核心知識均來源於,《3D 數學基礎:圖形與遊戲開發》,建議看一下第 8 章;

4x4 整體

圖片通過 sketch 製作,請放大看

1、4 x 4 方陣

4X4 方陣

  • 1) 它其實就是一個齊次矩陣,是對 3D 運算的一種簡便記法;
  • 2) 3x3 矩陣並沒有包含平移,所以擴展到 4x4 矩陣,從而可以引入平移的運算;

2、線性變換(縮放與旋轉)

線性變換

  • n,是標準化向量,而向量標準化就是指單位化:

normalied

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

圖片來源於《3D 數學基礎:圖形與遊戲開發》5.7

  • k,是一個常數;

  • a,是一個弧度角;

1) 線性縮放

線性縮放

  • XYZ 方向的縮放:

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

圖片來源於《3D 數學基礎:圖形與遊戲開發》8.3.1

2) 線性旋轉

線性旋轉

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

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

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

圖片來源於《3D 數學基礎:圖形與遊戲開發》8.2.2

3、平移

平移

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

圖片來源於《3D 數學基礎:圖形與遊戲開發》9.4.2

4、向量(四元數)

四元數

a. 向量,即 4D 向量,也稱齊次座標 {x, y, z, w}; 4D->3D,{x/w, y/w, z/w};
b. 四元數,[ w, v ]或 [w, (x,y,z) ] 兩種記法,其中 w 就是一個標量,即一個實數;
c. 點乘

矩陣乘法,點乘

c.1 上面兩種是合法的,而下面兩種是不合法的,就是沒有意義的;
c.2 第一個爲 A(1x3) 行向量 (矩陣) 與 B(3x3)方陣的點乘,第二個是 A(3x3) 的方陣與 A(3x1) 的列向量 (矩陣) 的點乘;

圖片來源於《3D 數學基礎:圖形與遊戲開發》7.1.7

5、w 與 其它

這塊內容現在先不深究,不影響對本文內容的理解。

  • W

w

w,與平移向量 {x, y, z} 組成齊次座標;一般情況下,都是 1;

  • 投影

投影

這裏主要是控制投影,如透視投影;如:

圖片來源於《3D 數學基礎:圖形與遊戲開發》9.4.6


三、OpenGL 下的三維變換

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
這裏主要討論第一階段 Vertex 的 3D 變換,對於視圖變換、投影變換,不作過多討論;如果要完全掌握後面兩個變換,還需要掌握 OpenGL 下的多座標系系統,以及攝像機系統的相關知識。
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

1、OpenGL 的座標系

  • 座標系方向定義分兩種:

圖片來源於,《3D 數學基礎:圖形與遊戲開發》8.1;左右手座標系是用來定義方向的。

  • 旋轉的正方向

右手座標

圖片來源於,Diney Bomfim 的《Cameras on OpenGL ES 2.x - The ModelViewProjection Matrix》;這個就是 OpenGL 使用的座標系,右手座標系;其中白色小手演示了在各軸上旋轉的正方向(黑色箭頭所繞方向);

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

  • 這裏討論的核心是,gl_Position 接收的是 行向量,還是列向量?

行向量

列向量

  • 討論行列向量的目的是明確,3D 矩陣變換在做乘法的時候是使用左乘還是右乘;

圖片來源於,《線性代數》矩陣及其運算一節

從圖中的結果就可以看出,左乘和右乘運算後是完全不一樣的結果;雖然圖片中的矩陣是 2 x 2 方陣,但是擴展到 n x n 也是一樣的結果;

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

圖 1,列向量

* **英文大意**:矩陣和矩陣乘法在處理座標系顯示模型方面是一個非常有用的途徑,而且對於處理線性變換而言也是非常方便的機制。  

圖 2

紅框處的向量就是 v_Position 頂點數據;即 OpenGL 用的是列向量;(木有找到更有力的證據,只能這樣了)

  • 左乘右乘問題?

圖 3

* **英文大意**:在我們的視圖模型中,我們想通過一個向量來與矩陣變換進行乘法運算,這裏描述了一個矩陣乘法,向量先乘以 A 矩陣再乘以 B 矩陣:  

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

圖 1、3 來源於,《OpenGL Programming Guide 8th》第 5 章第二節
圖 2 來源於,《3D 數學基礎:圖形與遊戲開發》7.1.8

3、單次三維變換與多次三維變換問題

多次變換

1) OpenGL 的三維變換整體圖:

4x4 整體 OpenGL

因爲列向量的影響,在做點乘的時候,平移放在下方與右側是完全不一樣的結果,所以進行了適應性修改

  • 平移部分的內容:

4X4 方陣 OpenGL

平移 OpenGL

* 矩陣平移公式  

等式左側:A(4x4)方陣點乘 {v.x, v.y, v.z, 1.0} 是頂點數據列向量;右側就是一個 xyz 均增加一定偏移的列向量

圖片來源於,《OpenGL Superblble》7th, Part 1, Chapter 4. Math for 3D Graphics

  • 投影(就是零)

投影 OpenGL

2) 所有的變換圖例演示
物體的座標是否與屏幕座標原點重疊

Linaer Transforms

  • 單次變換(原點重疊)

Identity

無變換,即此矩陣與任一向量相乘,不改變向量的所有分量值,能做到這種效果的就是單位矩陣,而我們使用的向量是齊次座標 {x, y, z, w},所以使用 4 x 4 方陣;{w === 1}.

  • 縮放

Scale

單一的線性變換——縮放,縮放變換是作用在藍色區域的 R(3x3) 方陣的正對角線(從 m11(x)->m22(y)->m33(z))中; 例子是 X、Y、Z 均放大 3 倍。

  • 旋轉

Rotate

單一的線性變換——旋轉,旋轉變換是作用在藍色區域的 R(3x3) 方陣中; 例子是繞 Z 軸旋轉 50 度。

  • 平移

Translation

單一的線性變換——平移,平移變換是作用在綠色區域的 R(3x1) 矩陣中({m11, m21, m31} 對應 {x, y, z}); 例子是沿 X 正方向平移 2.5 個單位。

  • 單次變換(原點不重疊)

Translation&Scale

Translation&Rotate

以上圖片內容來源於《OpenGL Programming Guide》8th, Linear Transformations and Matrices 一小節,使用 skecth 重新排版並導出

3) 多次變換

連續變換

這裏的問題就是先旋轉還是後旋轉。旋轉前後,變化的是物體的座標系(虛線(變換後),實線(變換前)),主要是看你要什麼效果,而不是去評論它的對錯。
圖片來源於,《OpenGL Superblble》7th, Matrix Construction and Operators 一節;

4、OpenGL 的變換是在那個階段發生的,如何發生

3D 變換

ES 主要看紅框處的頂點着色階段即可,所以我們的變換代碼是寫在 Vertex Shader 的文件中。

變換轉換

這裏描述了三個變換階段,第一個階段是模型變換,第二個是視圖變換階段,第三個是投影變換階段,最後出來的纔是變換後的圖形。本文討論的是第一個階段。

詳細過程

作爲了解即可
以上圖片均來源於,《OpenGL Programming Guide》8th, 5. Viewing Transformations, Clipping, and Feedback 的 User Transformations 一節;


四、修復拉伸問題

1、改寫 Shader Code

增加了一個 uniform 變量,而且是 mat4 的矩陣類型,同時左乘於頂點數據;

  • 爲什麼使用 uniform 變量?
    • 首先, Vertex Shader 的輸入量可以是 : attribute、unforms、samplers、temporary 四種;
    • 其次,我們的目的是把每一個頂點都縮小一個倍數,也就是它是一個固定的變量,即常量,所以排除 arrribute、temporary ;
    • 同時,既然是一個常量數據,那麼 samplers 可以排除,所以最後使用的是 uniforms 變量;
  • 爲什麼使用 mat4 類型?
    v_Position 是 {x, y, z, w} 的列向量,即爲 4 x 1 的矩陣,如果要最終生成 gl_Position 也是 4 x 1 的列向量,那麼就要左乘一個 4 x 4 方陣;而 mat4 就是 4 x 4 方陣。

補充:n x m · 4 x 1 -> 4 x 1,如果要出現最終 4 x 1 那麼,n 必須要是 4;如果矩陣點乘成立,那麼 m 必須要是 4; 所以最終結果是 n x m = 4 x 4 ;

2、應用 3D 變換知識,重新綁定數據

這裏主要解決,如何給 uniform 變量賦值,而且在什麼時候進行賦值的問題

::::::::::::::::::::::::::::::::::::::::::: 核心步驟::::::::::::::::::::::::::::::::::::::::::::::::::::::
*
》》》 1、在 glLinkProgram 函數之後,利用 glGetUniformLocation 函數
》》》 得到 uniform 變量的 location (內存標識符)
》》》 2、從 Render Buffer 得到屏幕的像素比(寬:高)值,即爲縮小的值
》》》 3、使用 Shader Program , 調用 glUseProgram 函數
》》》 4、使用 3D 變換知識,得到一個縮放矩陣變量 scaleMat4
》》》 5、使用 glUniform
 函數把 scaleMat4 賦值給 uniform 變量 **

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

  • 如何給 uniform 變量賦值?

》》》1、得到 uniform 的內存標識符

要在 glLinkProgram 後,再獲取 location 值,因爲只有鏈接後 Program 纔會 location 的值

- (BOOL)linkShaderWithProgramID:(GLuint)programID {
    // 綁定 attribute 變量的下標
    // 如果使用了兩個或以上個 attribute 一定要綁定屬性的下標,不然會找不到數據源的
    // 因爲使用了一個的時候,默認訪問的就是 0 位置的變量,必然存在的,所以纔不會出錯
    [self bindShaderAttributeValuesWithShaderProgramID:programID];
    // 鏈接 Shader 到 Program
    glLinkProgram(programID);
    // 獲取 Link 信息
    GLint linkSuccess;
    glGetProgramiv(programID, GL_LINK_STATUS, &linkSuccess);
    if (linkSuccess == GL_FALSE) {
        GLint infoLength;
        glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &infoLength);
        if (infoLength > EmptyMessage) {
            GLchar *messages = malloc(sizeof(GLchar *) * infoLength);
            glGetProgramInfoLog(programID, infoLength, NULL, messages);
            NSString *messageString = [NSString stringWithUTF8String:messages];
            NSLog(@"Error: Link Fail %@ !", messageString);
            free(messages);
        }
        return Failure;
    }
    // 在這裏
    [self.shaderCodeAnalyzer updateActiveUniformsLocationsWithShaderFileName:@"VFVertexShader"
                                                                   programID:programID];
    return Successfully;
}
- (void)updateActiveUniformsLocationsWithShaderFileName:(NSString *)fileName programID:(GLuint)programID {

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

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

}

補充:

glGetActiveUniform
void glGetActiveUniform(GLuint program, GLuint index, GLsizei bufSize, GLsizei length, GLint size, GLenum type, char name)
program 指 Shader Program 的內存標識符
index 指下標,第幾個 uniform 變量,[0, activeUniformCount]
bufSize _所有變量名的字符個數,如:v_Projection , 就有 12 個,如果還定義了 vTranslation 那麼就是 12 + 13 = 25 個
length NULL 即可
size 數量,uniform 的數量,如果不是 uniform 數組,就寫 1,如果是數組就寫數組的長度
type _uniform 變量的類型,GL_FLOAT, GL_FLOAT_VEC2,GL_FLOAT_VEC3, GL_FLOAT_VEC4,GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4, GL_BOOL,GL_BOOL_VEC2, GL_BOOL_VEC3, GL_BOOL_VEC4,GL_FLOAT_MAT2, GL_FLOAT_MAT3, GL_FLOAT_MAT4,GL_SAMPLER_2D, GL_SAMPLERCUBE
name uniform 變量的變量名
// 這個函數可以得到,正在使用的 uniform 個數,即可以知道 index 是從 0 到幾;
// 還有可以得到,bufSize 的長度
glGetProgramiv(progObj, GL_ACTIVE_UNIFORMS, &numUniforms);
glGetProgramiv(progObj, GL_ACTIVE_UNIFORM_MAX_LENGTH,
&maxUniformLen);

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

注:VFShaderValueRexAnalyzer 類就是一個方便進行調用的一種封裝而已,你可以使用你喜歡的方式進行封裝;

圖片來源於,《OpenGL ES 2.0 Programming Guide》4. Shaders and Programs,Uniforms and Attributes 一節

  • 在什麼時候進行賦值操作?
    一定要在 glUseProgram 後再進行賦值操作,不然無效
- (void)drawTriangle {

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

}

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

- (CGFloat)windowScaleFactor {

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

    return scaleFactor;

}

補充:renderBufferSize

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

》》》3、使用 Shader Program

- (void)useShader {

    glUseProgram(self.shaderProgramID);

}```

》》》**4、使用 3D 變換知識,得到一個縮放矩陣變量 scaleMat4**

VFMatrix4 scaleMat4 = VFMatrix4MakeScaleY(scale);```

擴展 1:

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

它們都定義在:

VFMath

注:如果不想自己去寫這些函數,那麼可以直接使用 GLKit 提供的

數學函數

》》》》》》 個人建議,自己去嘗試寫一下會更好

》》》*5、使用 glUniform 函數把 scaleMat4 賦值給 uniform 變量 **

- (void)makeScaleToFitCurrentWindowWithScale:(float)scale {

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

    // v_Projection 投影
//    VFMatrix4 scaleMat4 = VFMatrix4Identity;
    VFMatrix4 scaleMat4 = VFMatrix4MakeScaleY(scale);
    VFMatrix4 transMat4 = VFMatrix4Identity; //VFMatrix4MakeTranslationX(0.3)
    glUniformMatrix4fv((GLint)uniforms[@"v_Projection"].location,   // 定義的 uniform 變量的內存標識符
                       1,                                           // 不是 uniform 數組,只是一個 uniform -> 1
                       GL_FALSE,                                    // ES 下 只能是 False
                       (const GLfloat *)scaleMat4.m1D);             // 數據的首指針

    glUniformMatrix4fv((GLint)uniforms[@"v_Translation"].location,   // 定義的 uniform 變量的內存標識符
                       1,                                           // 不是 uniform 數組,只是一個 uniform -> 1
                       GL_FALSE,                                    // ES 下 只能是 False
                       (const GLfloat *)transMat4.m1D);             // 數據的首指針

}

擴展 2:

  • 賦值函數有那些?
    它們分別是針對不同的 uniform 變量進行的賦值函數

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

> glsl 代碼分析類  
> 
> 
> ![](/usr/uploads/2017/10/4046991714.png)  
> 
> 
>   
> 核心的知識是**正則表達式**,主要是把代碼中的變量解析出來,可以對它們做大規模的處理。有興趣可以看一下,沒有興趣的可以忽略它完全不影響學習和練習本文的內容。

* * *

#### **學習這篇文章的大前提是,你得有[《OpenGL ES 2.0 (iOS): 一步從一個小三角開始》](http://www.jianshu.com/p/d22cf555de47)的基礎知識。**

* * *

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

# **本文核心目的就是熟練圖形的分析與繪製**

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

* * *

> ## 目錄
> 
> ### 零、目標+準備
> 
> ### 一、圖元繪製之線

0. 工程目錄

  1. 繪製單一、交叉的線
  2. 繪製折線
  3. 繪製幾何圖形
  4. 繪製三角化的幾何圖形
  5. 繪製曲線、圓形

二、圖元繪製之三角形

0.工程目錄
1. 繪製基本幾何圖形

三、圖元繪製之點精靈(內容爲空)

四、練練手

0.工程目錄
1. 繪製一棵卡通樹
2. 繪製一張卡片
3. 繪製一棵草

零、目標 + 準備

1) 目標

Geometries

2) 準備

  • 觀察所有圖形,發現它們都是點與點之間的連線(直線或曲線),組成一個幾何形狀( ^_^ 好像有點廢話);
  • 除了點線的問題外,還可以知道幾何形狀,有交疊、閉環、開環三種情況;
  • 除此之外,還有填充色有無的問題;

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

  • A、根據 OpenGL ES 的特點,歸納總結:
    • a. 要繪製這些圖形,需要控制頂點的數量
    • b. 控制頂點與頂點之間的連接情況,Strip 或 Loop(Fan) 或 沒關係
    • c. 控制圖形的填充色,即 Fragment Shader 與 Vertex Shader 之間的顏色傳遞問題;
  • B、OpenGL ES 下控制數據源與繪製方式的函數有那些?(VBO 模式)
    • a. 綁定 VBO 數據 glBufferData
    • b. 繪製數據 glDrawArrays/glDrawElements
    • c. 繪製模式有:
      • GL_POINTS (點)
      • GL_LINES/GL_LINE_STRIP/GL_LINE_LOOP (線)
      • GL_TRIANGLES/GL_TRIANGLE_STRIP/GL_TRIANGLE_FAN (面)

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

所以本文就是根據圖形的形態,選擇適當的繪製方式,去繪製圖形;核心目的就是熟練圖形的分析與繪製;
因爲是練習圖元,所以學習的重點在,數據綁定和圖形繪製這一塊;


一、圖元繪製之線

Lines,多條線的意思;
Line Strip , 指首尾相接的線段,第一條線和最後一條線沒有連接在一起;
Line Loops, 指首尾相接的線段,第一條線和最後一條線連接在一起,即閉合的曲線;

模式線與點的數量關係
GL_LINESnPoints = 2 * mLines
GL_LINE_STRIPnPoints = mLines + 1
GL_LINE_LOOPnPoints = mLines

ep: 上圖中的圖形

模式線與點的數量關係
GL_LINESv0~v5(6) = 2 * 3
GL_LINE_STRIPv0~v3(4) = 3 + 1
GL_LINE_LOOPv0~v4(5) = 5

0. 工程目錄

完整的線元工程在,這一章的結尾;

工程目錄

圖中紅色箭頭所指的就是要修改的類,其中 VFVertexDatasManager 類是核心,它是負責整個工程的數據綁定和圖形繪製的;
藍色框所指的都是工程中的靜態頂點數據(當然你也可以動態生成並進行綁定繪製);

1. 繪製單一、交叉的線

LINES

  • 圖形分析

    • 首先它們都是線,所以選擇的是 線模式;
    • 左側就是一條線 -> GL_LINES,有兩個頂點座標,而且座標是左底右高
    • 右側是兩條交叉線 -> GL_LINES,有四個頂點座標

nPoints = 2 * mLines

  • 開始寫代碼

    • 數據源準備
// 位於 VFBaseGeometricVertexData.h
// 單線段
static const VFVertex singleLineVertices[] = {
  { 0.5f,  0.5f, 0.0f},
  {-0.5f, -0.5f, 0.0f},
};
// 交叉線
static const VFVertex crossLinesVertices[] = {
  // Line one
  { 0.5f,  0.5f, 0.0f},
  {-0.5f, -0.5f, 0.0f},
  // Line Two
  {-0.53f, 0.48f, 0.0f},
  { 0.55f, -0.4f, 0.0f},
};
* 修改數據綁定方法
/**
*  裝載數據
*/
- (void)attachVertexDatas {
  self.currentVBOIdentifier = [self createVBO];
  self.drawInfo = [self drawInfoMaker];
  if (self.drawInfo.elementDataPtr) {
      self.currentElementVBOIdentifier = [self createVBO];
      [self bindVertexDatasWithVertexBufferID:self.currentElementVBOIdentifier
                                   bufferType:GL_ELEMENT_ARRAY_BUFFER
                                 verticesSize:self.drawInfo.elementDataSize
                                     datasPtr:self.drawInfo.elementDataPtr];
  }
  [self bindVertexDatasWithVertexBufferID:self.currentVBOIdentifier
                               bufferType:GL_ARRAY_BUFFER
                             verticesSize:self.drawInfo.dataSize
                                 datasPtr:self.drawInfo.dataPtr]; // CPU 內存首地址
  [self attachVertexArrays];
}

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

/**
*  使用頂點緩存對象
*
*  @param vertexBufferID 頂點緩存對象標識
*/
- (void)bindVertexDatasWithVertexBufferID:(GLuint)vertexBufferID
                               bufferType:(GLenum)bufferType
                             verticesSize:(GLsizeiptr)size
                                 datasPtr:(const GLvoid*)dataPtr {

  glBindBuffer(bufferType, vertexBufferID);
  // 創建 資源 ( context )
  glBufferData(bufferType,        // 緩存塊 類型
               size,              // 創建的 緩存塊 尺寸
               dataPtr,           // 要綁定的頂點數據
               GL_STATIC_DRAW);   // 緩存塊 用途
}

還有- (VFLineDrawInfo)drawLineInfoMaker 方法,生成相應圖形的數據源信息,如下:

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

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

        break;
    }
    case VFDrawGeometryType_CrossLines: {

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

        break;
    }

其中 @property (assign, nonatomic) VFDrawInfo drawInfo; 是定義的數據源信息結構體,具體信息如下:

// 位於 VFVertexDatasManager 類中
typedef struct {
  // 數據所佔的內存大小
  GLsizeiptr dataSize;
  // 數據的內存首地址
  const GLvoid *dataPtr;
  // 需要繪製的點數量
  GLsizei verticesIndicesCount;
  // 圖元的繪製類型
  VFPrimitiveMode primitiveMode;
  // 下標數據所佔的內存大小
  GLsizeiptr elementDataSize;
  // 下標內存首地址
  const GLvoid *elementDataPtr;
  // 下標個數
  GLsizei elementIndicesCount;
} VFDrawInfo;
* 修改繪製方法,直接獲取信息即可
// 位於 VFVertexDatasManager 類中
#define GPUVBOMemoryPtr    (0)
/**
 *  繪製圖形
 */
- (void)draw {
  glLineWidth(DEFAULT_LINE_WITH);
  if (self.drawInfo.elementIndicesCount) {
     glDrawElements(self.drawInfo.primitiveMode,
                    self.drawInfo.elementIndicesCount,
                    GL_UNSIGNED_BYTE,
                    GPUVBOMemoryPtr);  // GPU 內存中的首地址
    return;
}
  glDrawArrays(self.drawInfo.primitiveMode,
               StartIndex, // 就是 0
               self.drawInfo.verticesIndicesCount);
}

其中 glLineWidth函數是修改線的寬度的;
glDrawElements是繪製下標的方法;這裏不需要用到,所以先不解釋;

* 修改圖形顯示
// 位於 VFVertexDatasManager 類中
/**
*  繪製的幾何圖形類型
*/
@property (assign, nonatomic) VFDrawGeometryType drawGeometry;

// 位於 VFRenderWindow 類
// 位於 .m 文件的 263 行
/**
*  裝載頂點數據
*/
- (void)prepareVertexDatas {
  [self.vertexManager setDrawGeometry:VFDrawGeometryType_CrossLines];
  [self.vertexManager attachVertexDatas];
}

這裏新增了一個枚舉類型的變量,drawGeometry ,目的是方便外部類進行操控,而進行何種類型圖形的繪製渲染,VFDrawGeometryType 定義如下:

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

  VFDrawGeometryType_SingleLine = 0,  // 單條線
  VFDrawGeometryType_CrossLines,      // 交叉線

  VFDrawGeometryType_MountainLines,   // 拆線山

  VFDrawGeometryType_TriangleLines,   // 線三角
  VFDrawGeometryType_RectangleLines,  // 線正方形
  VFDrawGeometryType_PentagonsLines,  // 線五邊形
  VFDrawGeometryType_HexagonsLines,   // 線六邊形
  VFDrawGeometryType_TrapezoidLines,  // 線梯形
  VFDrawGeometryType_PentagramLines,  // 線五角星
  VFDrawGeometryType_RoundLines,      // 線圓

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

  VFDrawGeometryType_BezierMountain,  // Bezier 山
  VFDrawGeometryType_BezierRound,     // Bezier 圓
  VFDrawGeometryType_BezierOval,      // Beizer 橢圓
};

這一節只是,單線與交叉線的繪製;

  • 程序運行結果

2. 繪製折線

LINE STRIP MOUN

  • 圖形分析
    • 首先這是一條線,所以選擇的是 線模式;
    • 但是它是一條折線,即多段線首尾相接組成的線,而且沒有閉合,GL_LINES_STRIP 模式;
    • 有 7 個頂點,6 條線 (nPoints = mLines + 1)
  • 開始寫代碼

    • 數據源
// 位於 VFBaseGeometricVertexData.h
// 折線(山丘)
static const VFVertex mountainLinesVertices[] = {
// Point one
{-0.9f, -0.8f, 0.0f},

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

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

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

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

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

// Point Seven
{ 0.95f, -0.95f, 0.0f},
};
* 修改數據綁定方法  

在 drawLineInfoMaker 類中增加新的內容,其它不變;

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

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

        break;
    }
* 修改圖形的顯示
// 位於 VFRenderWindow 類
// 位於 .m 文件的 263 行
/**
*  裝載頂點數據
*/
- (void)prepareVertexDatas {
  [self.vertexManager  setDrawGeometry:VFDrawGeometryType_MountainLines];
  [self.vertexManager attachVertexDatas];
}
  • 程序運行結果

3. 繪製幾何圖形

Triangle2Round.gif

LINE LOOP

  • 圖形分析
    多段線首尾相接組成的幾何形狀,GL_LINES_LOOP 模式;

nPoints = mLines

  • 開始寫代碼

    • 數據源(從左至右),其中五角星這個數據,可以利用內五邊形與外五邊形相結合的方法(當然內五邊形的點要做一個角度旋轉),生成相應的點;

所有的點,都通過程序動態生成,如下:

這個類的計算原理是,建立極座標系,確定起始點,再循環增加旋轉角度,就可以得到所有的點,包括圓的點(圓即正多邊形,不過它的邊數已經多到細到人眼無法識別,而出現曲線的效果,就像這一小節開始的動態圖一樣的原理,當然橢圓的點集也可以通過這種方式得到)

這兩個類在另外的工程裏面, Github: 動態計算點

它的小應用,你可以按照自己的想法盡情改寫......

紅框處的,就是點的生成方法;箭頭所指的函數是把生成的點數據按照一定的格式寫入文件的方法(文件會自動創建);

下面是具體的數據:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

圓的頂點數據在單獨的文件中, VFRound.h,也是通過動態點生成的【因爲點太多,所以單獨放在一個文件中進行管理】;

* 修改數據綁定方法,在 drawLineInfoMaker 方法中增加新的內容
//  位於 VFVertexDatasManager 類的
// - (VFLineDrawInfo)drawLineInfoMaker; 方法中
    case VFDrawGeometryType_TriangleLines: {

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

        break;
    }
    case VFDrawGeometryType_RectangleLines: {

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

        break;
    }
    case VFDrawGeometryType_PentagonsLines: {

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

        break;
    }
    case VFDrawGeometryType_HexagonsLines: {

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

        break;
    }
    case VFDrawGeometryType_TrapezoidLines: {

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

        break;
    }
    case VFDrawGeometryType_PentagramLines: {

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

        break;
    }
    case VFDrawGeometryType_RoundLines: {

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

        break;
    }
  • 圖形顯示類(VFRenderWindow )也做相應的修改即可,位於 .m 文件的 263 行;

  • 程序運行結果

TRI-ROUND

4. 繪製三角化的幾何圖形(Low Poly)

TRIANGLE STRIP FAN PLO

  • 圖形分析
    • 首先它們都是由線組成,線模式
    • 其次,它們的線是閉合的,首尾相接?GL_LINES_LOOP ?
    • 所謂首尾相接,形成閉合圖形,是起點直接到達終點,就是說起點只會被經過一次,就是最後閉合的那一次;觀察圖形,起點如果只被經過一次,能不能用線繪製出來,很難吧,特別是最後一個,所以這裏直接用 GL_LINESSTRIP 模式,之後任意編排線經過點的順序,即可。(當然,如果你有興趣的話,也可以寫一個算法去計算點被經過最少的次數下,圖形可以完整繪製出來)_
    • 點可能會多次被經過,那麼就是說,這個點要被程序調度多次,但是 glDrawArrays 只能一個頂點被調度一次啊。所以這裏要用它的兄弟函數 glDrawElements 這個函數的意思就是繪製成員,頂點數據的下標就是它的成員,即通過頂點數據的成員來訪問數據而進行靈活繪製。

glDrawElements 根據頂點數據在內存的下標進行繪製的方法

glDrawElements
void glDrawElements(GLenum mode, GLsizei count,GLenum type, **const GLvoid*** indices)
mode _只能是以下幾種:GL_POINTS、GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP、GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLEFAN
count indices 的數量
type _下標的數據類型:GL_UNSIGNED_BYTE、GL_UNSIGNED_SHORT、GL_UNSIGNED_INT(它只能在使用了 OES_element_indexuint 才能使用)
indices 下標在內存中的首地址 (如果使用了 VBO,就是 GPU 內存中的首地址,若不是,則爲 CPU 內存中的首地址)
  • 開始寫代碼

    • VFLineDrawInfo 增加了對下標繪製的支持
typedef struct {
  // 數據所佔的內存大小
  GLsizeiptr dataSize;
  // 數據的內存首地址
  const GLvoid *dataPtr;
  // 需要繪製的點數量
  GLsizei verticesIndicesCount;
  // 圖元的繪製類型
  VFPrimitiveMode primitiveMode;
  // 下標數據所佔的內存大小
  GLsizeiptr elementDataSize; // 在這.....
  // 下標內存首地址
  const GLvoid *elementDataPtr; // 在這.....
  // 下標個數
  GLsizei elementIndicesCount; // 在這.....
} VFLineDrawInfo;
* 在原來的線數據基礎下,增加對應圖形的下標數據  

這裏選取下標的原則是,讓每一個點都儘可能少地被經過,從而完成圖形的繪製,目的就是爲了節省資源。

// 四邊形的下標數據
static const GLubyte rectangleElementIndeices[] = {
  0, 1, 2,
  3, 0, 2,
};
// 五邊形的下標數據
static const GLubyte pentagonsElementIndeices[] = {
  4, 1, 0, 4,
  3, 1, 2, 3,
};
// 六邊形的下標數據
static const GLubyte hexagonsElementIndeices[] = {
  5, 1, 0, 5,
  4, 1, 2, 4,
  3, 2,
};
// 梯形的下標數據
static const GLubyte trapezoidElementIndeices[] = {
1, 2, 3, 0,
1, 3,
};
//五角星形的下標數據
static const GLubyte pentagramElementIndeices[] = {
  1, 2, 3, 4,
  5, 6, 7, 8,
  9, 0, 1,
  9, 7, 5, 3, 1,
  5, 7, 1 
};
  • 修改數據綁定方法
    綁定新增加的下標數據支持,使用 VBO 的方式(雖然前面已經寫過,這裏重溫一下,因爲這裏都是真正的應用)
// 核心方法
/**
*  裝載數據
*/
- (void)attachVertexDatas {
  self.currentVBOIdentifier = [self createVBO];

  self.lineInfo = [self drawLineInfoMaker];

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

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

  [self attachVertexArrays];
}

在 drawLineInfoMaker 方法中新增內容:

// drawLineInfoMaker 裏面的新增內容
      case VFDrawGeometryType_LowPolyRectLines: {

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

          break;
      }
      case VFDrawGeometryType_LowPolyPentLines: {

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

          break;
      }
      case VFDrawGeometryType_LowPolyHexLines: {

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

          break;
      }
      case VFDrawGeometryType_LowPolyTrazLines: {

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

          break;
      }
      case VFDrawGeometryType_LowPolyStarLines: {

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

          break;
      }
// 修改的數據綁定方法
/**
*  使用頂點緩存對象
*
*  @param vertexBufferID 頂點緩存對象標識
*/
- (void)bindVertexDatasWithVertexBufferID:(GLuint)vertexBufferID
                             bufferType:(GLenum)bufferType
                           verticesSize:(GLsizeiptr)size
                               datasPtr:(const GLvoid*)dataPtr {

  glBindBuffer(bufferType, vertexBufferID);

  // 創建 資源 ( context )
  glBufferData(bufferType,        // 緩存塊 類型
               size,              // 創建的 緩存塊 尺寸
               dataPtr,           // 要綁定的頂點數據
               GL_STATIC_DRAW);   // 緩存塊 用途
}
  • 數據繪製方法中的下標繪製支持
// 修改的繪製方法
#define GPUVBOMemoryPtr    (0)
/**
*  繪製圖形
*/
- (void)draw {

  glLineWidth(DEFAULT_LINE_WITH);

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

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

Rect-Star

5. 繪製曲線、圓形

BAISER

  • 圖形分析

    • 首先,它們都是曲線,它們都可以通過 GL_LINE_STRIP 條帶來進行繪製,而且後者也可能通過 GL_LINE_LOOP 進行繪製;
    • 根據上一節的圓可以知道,只要線足夠短,以致人眼無法分辨,那麼折線就可以形成曲線,但是有個問題?左邊的,折線怎麼控制它的方向呢,第一個點與第二個點之間的折線彎曲程度,要怎麼才能生成它的點集呢?
    • OpenGL 是以點爲基礎進行圖元的繪製的,那麼只要有一個方法動態地根據固定點去控制之間曲線點的生成,問題就解決了。座標與點,那麼肯定是函數,要生成曲線,貝塞爾曲線函數就可以了(如果想不到,回憶你所見過的任一個圖形繪製軟件,就秒懂了,如:PS 的鋼筆工具, skecth 的鋼筆工具......)。
  • 知識補充 (貝塞爾曲線)
    請看下面的 word/pdf 文檔《貝塞爾曲線推導》
    書寫貝塞爾曲線函數如下,具體實現也在 Github: 動態計算點 這裏

文件

應用

  • 開始寫代碼

    • 數據源都在 文件中,紅框處

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

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

       break;
   }
   case VFDrawGeometryType_BezierRound: {

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

       break;
   }
   case VFDrawGeometryType_BezierOval: {

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

       break;
   }
  • 當然圖形顯示類,也要改咯!

  • 程序運行結果

Bezier


二、圖元繪製之三角形

Triangles,就是多個三角形;
Triangle Strip, 指條帶,相互連接的三角形;
Triangle Fan, 指扇面,相互連接的三角形;

圖 1:三角形模式

圖 2:STRIP

圖 3:FAN

模式三角形與點的數量關係
GL_TRIANGLESnPoints = 3 * mTriangles
GL_TRIANGLE_STRIPnPoints = mTriangles + 2
GL_TRIANGLE_FANnPoints = mTriangles + 2

ep: 圖 1 中的圖形

模式三角形與點的數量關係
GL_TRIANGLESv0~v5(6) = 3 * 2
GL_TRIANGLE_STRIPv0~v4(5) = 3+ 2
GL_TRIANGLE_FANv0~v4(5) = 3+ 2

0. 工程目錄

工程目錄

這裏沒有什麼太大的變化,只是數據的集合發生了一些變化而已;

1. 繪製基本幾何圖形

TRIANGLE STRIP FAN

  • 圖形分析

    • 首先,第一張圖片每一個圖形都是一個面,但是 OpenGL 只能直接繪製三角面,所以必須把圖形分解成三角面才能進行繪製;
    • 以下就是分解成三角面之後的圖形:

TRIANGLE LINESON

當然你也可以按照自己的方式進行分解,一定要遵守這裏的點、三角形關係

不然圖形是不能正確地繪製出來的;

  • 這裏容易出問題的是最後一個圖形(五角星形),三角形與點的關係:10(點的數量) = 10(分割出來的三角形數量) + 2,很明顯是不相等的,所以 10 個點是不可能繪製出來這個圖形的,只能再增加兩個點; 除了點的數量問題外,它還不是一個條帶(或者說用條帶來描述並不合適),它更適合用扇面來描述,即 GL_TRIANGLE_FAN;

    • 開始寫代碼
  • 數據源,它們都可以通過 FAN 或 STRIP 進行繪製,當然那個點用得少而且圖形繪製完整,以及方便,就用那個;像五角星那個圖形這麼麻煩,當然不做兩種試驗了;STRIP 模式下的點的分佈要特別注意,偶數下標在上面,奇數下標在下面【把圖形壓扁,你就能看出來了】
// 三角形
static const VFVertex triangleTrianglesVertices[] = {
  // Point V0
  {0.000000, 0.500000, 0.000000},

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  // GL_TRIANGLE_FAN
  // Point V0
  {0.000000, 0.000000, 0.000000}, // 在原來的基礎上,增加的起點

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

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

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

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

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

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

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

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

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

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

  // Point V11
  {0.000000, 0.500000, 0.000000},// 在原來的基礎上,增加的終點
};
* 數據的綁定(與線元一致),只是修改了 VFDrawGeometryType 枚舉和 drawLineInfoMaker 方法而已;

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

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

      break;
  }
  case VFDrawGeometryType_RectangleTriangles: {

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

      break;
  }
  case VFDrawGeometryType_PentagonsTriangles: {

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

      break;
  }
  case VFDrawGeometryType_HexagonsTriangles: {

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

      break;
  }
  case VFDrawGeometryType_TrapezoidTriangles: {

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

      break;
  }
  case VFDrawGeometryType_PentagramTriangles: {

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

      break;
  }
  case VFDrawGeometryType_RoundTriangles: {

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

      break;
  }
}
  * draw 方法
#define GPUVBOMemoryPtr    (0)
/**
*  繪製圖形
*/
- (void)draw {

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

  glDrawArrays(self.lineInfo.primitiveMode,
               StartIndex, // 0
               self.lineInfo.verticesIndicesCount);
}
  • 同樣要修改圖形顯示類(VFRenderWindow).m 文件的 263 行;

  • 程序運行結果

TRI-ROUND Triangle

完整的程序代碼: Github DrawGeometries_Triangles


三、圖元繪製之點精靈

這裏不進行詳細講解,個人感覺在這裏講沒什麼意思,還是放在 Texture 紋理部分進行詳細講解會比較有用,而且好玩;

如果只是學習 gl_PointSize 的話沒意思,結合 gl_PointCoord 去學習反而更有趣,不過這裏要有紋理的知識,所以先行不講了;


四、練練手

Challenges

這裏的目的不是爲了繪製它們而進行繪製,而是針對圖元繪製做一個深入的學習,要學習分析圖形和尋找合適有效的繪製方式,而且還要做到判斷數據的大致生成方法方式是什麼,不然你永遠都只是一個只會搞代碼的搬運工而已;編程可不僅僅是搞代碼;

0. 工程目錄

取消了採用結構體存取數據的方式,改用 Model 類,方便 OC 處理和傳輸;

1. 繪製一棵卡通樹

Tree

提示:進行兩次的 glDraw* 調用,分別繪製外邊的線和內部的填充圖

2. 繪製一張卡片

Card

提示:把數據分成左、右、右中線,三種,原因是左邊的數據是用貝塞爾曲線生成數據量非常大;主要是利用 glBufferSubData 與 glBufferData 的結合,以及 glVertexAttribPointer 的配合;

3. 繪製一棵草

Grass

注意:儘可以地用肉眼去判斷線的走向,用 動態計算點 的類做實驗,不斷成長起來吧。

完整的挑戰項目:Github DrawGeometries_Challenge


目錄

一、多座標系

1.  世界座標系
2.  物體(模型)座標系
3.  攝像機座標系
4.  慣性座標系

二、座標空間

1.  世界空間
2.  模型空間
3.  攝像機空間
4.  裁剪空間
5.  屏幕空間

三、OpenGL ES 2 3D 空間

1.  變換髮生的過程
2.  各個變換流程分解簡述
3.  四次變換與編程應用

四、工程例子

五、參考書籍


一、多座標系

1. 世界座標系

  • 即物體存在的空間,以此空間某點爲原點,建立的座標系

  • 世界座標系是最大的座標系,世界座標系不一定是指 “世界”,準確來說是一個空間或者區域,就是足以描述區域內所有物體的最大空間座標,是我們關心的最大座標空間;

  • 例子

    • ep1:
      比如我現在身處廣州,要描述我現在所在的空間,對我而言最有意義就是,我身處廣州的那裏,而此時的廣州就是我關心的 “世界座標系”,而不用描述我現在的經緯座標是多少,不需要知道我身處地球的那個經緯位置。
      這個例子是以物體的方向思考的最合適世界座標系;(當然是排除我要與廣州以外的區域進行行爲交互的情況咯!)

    • ep2:
      如果現在要描述廣州城的全貌,那麼對於我們而言,最大的座標系是不是就是廣州這個世界座標系,也就是所謂的我們最關心的座標系;
      這個例子是以全局的方向思考的最合適世界座標系;
  • 世界座標系主要研究的問題:
    1) 每個物體的位置和方向
    2) 攝像機的位置和方向
    3) 世界的環境(如:地形)
    4) 物體的運動(從哪到哪)

2. 物體(模型)座標系

  • 模型自身的座標系,座標原點在模型的某一點上,一般是幾何中心位置爲原點

  • 模型座標系是會跟隨模型的運動而運動,因爲它是模型本身的 “一部份” ;

  • 模型內部的構件都是以模型座標系爲參考進而描述的;

  • ep:
    比如有一架飛機,機翼位於飛機的兩側,那麼描述機翼最合適的座標系,當然是相對於飛機本身,機翼位於那裏;飛機在飛行的時候,飛機本身的座標系是不是在跟隨運動,機翼是不是在飛機的座標中同時運動着。

3. 攝像機座標系

  • 攝像機座標系就是以攝像機本身爲原點建立的座標系,攝像機本身並不可見,它表示的是有多少區域可以被顯示(渲染)

  • 白色線所圍成的空間,就是攝像機所能捕捉到的最大空間,而物體則位於空間內部;

  • 位於攝像機捕捉空間外的圖形會直接被剔除掉;

4. 慣性座標系

  • 它的 X 軸與世界座標系的 X 軸平行且方向相同,Y 軸亦然,它的原點與模型座標系相同

  • 它的存在的核心價值是,簡化座標系的轉換,即簡化模型座標系到世界座標系的轉換;

二、座標空間

  • 座標空間就是座標系形成的空間


1. 世界空間

世界座標系形成的空間,光線計算一般是在此空間統一進行;

2. 模型空間

模型座標系形成的空間,這裏主要包含模型頂點座標和表面法向量的信息;


第一次變換
模型變換(Model Transforms):就是指從模型空間轉換到世界空間的過程


3. 攝像機空間

攝像機空間

攝像機空間,就是黃色區域所包圍的空間;
攝像機空間在這裏就是透視投影,透視投影用於 3D 圖形顯示,反映真實世界的物體狀態;

透視知識擴展 《透視》


第二次變換
視變換(View Transforms):就是指從世界空間轉換到攝像機空間的過程


  • 攝像機空間,也被稱爲眼睛空間,即可視區域;
  • 其中,LookAt(攝像機的位置) 和 Perspective(攝像機的空間) 都是在調整攝像空間;

4. 裁剪空間

圖形屬於裁剪空間則保留,圖形在裁剪空間外,則剔除(Culled)

攝像機 帶註解

標號(3)[視景體] ,所指的空間即爲裁剪空間,這個空間就由 Left、Right、Top、Bottom、Near、Far 六個面組成的四棱臺,即視景體。

視景體

圖中紫色區域爲視場角

fov & zoom

從而引出,視場縮放爲:

zoom

  • 其次,頂點是用齊次座標表示 {x, y, z, w}, 3D 座標則爲{x/w, y/w, z/w} 而 w 就是判斷圖形是否屬於裁剪空間的關鍵:
錐面關係
Nearz < -w
Farz > w
Bottomy < -w
Topy > w
Leftx < -w
Rightx > w

即座標值,不符合這個範圍的,都會被裁剪掉

座標值範圍
x[-w , w]
y[-w, w]
z[-w, w]

第三次變換
投影變換(Projection Transforms): 當然包括正交、透視投影了,就是指從攝影機空間到視景體空間的變換過程


5. 屏幕空間

它就是顯示設備的物理屏幕所在的座標系形成的空間,它是 2D 的且以像素爲單位,原點在屏幕的幾何中心點

屏幕座標空間. jpg


第四次變換(最後一次)
視口變換(ViewPort Transforms): 指從裁剪空間到屏幕空間的過程,即從 3D 到 2D


這裏主要是關注像素的分佈,即像素縱橫比;因爲圖形要從裁剪空間投影映射到屏幕空間中,需要知道真實的環境的像素分佈情況,不然圖形就會出現變形;

《OpenGL ES 2.0 (iOS)[02]:修復三角形的顯示》這篇文章就是爲了修復屏幕像素比例不是 1 : 1 引起的拉伸問題,而它也就是視中變換中的一個組成部分。

  • 像素縱橫比計算公式

像素縮放比

三、OpenGL ES 2 3D 空間

1. 變換髮生的過程

OpenGL ES 2 變換流程圖

  • 這個過程表明的是 GPU 處理過程(渲染管線);
  • 變換過程發生在,頂點着色與光柵化之間,即圖元裝配階段;
  • 編寫程序的時候,變換的操作是放在頂點着色器中進行處理;
  • 右下角寫明瞭,總共就是四個變換過程:模型變換、視變換、投影變換、視口變換,經過這四個變換後,圖形的點就可以正確並如願地顯示在用戶屏幕上了;
  • 側面反應,要正確地渲染圖形,就要掌握這四種變換;

2. 各個變換流程分解簡述

  • 階段一:追加 w 分量爲 1.0 (第一個藍框)

這個階段不需要程序員操作

這裏的原因是,OpenGL 需要利用齊次座標去進行矩陣的運算,核心原因當然就是方便矩陣做乘法咯(R(4x4) 點乘 R(4x1) 嘛)!

  • 階段二:用戶變換 (第二個藍框)

這個階段需要程序員操作,在 Vertex Shader Code 中進行操作

這個階段主要是把模型正確地通過 3D 變換 (旋轉、縮放、平移) 放置於攝像機的可視區域(視景體)中,包括處理攝像機的位置、攝像機的可視區域佔整個攝像機空間的大小。

這個階段過後,w 就不在是 1.0 了

  • 階段三:重新把齊次座標轉換成 3D 座標 (第三個藍框)

這個階段不需要程序員操作

要重新轉換回來的原因,也很簡單 ---- 齊次座標只是爲了方便做矩陣運算而引入的,而 3D 座標點纔是模型真正需要的點位置信息。

這個階段過後,所有的點座標都會標準化(所謂標準化,就是單位爲 1),x 和 y 值範圍均在 [-1.0, 1.0] 之間,z 就在 [ 0.0, 1.0 ] 之間;

x 和 y 值範圍均在 [-1.0, 1.0] 之間,才能正確顯示,原因是 OpenGL 的正方體值範圍就是 [ -1.0, 1.0 ] 不存在其它範圍的值;而 z 的值範圍是由攝像機決定的,攝像機所處的位置就是 z = 0,的位置,所以 0 是指無限近,攝像機可視區的最遠處就是 z = 1, 所以 1 是指無限遠;

  • 階段四:重新把齊次座標轉換成 3D 座標 (第四個藍框)

* 這個階段需要程序員操作,在圖形渲染前要進行操作,即在 gldraw 前 **

這個階段核心的就是 ViewPort 和 DepthRange 兩個,前者是指視口,後者是深度,分別對應的 OpenGL ES 2 的 API 是:

函數描述
glViewport調整視窗位置和尺寸
glDepthRange調整視景體的 near 和 far 兩個面的位置 (z)
glViewport
void glViewport(GLint x, GLint y, GLsizei w, GLsizei h)
x, y 以渲染的屏幕座標系爲參考的視口原點座標值(如:蘋果的移動設備都是是以左上角爲座標原點)
w, h 要渲染的視口尺寸,單位是像素
glDepthRange
void glDepthRange(GLclampf n, GLclampf f)
n, f n, f 分別指視景體的 near 和 far ,前者的默認值爲 0 ,後者的默認值爲 1.0, 它們的值範圍均爲 [0.0, 1.0], 其實就是 z 值

3. 四次變換與編程應用

  • 下面這兩張圖片就是 Vertex Shader Code 中的最終代碼
#version 100

attribute vec4 v_Position;
uniform mat4 v_Projection, v_ModelView;

attribute vec4 v_Color;
varying mediump vec4 f_color;

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

請看《OpenGL ES 2.0 (iOS)[02]:修復三角形的顯示》 這篇文章,專門講模型變換的。

  • 餘下的幾次變換,都是和攝像機模型在打交道
    攝像機裏面的模型

Camera Model

要完成攝像機正確地顯示模型,要設置攝像機位置、攝像機的焦距:

1. 設置攝像機的位置、方向 --&gt; (視變換) gluLookAt (ES 沒有這個函數),使要渲染的模型位於攝像機可視區域中;【完成圖中 1 和 2】
2. 選擇攝像機的焦距去適應整個可視區域 --&gt; (投影變換) glFrustum(視景體的六個面)、gluPerspective(透視) 、glOrtho(正交)( ES 沒有這三個函數) 【完成圖中 3】
3. 設置圖形的視圖區域,對於 3D 圖形還可以設置 depth- range --&gt; glViewport 、glDepthRange
  • 第二次變換:視變換,世界空間到攝像機空間 ( 2 -> 3 )

上面提到, ES 版本沒有 gluLookAt 這個函數,但是我們知道,這裏做的都是矩陣運算,所以可以自己寫一個功能一樣的矩陣函數即可;

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

Frustum

函數的 eye x、y、z 就是對應圖片中的 Eye at ,即攝像機的位置;
函數的 center x、y、z 就是對應圖片中的 z-axis 可視區域的中心點;
函數的 up x、y、z 就是對應圖片中的 up 指攝像機上下的位置(就是角度);

  • 第三次變換:投影變換,攝像機空間到裁剪空間 ( 3 -> 4 )

view frustum

當模型處於視景體外時會被剔除掉,如果模型有一部分在視景體內時,模型的點信息只會剩下在視景體內的,其它的點信息不渲染;

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

這個是設置視景體六個面的大小的;

  • 透視投影

透視投影

對應的投影公式 :

完整的透視投影公式

使用 GLKit 提供的函數:

/*
Equivalent to gluPerspective.
*/
GLK_INLINE GLKMatrix4 GLKMatrix4MakePerspective(float fovyRadians, // 視場角
                                                float aspect,  // 屏幕像素縱橫比
                                                float nearZ, // 近平面距攝像機位置的距離
                                                float farZ); // 遠平面攝像機位的距離
  • 正交投影

Orthographic projection

對應的投影公式 :

完整的正交投影公式

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

這裏就是設置 glViewPort 和 glDepthRange 當然 2D 圖形不用設置 glDepthRange ;

  • 實際編程過程中的使用過程

  • 第一步,如果是 3D 圖形的渲染,那麼要綁定深度渲染緩存(DepthRenderBuffer),若是 2D 可以跳過,因爲它的頂點信息中沒有 z 信息 ( z 就是頂點座標的深度信息 );

    1. Generate ,請求 depth buffer ,生成相應的內存標識符
    2. Bind,綁定申請的內存標識符
    3. Configure Storage,配置儲存 depth buffer 的尺寸
    4. Attach,裝載 depth buffer 到 Frame Buffer 中
      具體的程序代碼:

  • 第二步,縮寫 Vertex Shader Code
#version 100

attribute vec4 v_Position;
uniform mat4 v_Projection, v_ModelView; // 投影變換、模型視圖變換

attribute vec4 v_Color;
varying mediump vec4 f_color;

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

一般是把四次變換寫成這兩個,當然也可以寫成一個;因爲它們是一矩陣,等同於一個常量,所以使用的是 uniform 變量,變量類型就是 mat4 四乘四方陣(齊次矩陣);

  • 第三步,就是外部程序賦值這兩個變量

* 注意,要在 glUseProgram 函數後,再使用 glUniform 函數來賦值變量,不然是無效的;**

依次完成 模型變換、視變換、投影變換,即可;它們兩兩用矩陣乘法進行連接即可;

如:modelMatrix 點乘 viewMatrix , 它們的結果再與 projectionMatrix 點乘,即爲 ModelViewMatrix ;

GLKit 點乘函數,
GLK_INLINE GLKMatrix4 GLKMatrix4Multiply(GLKMatrix4 matrixLeft, GLKMatrix4 matrixRight);

  • 第四步,如果是 3D 圖形,有 depth buffer ,那麼要清除深度渲染緩存

使用 glClear(GL_DEPTH_BUFFER_BIT); 進行清除,當然之後就是要使能深度測試 glEnable(GL_DEPTH_TEST); 不然圖形會變形;

最好,也使能 glEnable(GL_CULL_FACE); 這裏的意思就是,把在屏幕後面的點剔除掉,就是不渲染;判斷是前還是後,是利用提供的模型頂點信息中點與點依次連接形成的基本圖元的時鐘方向進行判斷的,這個 OpenGL 會自行判斷;

ClockWise & Counterclockwise

左爲順時針,右爲逆時針;

  • 第五步,設置 glViewPort 和 glDepthRange

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


四、工程例子

Github: 《DrawSquare_3DFix》


五、參考書籍

《OpenGL ES 2.0 Programming Guide》
《OpenGL Programming Guide 8th》
《3D 數學基礎:圖形與遊戲開發》
《OpenGL 超級寶典 第五版》
《Learning OpenGL ES For iOS》

目錄

一、目標

1. 基礎知識準備
2. 圖形分析

二、編寫程序

0. 工程結構與整體渲染管線
1. Depth Render Buffer
2. 數據源的編寫與綁定
3. 深度測試與繪製
4. 讓正方體動起來

三、參考書籍、文章


一、目標

正方體. gif

1. 基礎知識準備

a. 渲染管線的基礎知識
《OpenGL ES 2.0 (iOS)[01]: 一步從一個小三角開始》

b. 3D 變換
《OpenGL ES 2.0 (iOS)[04]:座標空間 與 OpenGL ES 2 3D 空間》

2. 圖形分析

a. 它是一個正方體,由六個正方形面組成,有 8 個頂點;

b. 正方體並不是二維圖形,而是三維圖形,即頂點座標應爲 {x, y, z},而且 z 不可能一直爲 0;

c. 若由 OpenGL ES 繪製,z 座標表示深度(depth)信息;

d. 六個面均有不一樣的顏色,即 8 個頂點都帶有顏色信息,即渲染的頂點要提供相應的顏色信息;

e. 六個正方形面,若由 OpenGL ES 繪製,需要由兩個三角面組合而成,即繪製模式爲 GL_TRIANGLE*;

f. 正方體的每一個頂點都包含在三個面中,即一個頂點都會被使用多次,即繪製的時候應該使用 glDrawElements 方法而不是 glDrawArrays 方法,所以除 8 個頂點的數據外還需增加下標數據纔有可能高效地繪製出正方體;

g. 正方體在不斷地旋轉運動,即可能要實時改變頂點的信息並進行重新繪製以達到運動的效果(思路:動圖就是靜態圖的快速連續變化,只要變化的速度大於人眼可以辨別的速度,就會產生自然流暢的動圖)

分析可程序化:
1) 結合 a、b、c、d 四點可以知道,頂點的數據格式可以爲:

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

當然你也可以把 position 和 color 分開來,只不過我認爲放在一起更好管理罷了。

2) 從 e、f 兩點可以知道,增加的數據及繪製的方式:

因爲使用 element 方式,所以增加下標信息;

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

3) 從 g 點可以知道:

圖形的運動,表明圖形在一定時間內不斷地進行更新(重新繪製並渲染),即只要使用具有定時功能的方法即可處理圖形的運動,NSTimer 就可以勝任這個工作,不過 iOS 提供了一個 CADisplayLink 類來專門做定時更新的工作,所以可以選用它進行運動更新;


二、編寫程序

0. 工程結構與整體渲染管線

結構目錄簡述
1) 藍框是包含 CADisplayLink 子類的類,用於更新渲染,就是讓圖形動起來;

2) 紅框就是整體的渲染管線,所有的繪製渲染工作均在此處;

渲染管線 + Depth
Render Buffer 有三種緩存,Color 、Depth 、Stencil 三種;而單純繪製 2D 圖形的時候因爲沒有引入 z 座標(z != 0)而只使用了 Render Buffer 的 Color Render Buffer ;
而如今要進行渲染的正方體,是帶有 z 座標,即深度信息,所以自然要引入 Depth Render Buffer 了;
引入 Depth Render Buffer 並使其工作的步驟:

Depth Render Buffer

ViewController 的程序調度

#import "ViewController.h"

#import "VFGLCubeView.h"

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

@implementation ViewController

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

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

    [_cubeView prepareDisplay];
    [_cubeView drawAndRender];

    [self.view addSubview:_cubeView];

}

- (void)viewDidAppear:(BOOL)animated {

    [super viewDidAppear:animated];

    [self.cubeView update];

}

- (void)viewWillDisappear:(BOOL)animated {

    [super viewWillDisappear:animated];

    [self.cubeView pauseUpdate];

}

@end

內容並不複雜,所以此處不進行贅述;

渲染管線
prepareDisplay + drawAndRender

prepareDisplay 渲染管線的準備部分

- (void)prepareDisplay {

    // 1. Context
    [self settingContext];

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

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

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

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

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

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

    [self linkProgramWithProgramID:_programID];

    [self updateUniformsLocationsWithProgramID:_programID];

    // 4. Attach VBOs
    [self attachCubeVertexArrays];

}

基於這部分,本文的工作在以下兩處進行:

    // 1. Context
    [self settingContext];

它負責確定渲染上下文,以及 Render Buffer 與 Frame Buffer 的資源綁定處理;
[self settingContext]; 詳見 本章 1.Depth Render Buffer 一節

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

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

它是處理頂點緩存數據的;
VBO 與 數據源 詳見 本章 2. 數據源的編寫與綁定

drawAndRender 渲染管線的餘下部分

- (void)drawAndRender {

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

    // 5.1 應用 3D 變換
    self.modelPosition = GLKVector3Make(0, -0.5, -5);
    [self transforms];

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

    // 5.3 開啓深度測試
    [self enableDepthTesting];

    // 5.4 繪製圖形
    [self drawCube];

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

}

基於這部分,本文的工作在此處進行:

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

    // 5.3 開啓深度測試
    [self enableDepthTesting];

    // 5.4 繪製圖形
    [self drawCube];

詳見 本章 3. 深度測試與繪製 一節

關於實時更新的內容

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

詳見 本章 4. 讓正方體動起來

1. Depth Render Buffer

[self settingContext];
它的內容爲:

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

    if (_context != context) {

        [EAGLContext setCurrentContext:_context];

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

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

        _context = context;

        if (context != nil) {

            _context = context;
            [EAGLContext setCurrentContext:_context];

            // 2. Render / Frame Buffer

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

            self.frameBufferID = [self createFrameBuffer];

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

            self.colorRenderBufferID = [self createRenderBuffer];

            [self renderBufferStrogeWithRenderID:self.colorRenderBufferID];

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

            // 2.2 檢查 Frame 裝載 Render Buffer 的問題
            [self checkFrameBufferStatus];

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

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

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

                self.depthRenderBufferID = [self createRenderBuffer];

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

                [self renderBufferStrogeWithRenderID:self.depthRenderBufferID];

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

            }

            // 2.4 檢查 Frame 裝載 Render Buffer 的問題
            [self checkFrameBufferStatus];

        }

    }

}

- (void)settingContext {

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

}

這裏重寫了 setContext: 方法,核心內容是
// 2.3 Add Depth Render Buffer

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

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

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

        self.depthRenderBufferID = [self createRenderBuffer];

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

        [self renderBufferStrogeWithRenderID:self.depthRenderBufferID];

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

    }

步驟分解:

Step One

第一步,創建並綁定深度渲染緩存,對應程序代碼爲:

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

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

    return ID;

}

第二步,存儲新創建的渲染緩存,對應程序代碼爲:

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

    if (renderBufferID == self.colorRenderBufferID) {

        // 必須要在 glbindRenderBuffer 之後 (就是使用 Render Buffer 之後), 再綁定渲染的圖層
        [self bindDrawableObjectToRenderBuffer];

        self.renderBufferSize = [self getRenderBufferSize];

    }

    if (renderBufferID == self.depthRenderBufferID) {

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

    }

}

核心函數:存儲渲染信息

glRenderbufferStorage
void glRenderbufferStorage(GLenum target,GLenum internalformat,GLsizei width, GLsizei height)
target _只能是 GLRENDERBUFFER
internalformat 可用選項見下表
width 渲染緩存的寬度(像素單位)
height 渲染緩存的高度(像素單位)
internalformat存儲格式(位 = bit)
顏色方面GL_RGB565(5 + 6 + 5 = 16 位)、GL_RGBA4(4 x 4 = 16)、GL_RGB5_A1(5 + 5 + 5 + 1 = 16)、GL_RGB8_OES(3 x 8 = 24 )、GL_RGBA8_OES(4 x 8 = 32)
深度方面GL_DEPTH_COMPONENT16(16 位)、GL_DEPTH_COMPONENT24_OES(24 位)、GL_DEPTH_COMPONENT32_OES(32 位)
模板方面GL_STENCIL_INDEX8、GL_STENCIL_INDEX1_OES、GL_STENCIL_INDEX4_OES
深度與模板GL_DEPTH24_STENCIL8_OES

第三步,裝載渲染緩存到幀緩存中,對應程序代碼爲:

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

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, renderBufferID);

}
2. 數據源的編寫與綁定

數據源的書寫
從 2D 到 3D :

右下方,線框正方體的 8 個頂點座標分佈,其實 0~7 的編號是你決定的,也就是說 0 放在那裏開始都是可以的,只要是 8 個點即可;

Cube

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

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

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

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

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

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

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

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

只要你空間想像不是特別差,估計能看出每個點的座標吧!你可以把這樣的點 {1.0, -1.0, -1.0} 改成你喜歡的數值亦可,只要最終是正方體即可;

真正重要的數據其實是下標數據:

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

這些下標的值由兩個因素決定,第一個因素是上面 8 個頂點數據的下標;第二個因素是時鐘方向;

現在看看時鐘方向:

有沒有發現,每一個正方形的兩個小三角,都是逆時針方向的;當然你也可以換成順時針方向,相應的下標數據就要發生改變;

EP: 如 Front 這個面,如果使用順時針來寫數據爲:

    // Front  ------------- 白綠橙藍 中間線(白橙)
    3, 2, 1, // 白綠橙
    1, 0, 2, // 橙藍綠

你也可以從 2 或 1 開始,看你的喜好咯;

方向只有兩個:

資源綁定
這裏主要是 VBO 的數據綁定,增加 Element 的支持而已;

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

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

    // 創建 資源 ( context )
    glBufferData(target,            // 緩存塊 類型
                 size,              // 創建的 緩存塊 尺寸
                 data,              // 要綁定的頂點數據
                 GL_STATIC_DRAW);   // 緩存塊 用途

}

此處不再贅述;
如果實在不懂,請移步至
《OpenGL ES 2.0 (iOS)[03]:熟練圖元繪製,玩轉二維圖形》練習練習;

3. 深度測試與繪製

Step Two

清除舊的深度緩存信息

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

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

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

    glClear(colorBit | depthBit | stencilBit);

}

啓用深度測試

[self enableDepthTesting];
- (void)enableDepthTesting {

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);

}

這裏多了一個 GL_CULL_FACE 的啓用,它的意思就是,把看不見的像素信息剔除掉,只保留能看見的信息(留前去後);
如果沒有啓用 GL_DEPTH_TEST 程序運行後是這樣的:

關掉 GL_DEPTH_TEST.gif

很明顯圖形是有穿透性的,如果去掉 GL_DEPTH_TEST 就不是實體的正方體了;當然如果你喜歡這種效果,也可以關掉 GL_DEPTH_TEST (反正我個人覺得關掉也蠻好看的);

重新綁定 Color Render Buffer
原因,因爲當綁定 Depth Render Buffer 之後,渲染管線從原來的綁定(激活)的 Color Render Buffer 切換成了,綁定(激活)Depth Render Buffer ,從而導致渲染出來的結果,不是期望中的那樣;所以在繪製前要重新綁定(激活)Color Render Buffer .

Step Three

- (void)drawCube {

    // 失敗的核心原因
    // 因爲 depth buffer 是最後一個綁定的,所以當前渲染的 buffer 變成了 depth 而不是 color
    // 所以 渲染的圖形沒有任何變化,無法產生深度效果
    // Make the Color Render Buffer the current buffer for display
    [self rebindRenderBuffer:@[@(self.colorRenderBufferID)]];

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

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

}

這是註釋了代碼中,[self rebindRenderBuffer:@[@(self.colorRenderBufferID)]]; 的運行結果;

4. 讓正方體動起來

ViewController 的調度
其實就是,view 顯示的時候更新,不顯示的時候停止更新;

- (void)viewDidAppear:(BOOL)animated {

    [super viewDidAppear:animated];

    [self.cubeView update];

}

- (void)viewWillDisappear:(BOOL)animated {

    [super viewWillDisappear:animated];

    [self.cubeView pauseUpdate];

}

CubeView 的應用

#pragma mark - DisplayLink Update

- (void)preferTransformsWithTimes:(NSTimeInterval)time {

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

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

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

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

}

本類提供的改變參數有:

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

已經包含了所有的變換操作;

以下的幾個方法均是處理 VFRedisplay 類的實時更新問題;

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

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

}

#pragma mark - Update

- (void)update {

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

}

- (void)pauseUpdate {

    [self.displayUpdate pauseUpdate];

}

#pragma mark - Dealloc

- (void)dealloc {

    [self.displayUpdate endUpdate];

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

核心是 - (void)updateContentsWithTimes:(NSTimeInterval)times 方法,這個方法是用於更新時,實時調用的方法;由VFRedisplay 類提供的協議 @interface VFGLCubeView ()<VFRedisplayDelegate> 方法;

VFRedisplay.h 主要內容

@protocol VFRedisplayDelegate <NSObject>

- (void)updateContentsWithTimes:(NSTimeInterval)times;

@end

......

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

VFRedisplay.m 主要內容
開始更新的方法:

- (void)startUpdate {

    if ( ! self.delegate ) {
        return;
    }

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

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

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

    self.displayPause = kDefaultDisplayPause;

}

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

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

        [self.delegate updateContentsWithTimes:self.updateContentTimes];

    }

}

四步走:
第一步,創建相應的更新調度方法- (void)displayContents:(CADisplayLink *)sender,這個方法必須是- (void)selector:(CADisplayLink *)sender這種類型的;
第二步,指定一個更新頻率(就是一秒更新多少次)frameInterval 一般是 24、25、30,默認是 30 的;
第三步,把 CADisplayLink 的子類添加到當前的 RunLoop [NSRunLoop currentRunLoop] 上,不然程序是無法調度指定的方法的;
第四步,啓動更新 static const BOOL kDefaultDisplayPause = NO; 

displayPause 屬性

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

停止更新的方法:

- (void)pauseUpdate {

    self.displayPause = YES;

}

結束更新的方法:

- (void)endUpdate {

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

}

不用的時候,當然要先停止更新,再關掉時鐘(CADisplayLink 就是一個時鐘類),最後要從當前 RunLoop 中移除;

5. 工程文件

Github: DrawCube

Github:DrawCube_Onestep

增加魔方色開關,RubikCubeColor 宏定義;

開關

數據源

正方體_魔方色. gif


三、參考書籍、文章

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

前言:如果你沒有 OpenGL ES 2 的基礎知識,請先移步 《OpenGL ES 2.0 (iOS) 筆記大綱》 學習一下基礎的知識。

目錄

一、軟件運行效果演示
(一)、最終效果
(二)、信息提取
二、紋理處理的流程【核心】
(一)、Texture 是什麼?
(二)、Texture
(三)、引入了 Texture 的 Shader 文件
(四)、Texture 正確的 “書寫” 順序
三、知識擴充:圖片加載
使用 Quartz Core 的知識加載圖片數據


一、軟件運行效果演示

(一)、最終效果

工程地址:Github

Texture-Base.gif

(二)、信息提取

  1. 不同的模型【2D & 3D】,不同維度下,Texture 的處理區別
  2. 單一像素信息【pixelBuffer】 與 複雜像素信息【圖片】的顯示區別
  3. 正方圖【單張或多張圖片】 與 長方圖,像素的顯示控制區別

二、紋理處理的流程【核心】

(一)、Texture 是什麼?

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

  1. 因爲 OpenGL 就是圖像處理庫,所以 Texture 在 OpenGL 裏面有多重要,可想而知;
  2. 其中間接地鑑明了一點,圖片本身可以有多大變化,OpenGL 就可以有多少種變化。

學好 Texture 非常重要

(二)、Texture

Texture 在 OpenGL 裏面有很多種類,但在 ES 版本中就兩種——Texture_2D + Texture_CubeMap;

Texture_2D: 就是 {x, y} 二維空間下的像素呈現,也就是說,由效果圖上演示可知,很難做到使正方體的六個面出現不同的像素組合;圖片處理一般都使用這個模式;[x 、y 屬於 [0, 1] 這個範圍]

Texture_CubeMap: 就是 {x, y, z} 三維空間下的像素呈現,也就如效果圖中演示的正方體的六個面可以出現不同的像素組合;它一般是用於做環境貼圖——就是製作一個環境,讓 3D 模型如同置身於真實環境中【卡通環境中也行】。[x、y、z 屬於 [-1, 1] 這個範圍,就是與 Vertex Position 的值範圍一致]

注:上面提到的所有座標範圍是指有效渲染範圍,也就是說你如果提供的紋理座標超出了這個範圍也沒有問題,只不過超出的部分就不渲染了;

感受一下怎麼具體表達:

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

Texture_2D:

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

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

};

Texture_CubeMap:

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

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

};

這種座標,是剛好貼合【完全覆蓋】的狀態;

數據特點:一個頂點數據綁定一個紋理數據;

【有沒有注意到,CubeMap 裏面就是直接拷貝頂點數據到紋理座標上,就行了。(CubeMap 中間那個空的 {} 是結構體中的 2D 紋理數據(就是空的))】

其它的數據形態【對於不是正方的圖片】,
【希望大一點,或小一點,即只顯示某一部分】:

都是類似圖中的分割一樣,劃分成多個小圖片【小的 TexCoord】,最終的數據形態是:

static const VYVertex tex2DElongatedDDCubeDatas[] = {

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

};

也可以是沒有填充完整的圖片,只取其中的一部分,數據形態也是上面的:

擴展:
CubeMap 用於做環境貼圖,還需要 Light + Shadow 【光 + 陰影】的知識,爲什麼?環境,有物體 + 自然光 + 人造光 + 光與物體產生的陰影 + 光與物體作用後的顏色;【顏色和陰影是因爲有光才產生的,OpenGL 本身默認有一個全局光,不然你沒有寫光的代碼,爲什麼可以看到你渲染的模型體】
即只有在具備了 光 + 影 的知識,去學習 環境貼圖纔好理解;【貼圖:HDR 圖片 (效果中的那張藍色森林就是 HDR 圖,沒有做 CubeMap) + CubeMap 格式】

CubeMap 圖片格式,就是把下圖中的 HDR 圖片直接轉換成,六個黃色框框的圖像,框框之間的邊緣是連接的哦:

連接

MipMapping: 根據不同的情形加載不同大小的圖片進行渲染;【不同情形,指不同遠近,不同光影環境下對圖片 “看清”“看不清” 的程度,OpenGL 自動選擇合適的圖片大小】【不同大小的圖片,程序員要事先加載一張圖片的不同大小 ( 2^n , 2^m ) 的像素數據(0 ~ n level),又因爲 ES 是基於移動端的,所以內存容易告急,即能不用則不用】

Fliter + 特效 : 我們天天看到的最多的東西,就是給圖片像素加入各種 “想法” 變成你想要的效果【加霧、馬賽克、調色、鏡像、模糊、素描、液化、疊加、藝術化 ......】,它的核心知識在 Fragment Shader【重點】 + OpenGL ES 提供的基礎混合模式【濾波 + Blend】,放在下一篇文章專門講;

粒子系統:Texture + Point Sprites,製作雨水、下雪、飛舞的花瓣...... 只要渲染效果要求有多個相似點在那動來動去的,都可以用它們來實現;【數學中的分形理論好像也可以用上】【粒子,會用專門的一篇文章講】

所有的 “花樣” 特效,不管被稱之爲什麼,都與 數學知識【算法】 和 顏色構成知識【光構成、色彩構成】 密不可分;

所以我就要怕了嗎?
錯,你應該興奮;因爲~~ 反正我也沒有什麼可以失去的了,上來不就是幹了嗎? ^ _ ^ + ~_~ + $-$

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

Texture_2D:

2D Vertex:

#version 100

uniform mat4 u_modelViewMat4;
uniform mat4 u_projectionMat4;

attribute vec4 a_position;
attribute vec2 a_texCoord;

varying highp vec2 v_texCoord;

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

紋理輸入輸出:

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

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

輸入:
vec2 a_texCoord,上面提到過它是 {x, y} 的座標,所以使用的也是 vec2 ;

輸出:
同樣是 vec2 ,但是一定要記住加 highp 精度限定符,不然編譯會報錯哦;

不知道,你是否還記得渲染管線中的 Texture Memory ,看下圖:

渲染管線

紅色框框住的虛線,就是指代 Vertex Shader 中的紋理座標信息;

直接給的,爲什麼是虛線?
看清楚 Shader 代碼,這裏是直接就賦值【輸入 = 輸出,經過其它變換也行】了,也就是 Vertex Shader 內部不需要使用到它,它只是爲了傳到 Fragment 裏面使用的【varying 的作用】,所以就使用虛線來表示;

2D Fragment:

#version 100

uniform sampler2D us2d_texture;

varying highp vec2 v_texCoord;

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

上面的渲染管線圖中,黃色框框住的實線,就是指代 Fragment Shader 中的像素數據【sampler2D】來源;

這裏是核心,輸入輸出:

uniform sampler2D us2d_texture;
...

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

輸入:
sampler2D 就是一堆靜態數據的意思,像素信息就是一堆固定【不管是寫死,還是程序自動生成,都一樣】的顏色信息,所以要使用這種常量塊的類型限定符;

輸出:
這裏要使用 texture2D 內置函數來處理像素信息生成 vec4 的顏色信息,原型 vec4 texture2D(sampler2D s, vec2 texCoord);

所以剩下的問題就是如何得到 sampler2D 數據,並如何將像素數據寫入到 Shader 中

Texture_CubeMap:

#version 100

uniform mat4 u_modelViewMat4;
uniform mat4 u_projectionMat4;

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

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

uniform samplerCube us2d_texture;
varying highp vec3 v_normalCoord;

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

CubeMap 與 2D 的 Fragment 區別並不大,原理一樣的;
CubeMap Vertex ,只要把 vec2 --> vec3 即可;
CubeMap Fragment , 只要把 sampler2D --> samplerCube , texture2D 函數改成 textureCube 即可;

(四)、Texture 正確的 “書寫” 順序

前提,假設基本的渲染管線已經配置完成了,這裏只重點講紋理相關的;

1、 綁定 Texture Coord 紋理座標:

GLuint texCoordAttributeComCount = 2;

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

【如果看不懂,請回去看看第一篇文章,裏面有詳細講】

2、 請求 Texture 內存:

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

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

glGenTextures(GLsizei n, GLuint* textures); 和 glGenBuffers 等的使用是一樣的;它的意思就是,向 GPU 請求一塊 Texture 內存;
glBindTexture (GLenum target, GLuint texture); 和其它的 glBind... 方法一樣;它的意思是,告訴 GPU 請求一塊 target 【只有 2D 和 CubeMap 兩種】 類型的內存,只有當這個方法完成請求後,這塊 Texture 內存纔會生成【如果當前內存標識符指向的內存已經存在,則不會再創建,只會指向此處】;

3、 加載像素數據:

    glUseProgram(programObject);

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

(1)一定要在 glUseProgram 函數後進行這個步驟,爲什麼?
因爲 Fragment 使用的是 uniform samplerXXX 的數據,uniform 常量數據要在 glUseProgram 後再加載纔有效,而且它的內存標識符【內存】要在 link Program 之後 OpenGL 纔會分配;

(2)進入 setTextureWithProgram: texture: texMode:方法
先準備像素數據【pixelsDatas 或 ImageDatas】:
這裏的是,Pixels 的數據,就是寫死的數據

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

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

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

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

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

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

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

因爲 Texture_2D 狀態下,只有 {x, y} 平面的數據需要填充,所以這裏就只有一個面的顏色數據;

而在 Texture_CubeMap 狀態下,是 {x, y, z} 三維座標,即六個面需要填充,所以就是6 * 1(1 = 2 * 2) = 6個面的顏色數據;

注:圖片類型的數據要自己寫轉換方法,生成像素數據;當然也可以使用 GLKit 提供的 TextureLoder 類來加載圖片像素數據;

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

glTexImage2D
void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid* pixels)
target _指 如果是 2D ,就是 GL_Texture_2D,如果是 CubeMap 就是 GL_TEXTURE_CUBE_MAPXXX [+-x, +-y, +-z, 六個面]
level 指 mipmapping level 沒有做 mipmapping 則爲 0 ;如果做了,則爲 0 ~ levelMax [這個 max 是由你自己圖片數據決定的]
internalformat _指 像素數據的格式是什麼 GLRGB 等等
width 指 一塊像素的寬 [2D 下只有一塊,cubemap 會有多塊(六個面)]
height 指 一塊像素的高
border _指 ES 下是 GLFALSE
format 指 與 internalformat 格式一致
type _指 像素數據存儲的類型,如:GL_FLOAT, GL_UNSIGNEDBYTE
pixels 指 一塊像素的內存首地址

a. 像素模式下的使用:

if (texMode == GL_TEXTURE_2D) {
    glTexImage2D(texMode, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT, tex2DPixelDatas);
} else {

//                glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[0]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[1]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[2]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[3]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[4]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[5]);

    GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X;
    for (NSUInteger i = 0; i < 6; i++) {
        glTexImage2D(target, 0, GL_RGB,
                     2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[i]);
        target++;
    }

}

上面在 GL_TEXTURE_2D 狀態下的加載,只要理解了glTexImage2D函數參數的意思,也就會使用且明白了,這裏就不再贅述了;

特別要注意的是在 GL_Texture_Cube_Map 狀態下的使用,一定要六個面都進行像素數據加載;

#define GL_TEXTURE_CUBE_MAP_POSITIVE_X                   0x8515
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X                   0x8516
#define GL_TEXTURE_CUBE_MAP_POSITIVE_Y                   0x8517
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y                   0x8518
#define GL_TEXTURE_CUBE_MAP_POSITIVE_Z                   0x8519
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z                   0x851A

看看GL_TEXTURE_CUBE_MAP_POSITIVE_X它們的定義,因爲定義是連續的,所以我們纔可以用 for 循環來 “偷懶”;

b. 圖片像素模式下的使用:

if (texMode == GL_TEXTURE_2D) {

    UIImage *img = // img;

    [self.loadTexture textureDataWithResizedCGImageBytes:img.CGImage completion:^(NSData *imageData, size_t newWidth, size_t newHeight) {
        glTexImage2D(texMode, 0, GL_RGBA,
                     (GLsizei)newWidth, (GLsizei)newHeight,
                     GL_FALSE, GL_RGBA, GL_UNSIGNED_BYTE,
                     imageData.bytes);
    }];

} else {

    NSArray<UIImage *> *imgs = // imgs;

    GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X;
    [self.loadTexture textureDatasWithResizedUIImages:imgs completion:^(NSArray<NSData *> *imageDatas, size_t newWidth, size_t newHeight) {
        [imageDatas enumerateObjectsUsingBlock:^(NSData * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            glTexImage2D((GLenum)(target + idx), 0, GL_RGBA,
                         (GLsizei)newWidth, (GLsizei)newHeight,
                         GL_FALSE, GL_RGBA, GL_UNSIGNED_BYTE,
                         obj.bytes);
        }];
    }];

}

這裏的核心就是,self.loadTexture 的圖片加載方法,這是自己寫的加載方法,使用的技術是 Quartz Core ;具體的在下一節【三、知識擴充:圖片加載】會講到;

兩者的使用並不會有什麼區別,這只是兩種像素數據提供的方式不同罷了

(4)指定濾波設置【下一篇會重點講】 + 像素綁定 + 激活紋理

glTexParameteri(texMode, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(texMode, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

GLuint textureSourceLoc = glGetUniformLocation(programObject, "us2d_texture");
glUniform1i(textureSourceLoc, 0);

glEnable(texMode);
glActiveTexture(GL_TEXTURE0);
glBindTexture(texMode, texObj);

a. 設置濾波模式,函數就是glTexparameteri方法
原型:glTexParameteri (GLenum target, GLenum pname, GLint param);

ES 2 有四個這種濾波函數,下圖的參數已經說得很明白了,我就不一一解釋了:

MIN / MAG ?:

magnification【MAG】:放大的意思,指顯示在屏幕上的一個像素是紋理像素放大後的結果;
【只有 x、y 方向都進行放大,才需要這個參數,也就是說它是可選的】

minification【MIN】: 縮小的意思,指顯示在屏幕上的一個像素是一個紋理像素集縮小後的結果;
【一定要做的設置,如上述代碼中的glTexParameteri(xxx, GL_TEXTURE_MIN_FILTER, xxx);
【MipMapping 發揮作用的地方就是在縮小的時候,OpenGL 會自動選擇合適大小的像素數據】

如果紋理像素在 x、y 方向上是做同一個動作【拉伸或壓縮】,則需要放大或縮小像素;如果紋理像素在 x、y 方向上是做不同的動作,則需要放大或者縮小,不確定【由 OpenGL 自己選擇】;

WRAP_S / WRAP_T ? : 就是 x 或 y 方向填充覆蓋的意思;

LINEAR / NEAREST ? :

前者是指啓用線性濾波【就是平滑過渡】,後者是禁用線性濾波;

平滑過濾使用的技術——信號採樣,先看看一維的信號採樣:

意思就是,採樣提供的紋理像素,在放大、縮小的時候,使相鄰的像素進行 “一定程度的融合” 產生新的像素信息,使最終顯示在屏幕在的圖片更加平滑;上圖【猴子】中的效果就是利用這項技術來的,對於二維、三維,就相應地做多次採樣【二維,兩次;三維,三次......】;

b. 像素綁定【就是告訴 GPU Shader 的像素數據在那】+ 激活紋理

GLuint textureSourceLoc = glGetUniformLocation(programObject, 
                                               "us2d_texture");
glUniform1i(textureSourceLoc, 0);

glEnable(texMode);
glActiveTexture(GL_TEXTURE0);

glBindTexture(texMode, texObj);

glUniform1i類函數,可以理解成綁定一塊內存【像素塊內存】,也可以理解成綁定一個內存空間【一般常量】;
函數原型:void glUniform1i(GLint location, GLint x)

glEnable函數,就是打開一些什麼東西,這裏是打開 GL_TEXTURE_XXX ,不寫也行,這裏和其它地方的默認一樣, 0 這個位置的紋理就是打開的;【爲了良好習慣,還是寫吧】

glActiveTexture函數,名字已經告訴是激活紋理的意思,不用多說了;

重點:glUniform1i 的第二個參數是和 glActiveTexture 的第二個參數是對應的,前者使用的是 0,那麼後者就是對應 GL_TEXTURE0 【0~31,共 32 個】,依此類推

爲什麼還要做glBindTexture(texMode, texObj);重新綁定像素內存,其實就是防止中途有什麼地方把它給改了【如,bind 了其它的紋理】,所以是爲了保險起見,就最好寫上;但是因爲這裏很明顯地,只有 layoutSubviews 函數【此渲染代碼都是寫在這個函數內運行的】會綁定它,而且都是同一個的,所以也可以不寫;


三、知識擴充:圖片加載

使用 Quartz Core 技術 加載圖片數據,Bitmap Context :

本來它不屬於 OpenGL 的內容,但是它本身也是圖像處理的技術,包括 Core Image、 Accelerate 等圖像處理的框架,如果可以,請儘量去了解或去掌握或去熟練。

核心代碼:

#define kBitsPerComponent   8

#define kBytesPerPixels     4
#define kBytesPerRow(width)         ((width) * kBytesPerPixels)

- (NSData *)textureDataWithResizedCGImageBytes:(CGImageRef)cgImage
                                      widthPtr:(size_t *)widthPtr
                                     heightPtr:(size_t *)heightPtr {

    if (cgImage == nil) {
        NSLog(@"Error: CGImage 不能是 nil ! ");
        return [NSData data];
    }

    if (widthPtr == NULL || heightPtr == NULL) {
        NSLog(@"Error: 寬度或高度不能爲空。");
        return [NSData data];
    }

    size_t originalWidth  = CGImageGetWidth(cgImage);
    size_t originalHeight = CGImageGetHeight(cgImage);

    // Calculate the width and height of the new texture buffer
    // The new texture buffer will have power of 2 dimensions.
    size_t width  = [self aspectSizeWithDataDimension:originalWidth];
    size_t height = [self aspectSizeWithDataDimension:originalHeight];

    // Allocate sufficient storage for RGBA pixel color data with
    // the power of 2 sizes specified
    NSMutableData *imageData =
    [NSMutableData dataWithLength:height * width * kBytesPerPixels]; // 4 bytes per RGBA pixel

    // Create a Core Graphics context that draws into the
    // allocated bytes
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef cgContext = CGBitmapContextCreate([imageData mutableBytes],
                                                   width, height,
                                                   kBitsPerComponent,
                                                   kBytesPerRow(width),
                                                   colorSpace,
                                                   kCGImageAlphaPremultipliedLast); // RGBA
    CGColorSpaceRelease(colorSpace);
    // Flip the Core Graphics Y-axis for future drawing
    CGContextTranslateCTM (cgContext, 0, height);
    CGContextScaleCTM (cgContext, 1.0, -1.0);
    // Draw the loaded image into the Core Graphics context
    // resizing as necessary
    CGContextDrawImage(cgContext, CGRectMake(0, 0, width, height), cgImage);
    CGContextRelease(cgContext);

    *widthPtr  = width;
    *heightPtr = height;

    return imageData;
}

主流程:
1、規格化圖片尺寸,讓其符合 (2^n, 2^m)[n,m 均爲自然數]
爲什麼?
(1)因爲 CGBitmapContextCreate支持的是 size_t ((long) unsigned int) 的【來個 0.25 個像素也是醉了】;
(2)而且 OpenGL ES 支持的最大像素尺寸也是有限制的,當前環境支持的最大值是 (4096, 4096),這個值由以下兩個 xx_MAX_xx 得到【就在 aspectSizeWithDataDimension: 方法內】:

    GLint _2dTextureSize;
    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &_2dTextureSize);

    GLint cubeMapTextureSize;
    glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &cubeMapTextureSize);

glGetIntegerv函數是可以獲取當前環境下所有的默認常量的方法;

2、確定圖片像素最終輸出的顏色空間
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();,這個最容易出錯,它的顏色格式要和你使用glTexImage2D函數指名的顏色格式要一致,不然不可能顯示正常【如,你這裏定義成 CYMK, 指名了 GL_RGB 那麼肯定不對的】

3、確定最終像素的位深與位數
這裏是明確用多少位來表示一個像素位【如:R 用 8 位表示】,一個像素由多少個成員組成【如:RGBA 就是 4 個】

4、創建上下文環境
Bitmap 圖就是像素圖,包含所有的像素信息,沒有什麼 jpg / png 容器什麼的;
CGBitmapContextCreate函數的各個參數都很明顯了,所以就不廢話了;

5、變換像素的座標空間
爲什麼?
Texture 紋理座標空間的座標原點在,左下角,而蘋果設備顯示的圖形的座標系的座標原點在左上角,剛好是反的;

6、繪製生成最終的像素數據


謝謝看完,如果有描述不清或講述錯誤的地方,請評論指出!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章