目錄結構:
第一步,明確要幹嘛
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 Shader 和 Fragment Shader
7) 連結 Shader Program
五、渲染繪製
1) 清空舊渲染快取
2) 設定渲染視窗
3) 使用 Shder Program
4) **關聯資料**
5) 繪製圖形
c.物件導向的重新設計
複製程式碼
第四步,練練手
a.修改背景色
b.修改三角形的填充色
c.修改三角形的三個頂點的顏色(填充色)
複製程式碼
第一步,明確要幹嘛
1. 目標:
使用 OpenGL ES 2.0 在 iOS 模擬器中繪製一個三角形。
2. 效果:
3. 分析圖形:
-
背景顏色是藍色
--> 修改背景顏色 -
直角三角形
--> 繪製三角形
4.繪製三角形?三角形由什麼組成?
--> 三個端點 + 三條線 + 中間的填充色,即三個點連成線形成一個三角面。
1). 三個什麼端點(螢幕座標點)?
要回答這個問題要先了解 OpenGL ES 的座標系在螢幕上是怎樣分佈的:
注:圖片截自 《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)三維軟體繪製
二維就是長這樣的了:
- 三條線?
a.連線三個端點形成封閉的三角面,那麼 OpenGL ES 能不能直接繪製三角形 ?
--> 答案是能。
b.那麼 OpenGL 能直接畫正方形麼?
--> 答案是不能。
c.那OpenGL 能直接繪製什麼?
--> 答案是:點精靈、線、三角形,它們統稱為 圖元(Primitive)。
注:答案來自於《OpenGL ES 2.0 Programming Guide》 7. Primitive Assembly and Rasterization 一章,截圖如下:
- 線元
Line Strip , 指首尾相接的線段,第一條線和最後一條線沒有連線在一起;
Line Loops, 指首尾相接的線段,第一條線和最後一條線連線在一起,即閉合的曲線;
- 三角圖元
Triangle Strip, 指條帶,相互連線的三角形 Triangle Fan, 指扇面,相互連線的三角形
- 點精靈 【主要應用在 紋理 方面】
3)填充色?
就是指 RGBA 的顏色值;( ^_^ 感覺好廢但還是要說)
第二步,怎麼去畫(純理論)
怎麼去畫,就是通過多少個步驟完成一個完整的繪製渲染流程,當然這裡指 OpenGL ES 2 的渲染管線流程)
OpenGL ES 2 的渲染管線
因為這裡是 iOS 端的圖,所以重新繪製了一下:
注:此圖根據 《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 :
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 :
- __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 方式訪問】)】
繪製的三角形的陣列(三個頂(端)點座標)如下圖:
這是 C 語言的知識,應該不難理解。
- __ 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 一節中
- __輸入訊號:__Attributes、Uniforms、Samplers (optional)
a. Attributes : 屬性的意思,指每一個頂點資料;
b. __Uniforms : __
b-1. 統一的意思 , 是一個只讀全域性常量,儲存在程式的常量區;
b-2. 當 Vertex Shader 和 Fragment Shader 定義了同名同型別的 Uniform 常量時,此時的 Uniform 常量就變成了全域性常量(指向同一塊記憶體區的常量);
c. __Samplers (可選的) : __ 是一個特殊的 Uniforms 儲存的是 Texteures(紋理) 資料;
- 輸出訊號: Varying
__Varying : __
a. 它是 Vertex Shader 與 Fragment Shader 的介面,是為了解決功能性問題(兩個 Shader 的資訊互動);
b. 儲存 Vertex Shader 的輸出資訊;
c. Vertex Shader 與 Fragment Shader 中必須要有必須要同名同型別的Varying 變數,不然會編譯錯誤;(因為它是兩個 Shader 的資訊介面啊,不一樣還接什麼口啊。)
- 互動資訊: Temporary Variables
Temporary Variables :
a. 指臨時變數;
b. 儲存 Shader 處理過程中的中間值用的;
c. 宣告在 Funtions(函式) 或 Variable(變數) 內部;
- 輸出的內建變數: 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 (圖元裝配) :
-
第一步,把 Vertex Shader 處理後的頂點資料組織成 OpenGL ES 可以直接渲染的基本圖元:點、線、三角形;
-
第二步,裁剪 ( Clipping ) ,只保留在渲染區域(視錐體,視覺區域)內的圖元;
-
第二步,剔除 ( Culling ),可通過程式設計決定剔除前面、後面、還是全部;
注: 視錐體,實際上是一個三維錐體包含的空間區域,由攝影機和物體的捕捉關係形成;
圖片來源 《透視投影詳解》一文
5. Rasterization ( 光柵化 ) :
光柵化的訊號圖:
作用是,將基本圖元(點、線、三角形)轉換成二維的片元(Fragment, 包含二維座標、顏色值、紋理座標等等屬性), 畫素化基本圖元使其可以在螢幕上進行繪製(顯示)。
6. __Texture Memory ( 紋理記憶體 ) : __
Texture 就是指儲存了圖片(點陣圖)的所有顏色的快取;Texture Memory 就是圖片的顏色(畫素)記憶體;每一個嵌入式系統對 Texture Memory 的大小都是有限制的;
-
完整的 iOS 渲染繪製管線圖中,向上指向 Vertex Shader 的虛線,意指 Texture Coordinate (紋理座標)資訊是通過程式提供給它的;
-
完整的 iOS 渲染繪製管線圖中,指向 Fragment Shader 的實線,因為 Fragment Shader 處理的是光柵化後的資料,即畫素資料,而 Texture 本身就是畫素資料,所以 Texture Memory 可以直接當成 Fragment Shader 的輸入;
7. Fragment Shader ( 片元著色器 ) :
片元著色器訊號圖:
-
輸入訊號: Varying、Uniforms、Samples
與 Vertex Shader 的輸入是同一個意思,具體請檢視 Vertex Shader 處的解釋~~~; -
輸入的內建變數 :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 處的解釋;
- 輸出訊號 (內建變數) : 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 : __
訊號圖:
-
Pixel ownership test ( 畫素歸屬測試 ) :
判斷畫素在 Framebuffer 中的位置是不是為當前 OpenGL ES Context 所有,即測試某個畫素是否屬於當前的 Context 或是否被展示(是否被使用者可見); -
Scissor Test ( 裁剪測試 ) :
判斷畫素是否在由 glScissor* 定義的裁剪區域內,不在該剪裁區域內的畫素就會被丟棄掉; -
Stencil Test ( 模版測試 ):
將模版快取中的值與一個參考值進行比較,從而進行相應的處理; -
Depth Test ( 深度測試 ) :
比較下一個片段與幀緩衝區中的片段的深度,從而決定哪一個畫素在前面,哪一個畫素被遮擋; -
Blending ( 混合 ) :
將片段的顏色和幀快取中已有的顏色值進行混合,並將混合所得的新值寫入幀快取 (FrameBuffer) ; -
Dithering ( 抖動 ) :
使用有限的色彩讓你看到比實際圖象更為豐富的色彩顯示方式,以緩解表示顏色的值的精度不夠大而導致顏色劇變的問題。
9. Render Buffer & Frame Buffer:
關係圖:
- __Render Buffer ( 渲染快取 ) : __
a. 簡稱 RBO , Render Buffer Object; b. 是由程式(Application)分配的 2D 圖片快取; c. Render Buffer 可以分配和儲存顏色(color)、深度(depth)、模版(stectil)值,也可以把這三種值裝載到 Frame Buffer 裡面;
- __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 端的渲染視窗寄宿層;
【 看這裡:
-
EGL API 設計出來的目的就是為了在 OpenGL ES 2 能在視窗系統 (螢幕 ,iOS 是 CAEAGLLayer 類為寄宿層的 View)進行渲染繪製;
-
可以進行 EGL 渲染的前提是:
a. 可以進行顯示的裝置( iOS 下當然是手機或模擬器 )
b. 建立渲染面(rendering surface), 裝置的螢幕 ( on-screen ) 或 畫素快取 ( pixel Buffer ) ( off-screen )
注: pixel Buffer , 這種 buffer 是不能直接顯示的,只能成為渲染面或通過其它 API 分享出去,如: pbuffers 經常被用於 Texture 的 maps , 因為 Texture 本身也是畫素嘛;
- 建立渲染上下文 ( 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 類;
3. OpenGL 本身是 C Base 的語言,可以適應多個平臺,而在 iOS 下的封裝就是 GLKit ;
4. GLSL ES (也稱 ESSL ) ?
簡單流程圖:
- 編寫 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 等);
- 除編寫 Shader Code 外,其它的流程都由一個對應的 GLSL ES 的 API (函式)進行相應的操作;
注:此處只是做了一個 Program 的圖,不是隻能有一個 Program,而是可以有多個,需要使用多少個,由具體專案決定。
第三步,怎麼去畫(實戰)
以本文的小三角為例,開始浪吧~~~!
OpenGL ES 2 的渲染流程 實際繪製環境,流程細化
1.配置環境:
-
主要工作是,EAGL API 的設定。
-
核心操作:
a. CAEAGLLayer 替換預設的 CALayer,配置繪製屬性;
b. EAGLContext,即 Render Context ,設定成** OpenGL ES 2 API ** 環境,並使其成為當前活躍的上下文環境;
c. Frame Buffers / Render Buffer 的建立和使用,以及內容繫結;
d. **EAGLContext 繫結渲染的視窗 (on-screen),CAEAGLLayer ** ;
擴充套件:
** CAEAGLLayer **
- 繼承鏈: CALayer有的,當然 CAEAGLLayer 也有;
- 作用:
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 屬性來改變渲染屬性。
- 使用注意:
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
- 這裡的核心工作是 Shader Code ,即學習 GLSL ES 語言;
- iOS 端採用 glsl 字尾的檔案來編寫程式碼;
4. 渲染繪製
- 這裡要注意的是 清空舊快取、設定視窗,雖然只是一句程式碼的問題,但還是很重要的;
- 核心是學習 glDraw* 繪製 API ;
流程程式碼化
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;
}
複製程式碼
- 配置渲染上下文
// 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];
}
複製程式碼
- 配置幀渲染
- (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 只能填 GL_FRAMEBUFFER | |
framebuffer 指 Frame Buffer 物件的識別符號 |
- 配置渲染快取
- (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 只能填 GL_RENDERBUFFER | |
renderbuffers 指 Render Buffer 物件的識別符號 |
- 幀快取裝載渲染快取的內容
- (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 只能填 GL_FRAMEBUFFER | |
attachment *只能是三個中的一個:GL_COLOR_ATTACHMENT0 ( 顏色快取 )、GL_DEPTH_ATTACHMENT ( 深度快取 )、GL_STENCIL_ATTACHMENT ( 模板快取 ) * | |
renderbuffertarget 只能填 GL_RENDERBUFFER | |
renderbuffer 指 Render Buffer 物件的識別符號,而且當前的 Render Buffer 物件一定要是可用的 |
- 渲染上下文繫結渲染視窗(圖層)
- (void)bindDrawableObjectToRenderBuffer {
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
}
複製程式碼
函式 | 描述 |
---|---|
renderbufferStorage: fromDrawable: | 關聯 當前渲染上下文和渲染視窗 |
renderbufferStorage: fromDrawable: | |
---|---|
- (BOOL)renderbufferStorage:(NSUInteger)target fromDrawable:(id)drawable | |
target 只能填 GL_RENDERBUFFER | |
drawable *只能是 CAEAGLLayer 物件 * |
函式解釋:
- 為了使建立的 Render Buffer 的內容可以顯示在螢幕上,要使用這個函式繫結 Render Buffer 而且分配共享記憶體;
- 要顯示 Render Buffer 的內容, 就要使用 presentRenderbuffer:來顯示內容;
- 這個函式的功能等同於 OpenGL ES 中的它【內容太多,簡書不好排版】
函式 | 描述 |
---|---|
glRenderbufferStorage | 儲存渲染快取內容 |
glRenderbufferStorage | |
---|---|
void glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height) | |
target 只能填 GL_RENDERBUFFER | |
internalformat *分三種 color render buffer、 depth render buffer、stencil render buffer * | |
width *畫素單位,大小必須 <= GL_MAX_RENDERBUFFER_SIZE * | |
height *畫素單位,大小必須 <= GL_MAX_RENDERBUFFER_SIZE * |
internalformat | 值 |
---|---|
color render buffer [01] | GL_RGB565, GL_RGBA4, GL_RGB5_A1, |
color render buffer [02] | GL_RGB8_OES, GL_RGBA8_OES |
depth render buffer [01] | GL_DEPTH_COMPONENT16, |
depth render buffer [02] | GL_DEPTH_COMPONENT24_OES, GL_DEPTH_COMPONENT32_OE |
stencil render buffer | GL_STENCIL_INDEX8, GL_STENCIL_INDEX4_OES, GL_STENCIL_INDEX1_OE |
2.修改背景色
typedef struct {
CGFloat red;
CGFloat green;
CGFloat blue;
CGFloat alpha;
} RGBAColor;
static inline RGBAColor RGBAColorMake(CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) {
RGBAColor color = {
.red = red,
.green = green,
.blue = blue,
.alpha = alpha,
};
return color;
}
- (void)setRenderBackgroundColor:(RGBAColor)color {
glClearColor(color.red, color.green, color.blue, color.alpha);
}
複製程式碼
函式 | 描述 |
---|---|
glClearColor | 清空 Render Buffer 的 Color Render Buffer 為 RGBA 顏色 |
glClearColor | |
---|---|
void glClearColor (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); | |
red 指 [0, 1] 的紅色值 | |
green 指 [0, 1] 的綠色值 | |
blue 指 [0, 1] 的藍色值 | |
alpha 指 [0, 1] 的透明度值 |
注: 不想定義 RGBAColor 的話,可以直接使用 GLKit 提供的 GLKVector4 ,原型是
#if defined(__STRICT_ANSI__)
struct _GLKVector4
{
float v[4];
} __attribute__((aligned(16)));
typedef struct _GLKVector4 GLKVector4;
#else
union _GLKVector4
{
struct { float x, y, z, w; };
struct { float r, g, b, a; }; // 在這呢......
struct { float s, t, p, q; };
float v[4];
} __attribute__((aligned(16)));
typedef union _GLKVector4 GLKVector4; // 是一個共用體
#endif
複製程式碼
GLK_INLINE GLKVector4 GLKVector4Make(float x, float y, float z, float w)
{
GLKVector4 v = { x, y, z, w };
return v;
}
複製程式碼
3. 初始化資料
如果要使用 VBOs 最好在這裡建立 VBOs
物件並繫結頂點資料,當然直接在關聯資料一步做也沒問題;
#define VertexBufferMemoryBlock (1)
- (GLuint)createVBO {
GLuint vertexBufferID;
glGenBuffers(VertexBufferMemoryBlock, &vertexBufferID);
return vertexBufferID;
}
#define PositionCoordinateCount (3)
typedef struct {
GLfloat position[PositionCoordinateCount];
} VFVertex;
static const VFVertex vertices[] = {
{{-0.5f, -0.5f, 0.0}}, // lower left corner
{{ 0.5f, -0.5f, 0.0}}, // lower right corner
{{-0.5f, 0.5f, 0.0}}, // upper left corner
};
- (void)bindVertexDatasWithVertexBufferID:(GLuint)vertexBufferID {
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
// 建立 資源 ( context )
glBufferData(GL_ARRAY_BUFFER, // 快取塊 型別
sizeof(vertices), // 建立的 快取塊 尺寸
vertices, // 要繫結的頂點資料
GL_STATIC_DRAW); // 快取塊 用途
}
複製程式碼
函式 | 描述 |
---|---|
glGenBuffers | 申請 VBOs 物件記憶體 |
glBindBuffer | 繫結 VBOs 物件 |
glBufferData | 關聯頂點資料,並建立記憶體 |
glGenBuffers | |
---|---|
void glGenBuffers (GLsizei n, GLuint * buffers) | |
n *指返回多少個 VBO * | |
buffers 指 VBO 的識別符號記憶體地址 |
glBindBuffer | |
---|---|
void glBindBuffer (GLenum target, GLuint buffer) | |
target 可以使用 GL_ARRAY_BUFFER 或 GL_ELEMENT_ARRAY_BUFFER | |
buffer 指 VBO 的識別符號 |
glBufferData | |
---|---|
void glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage) | |
target 可以使用 GL_ARRAY_BUFFER 或 GL_ELEMENT_ARRAY_BUFFER | |
size 位元組單位,資料在記憶體中的大小(sizeof(...)) | |
data 頂點資料的記憶體指標 | |
usage 告訴程式怎麼去使用這些頂點資料 |
usage | 值 |
---|---|
GL_STATIC_DRAW | 程式只指定一次記憶體物件的資料(頂點資料),而且資料會被多次(非常頻繁地)用於繪製圖元。 |
GL_DYNAMIC_DRAW | 程式不斷地指定記憶體物件的資料(頂點資料),而且資料會被多次(非常頻繁地)用於繪製圖元。 |
GL_STREAM_DRAW | 程式只指定一次記憶體物件的資料(頂點資料),而且資料會被數次(不確定幾次)用於繪製圖元。 |
glGenBuffers 、glBindBuffer、glBufferData 都幹了什麼?
-
glGenBuffers 會在 OpenGL ES Context ( GPU )裡面,申請一塊指定大小的記憶體區;
-
glBindBuffer 會把剛才申請的那一塊記憶體宣告為 GL_ARRAY_BUFFER ,就是以什麼型別的記憶體來使用;
-
glBufferData 把存放在程式記憶體的頂點資料 ( CPU 記憶體 ) 關聯到剛才申請的記憶體區中;
注: 圖片截自, RW. Beginning. OpenGL ES.and.GLKit Tutorials 教程;圖片中的 “~~ 3) 拷貝頂點資料~~ ” 更正為 “ 3) 關聯頂點資料 ”, 因為從 CPU 拷貝資料到 GPU 是在 OpenGL ES 觸發繪製方法(後面會進到)的時候才會進行;
4. 配置 OpenGL ES Shader
- 編寫 Vertex Shader Code 檔案
a. 這是檔案形式的,建議使用這種, Xcode 會進行關鍵字提示
#version 100
attribute vec4 v_Position;
void main(void) {
gl_Position = v_Position;
}
複製程式碼
b. 這是直接 GLchar * 字串形式
+ (GLchar *)vertexShaderCode {
return "#version 100 \n"
"attribute vec4 v_Position; \n"
"void main(void) { \n"
"gl_Position = v_Position;\n"
"}";
}
複製程式碼
非常明顯地看出,a 不管編寫和閱讀都很輕鬆,而 b 就是一堆紅,不知道是什麼鬼,看久了眼睛會很累;
【 程式碼解釋:
a. #version 100 ,首先 OpenGL ES 2 使用的 GLSL ES 版本是100, 這個沒什麼好解釋的。《OpenGL ES 2 programming Guide》有提及
b. attribute vec4 v_Position;,
b-1. attribute 儲存型別限定符,表示連結,連結 OpenGL ES 的每一個頂點資料到頂點著色器(一個一個地);
注:
- attribute 只能定義 float, vec2, vec3, vec4, mat2, mat3,mat4 這幾種型別的變數,不能是結構體或陣列;
- 只能用在頂點著色器中,不能在片元著色器中使用,不然會編譯錯誤;
補充:其它的儲存型別限定符
限定符 | 描述 |
---|---|
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 頂點資料;- 編寫 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"
"}";
}
複製程式碼
- 配置 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_FRAGMENT_SHADER中的一個 | |
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_SHADER_TYPE 五種 | |
params 用於儲存當前獲取資訊的變數記憶體地址 |
glGetShaderInfoLog | |
---|---|
void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsei** *length, GLchar *infoLog) | |
shader 著色器的記憶體識別符號 | |
maxLength 指最大的資訊長度 | |
length *獲取的資訊長度,如果不知道可以是 NULL * | |
infoLog 儲存資訊的變數的記憶體地址 |
-
配置 Fragment Shader 與 3) 方法一樣;
-
建立 Shader Program
- (GLuint)createShaderProgram {
return glCreateProgram();
}
複製程式碼
函式 | 描述 |
---|---|
glCreateProgram | 建立 Shader Program 物件 |
glCreateProgram | |
---|---|
GLuint glCreateProgram() | |
return GLuint 返回著色器程式的識別符號 |
- 裝載 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 要裝載的著色器物件識別符號 |
- 連結 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_VALIDATE_STATUS | |
params 儲存資訊的變數的記憶體地址 |
glGetProgramInfoLog | |
---|---|
void glGetProgramInfoLog(GLuint program,GLsizei maxLength, GLsizei** *length, GLchar *infoLog) | |
program 著色器程式的識別符號 | |
maxLength 指最大的資訊長度 | |
length *獲取的資訊長度,如果不知道可以是 NULL * | |
infoLog 儲存資訊的變數的記憶體地址 |
5.渲染繪製
- 清空舊渲染快取
- (void)clearRenderBuffer {
glClear(GL_COLOR_BUFFER_BIT);
}
複製程式碼
函式 | 描述 |
---|---|
glClear | 清空 渲染快取的舊內容 |
glClear | |
---|---|
void glClear (GLbitfield mask) | |
mask 三者中的一個GL_COLOR_BUFFER_BIT ( 顏色快取 ),GL_DEPTH_BUFFER_BIT ( 深度快取 ), GL_STENCIL_BUFFER_BIT ( 模板快取 ) |
- 設定渲染視窗
- (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 |
- 使用 Shder Program
- (void)userShaderWithProgramID:(GLuint)programID {
glUseProgram(programID);
}
複製程式碼
函式 | 描述 |
---|---|
glUseProgram | 使用 Shader Program |
glUseProgram | |
---|---|
void glUseProgram(GLuint program) | |
program 著色器程式的識別符號 |
- 關聯資料
#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_VERTEX_ATTRIBS - 1] |
b. 關聯頂點資料
glVertexAttribPointer | |
---|---|
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *ptr) |
|
index attribute 變數的下標,範圍是[ 0, GL_MAX_VERTEX_ATTRIBS - 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_FLOAT_OES * | |
normalized *指是否進行資料型別轉換的意思,GL_TRUE 或 GL_FALSE * | |
stride *指每一個資料在記憶體中的偏移量,如果填 0(零) 就是每一個資料緊緊相挨著。 * | |
ptr 資料的記憶體首地址 |
知識擴充套件:
- 獲取最大 attribute 下標的方法
GLint maxVertexAttribs; // n will be >= 8
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxVertexAttribs);
複製程式碼
- 關於 size 補充
注, 圖片截自,《OpenGL ES 2 Programming Guide》第6章
- 使能頂點陣列資料?
其實頂點著色器中處理的資料有兩種輸入型別,CVOs ( Constant Vertex Objects )、VAOs ( Vertex Array Objects ); 而 glEnableVertexAttribArray、glDisableVertexAttribArray 函式就是使用 CVOs 還是 VAOs 的一組開關,看圖 :
注: 圖片截自,《OpenGL ES 2 Programming Guide》第6章
若使用了 CVOs 作為輸入資料的,要使用以下處理函式來替代 glVertexAttribPointer 函式:
- OpenGL ES 只支援 float-pointer 型別的資料,所以才會有 normalized 引數;
- 頂點著色器的資料傳遞圖, 注: 圖片截自,《OpenGL ES 2 Programming Guide》第6章
特別提醒,VBOs 只是一種為了加快資料訪問和渲染排程的一種手段,而不是資料輸入方式的一種;
強烈建議您去看一下 《OpenGL ES 2 Programming Guide》的 6. Vertex Attributes, Vertex Arrays, and Buffer Objects 這一章;
- 繪製圖形
#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_TRIANGLE_FAN 的一種 | |
first 從第幾個頂點下標開始繪製 | |
count 指有多少個頂點下標需要繪製 |
- 渲染圖形
- (void)render {
[self.context presentRenderbuffer:GL_RENDERBUFFER];
}
複製程式碼
函式 | 描述 |
---|---|
presentRenderbuffer: | 把 Renderbuffer 的內容顯示到視窗系統 ( CAEAGLLayer ) 中 |
presentRenderbuffer: | |
---|---|
- (BOOL)presentRenderbuffer:(NSUInteger)target | |
target *只能是 GL_RENDERBUFFER * | |
return BOOL 返回是否繫結成功 |
補充:同時,這個函式也說明了kEAGLDrawablePropertyRetainedBacking 為什麼要設為 YES 的原因:
如果要儲存 Renderbuffer 的內容就要把 CARAGLLayer 的 drawableProperties 屬性的 kEAGLDrawablePropertyRetainedBacking 設定為 YES 。
上面所有程式碼的工程檔案, 在Github 上 DrawTriangle_OneStep
物件導向的重新設計:
訊息處理的主流程就是上面的訊號流程圖的步序。
物件導向,就是把所有的訊息交給物件來處理咯,關注的就是訊息的傳遞和處理。【可以按照你的喜好來設計,反正可擴充套件性和可維護性都比較好就行了,當然也不能把訊息的傳遞變得很複雜咯】
專案檔案結構:
完整程式碼在 Github 上 DrawTriangle_OOP
第四步,練練手
建議按照自己的思路重新寫一個專案
1. 修改背景色
提示:glClear 函式
2.修改三角形的填充色:
提示:CVOs,三個頂點是統一的顏色資料
3. 修改三角形的三個頂點的顏色(填充色):
提示:VAOs / VBOs ,在三個頂點的基礎上新增新的顏色資料
它們三個主要是為了 [ 學 + 習 ] 如何關聯資料,對應的專案是:Github: DrawTriangle_OOP_Challenges_1
如果你發現文章有錯誤的地方,請在評論區指出,不勝感激!!!