2012年4月15日 星期日

OpenGL ES 入門: 六. 紋理及紋理映射之一

since: 2012/04/11
update: 2012/04/15

reference:
1. 原文:
iPhone Development: OpenGL ES From the Ground Up, Part 6: Textures and Texture Mapping

2. 翻譯:
從零開始學習OpenGL ES之六 – 紋理及紋理映射

紋理及紋理映射: 建立紋理與載入圖像

A. 前言  
      在 OpenGL ES 中另一種為多邊形定義顏色創建材質的方法是將紋理映射到
      多邊形. 這是一種很實用的方法, 它可以產生很漂亮的外觀並節省大量的處理器
      時間. 因為使用簡單的幾何體通過紋理映射的方法比使用材質的複雜幾何體的
      渲染快得多.

--------------------------------------------------------------------------------

B. 紋理映射的前置作業
      1. 開啓 GLView.h 檔案, 修改如下:
....
@protocol GLViewDelegate

@required
- (void)setupView:(GLView *)view;

@optional
//@update for drawing
- (void)drawView:(GLView *)view;
- (void)drawTriangle3D; // 畫三角形
- (void)drawSquare; // 畫正方形
- (void)drawVertexColor; // 畫頂點顏色
- (void)drawIcosahedron; // 畫二十面體
- (void)drawPerspective; // 畫透視多面體
- (void)drawLight; // 畫多面體光效
- (void)drawSpheres; // 畫球體
- (void)TextureMapping; // 紋理映射

@end

      2. 開啓 GLView.m 檔案, 修改如下:
....
- (void)drawView
{
....
    //@update for drawing
    //[self.delegate drawTriangle3D]; // 畫三角形
    //[self.delegate drawSquare]; // 畫正方形
    //[self.delegate drawVertexColor]; // 畫頂點顏色
    //[self.delegate drawIcosahedron]; // 畫二十面體
    //[self.delegate drawPerspective]; // 畫透視多面體
    //[self.delegate drawLight]; // 畫多面體光效
    //[self.delegate drawSpheres]; // 畫球體
    [self.delegate TextureMapping]; // 紋理映射
....
}
....

      3. 開啓 ViewController.m 檔案, 修改如下:
....
// 紋理映射
- (void)TextureMapping
{
    NSLog(@"ViewController => TextureMapping");
   
    static GLfloat rot = 0.0;
   
    glColor4f(0.0, 0.0, 0.0, 0.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
   
    static const Vertex3D vertices[] = {
        {-1.0,  1.0, -0.0},
        { 1.0,  1.0, -0.0},
        {-1.0, -1.0, -0.0},
        { 1.0, -1.0, -0.0}
    };
   
    static const Vector3D normals[] = {
        {0.0, 0.0, 1.0},
        {0.0, 0.0, 1.0},
        {0.0, 0.0, 1.0},
        {0.0, 0.0, 1.0}
    };
   
    glLoadIdentity();
    glTranslatef(0.0, 0.0, -3.0);
    glRotatef(rot, 1.0, 1.0, 1.0);
       
    glVertexPointer(3, GL_FLOAT, 0, vertices);
    glNormalPointer(GL_FLOAT, 0, normals);
       
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
   
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
       
    static NSTimeInterval lastDrawTime;
    if (lastDrawTime)
    {
        NSTimeInterval timeSinceLastDraw = [NSDate timeIntervalSinceReferenceDate] - lastDrawTime;
        rot +=  60 * timeSinceLastDraw;               
    }
    lastDrawTime = [NSDate timeIntervalSinceReferenceDate];
}
....

      4. 編譯並執行:
          三維旋轉之四方形薄片

--------------------------------------------------------------------------------

C. 啟動功能
      1. 說明:
          為了使用紋理, 我們需要打開 OpenGL 的一些開關以啟動我們需要的一些功能:

          a.  glEnable(GL_TEXTURE_2D)
              這個函數啟動所有兩維圖像的功能. 這個呼叫是必不可缺的; 如果你沒有啟動
              此功能, 那麼你就無法將圖像映射到多邊形上. 它可以在需要時啟動和關閉,
              但是通常不需要這樣做. 你可以啟動此功能而在繪圖時並不使用它, 所以通常
              只需在 ViewController 的 setupView: 方法中呼叫一次.

          b. glEnable(GL_BLEND)
             這個函數啟動了混色(blending) 功能. 混色提供了通過指定來源和目標怎樣
             組合而合成圖像的功能. 例如, 它可以允許你將多個紋理映射到多邊形中以產生
             一個有趣的新的紋理. 然而在 OpenGL 中, "混色" 是指合成任何圖像圖像與
             多邊形
表面合成, 所以即使你不需要將多個圖像混合, 你也需要啟動此功能.

          c. glBlendFunc(GL_ONE, GL_SRC_COLOR)
             這個函數指定了使用的混色方法. 混色函數定義了來源圖像怎樣與目標圖像
             表面合成. OpenGL 將計算出(根據我們提供的信息)怎樣將來源紋理的一個像素
             映射到繪製此像素的目標多邊形的一部分.

             (1). 一旦 OpenGL ES 決定怎樣把一個像素從紋理映射到多邊形, 它將使用指定
                   的混色函數來確定最終繪製的各像素的最終值. glBlendFunc() 函數決定
                   我們將怎樣進行混色運算, 它採用了兩個參數: 第一個參數定義了怎樣使用
                   源紋理. 第二個則定義了怎樣使用目標顏色或紋理.

             (2). 在本文中, 我們希望繪製的紋理完全不透明而忽略多邊形中現存的顏色
                    或紋理, 所以我們設置來源為 GL_ONE, 它表示來源圖像(被映射的紋理)
                    中各顏色通道的值將乘以 1.0 或者換句話說, 以完全顏色密度使用. 目標
                    設置為 GL_SRC_COLOR, 它表示要使用來源圖像中被映射到多邊形
                    特定點的顏色. 此混色函數的結果是一個完全不透明的紋理. 這可能是
                    最常用情況.

             (3). 注意: 如果你已經使用過 OpenGL 的混色功能, 你應該知道 OpenGL ES
                                並不支持所有 OpenGL 支持的混色功能. 下面是 OpenGL ES 支持的:
                                GL_ZERO, GL_ONE, GL_SRC_COLOR,
                                GL_ONE_MINUS_SRC_COLOR, GL_DST_COLOR,
                                GL_ONE_MINUS_DST_COLOR, GL_SRC_ALPHA,
                                GL_ONE_MINUS_SRC_ALPHA, GL_DST_ALPHA,
                                GL_ONE_MINUS_DST_ALPHA, 和
                                GL_SRC_ALPHA_SATURATE (它僅用於來源).

      2. 開啓 ViewController.m 檔案, 修改如下:       
....
-(void)setupView:(GLView *)view
{
....
    // 建構一個對應的座標系統
    glViewport(0, 0, rect.size.width, rect.size.height);  
    glMatrixMode(GL_MODELVIEW);
    
    //@add for Texture Mapping (Turn necessary features on)
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_SRC_COLOR);

    //@add
    glShadeModel(GL_SMOOTH);
 
    //@add for 啟動光效
    glEnable(GL_LIGHTING);
    
    //@add for 啟動第一個光源
    glEnable(GL_LIGHT0);
....
}
....
--------------------------------------------------------------------------------

D. 建立紋理
      1. 說明
           a. 一旦你啟動了紋理和混色, 就可以開始建立紋理了. 通常紋理是在開始顯示
               3D 物體給用戶前程序開始執行時或遊戲每關開始加載時建立的. 這不是
               必須的, 但卻是一個好的建議, 因為建立紋理需要佔用一些處理器時間, 如果
               在你開始顯示一些複雜的幾何體時進行此項工作, 會引起明顯的程序停頓.

           b. OpenGL 中的每一個圖像都是一個紋理, 紋理是不能直接顯示給最終用戶的,
                除非它映射到物體上. 但是有一個小小的例外, 就是對允許你將圖像繪製於
                指定點的所謂點精靈(point sprites), 但它有自己的一套規則, 所以那是一個
                單獨的主題. 通常的情況下, 任何你希望顯示給用戶的圖像必須放置在由頂點
                定義的三角形中, 有點像貼在上面的黏貼紙.

--------------------------------------------------------------------------------

      2. 產生紋理名稱
           a. 為建立一個紋理, 首先必須通知 OpenGL ES 產生一個紋理名稱. 這是一個
               令人迷惑的術語, 因為紋理名稱實際上是一個數字: 更具體的說是一個 GLuint.
               儘管 "名稱" 可以指任何字串, 但對於 OpenGL ES 紋理並不是這樣. 它是一個
               代表指定紋理的整數值. 每個紋理由一個獨一無二的名稱表示, 所以傳遞紋理
               名稱給 OpenGL 是我們區別所使用紋理的方式.

           b. 然而在產生紋理名稱之前, 我們要定義一個保存單個或多個紋理名稱的
               GLuint 陣列: GLuint texture[1]; 儘管只有一個紋理, 但使用一個元素的陣列
               而不是一個 GLuint 仍是一個好習慣. 當然, 仍然可以定義單個 GLuint 進行
               強制呼叫. 在程序式程式中, 紋理通常存於一個全域陣列中, 但在 Objective-C
               程式中, 使用實體變數儲存紋理名稱更為常見. 下面是代碼:
               glGenTextures(1, &texture[0]);

           c. 你可以呼叫 glGenTextures() 產生多個紋理; 傳遞給 OpenGL ES 的第一個
               參數指示了要產生幾個紋理. 第二個參數需要是一個具有足夠空間保存
               紋理名稱的陣列. 我們只有一個元素, 所以只要求 OpenGL ES 產生一個紋理
               名稱. 在呼叫之後, texture[0] 將保持紋理的名稱, 我們將在任何與紋理有關的
               地方都使用 texture[0] 來表示這個特定紋理.

           d. 開啓 ViewController.h 檔案, 修改如下:
....
@interface ViewController : UIViewController <GLViewDelegate>
{
    //@add for draw Spheres
    Vertex3D    *sphereTriangleStripVertices;   // 構成球面的三角形區塊之頂點
    Vector3D    *sphereTriangleStripNormals;    // 構成球面的三角形區塊之法線
    GLuint      sphereTriangleStripVertexCount; // 構成球面的三角形區塊之頂點數量
   
    Vertex3D    *sphereTriangleFanVertices;     // 構成球面的三角形扇狀之頂點
    Vector3D    *sphereTriangleFanNormals;      // 構成球面的三角形扇狀之法線
    GLuint      sphereTriangleFanVertexCount;   // 構成球面的三角形扇狀之頂點數量
   
    //@add for Texture Mapping
    GLuint texture[1];
}
....

           e. 開啓 ViewController.m 檔案, 修改如下:
....
-(void)setupView:(GLView *)view
{
....
    //@add for Texture Mapping (Turn necessary features on)
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_SRC_COLOR);
   
    //@add for Texture Mapping
    //
    // Bind the number of textures we need, in this case one.
    glGenTextures(1, &texture[0]);

    //@add
    glShadeModel(GL_SMOOTH);
   
    //@add for 啟動光效
    glEnable(GL_LIGHTING);
   
    //@add for 啟動第一個光源
    glEnable(GL_LIGHT0);
....
}
....

--------------------------------------------------------------------------------

      3. 紋理綁定
           a. 說明:
               產生紋理名稱後, 在為紋理提供圖像資料之前, 我們必須綁定紋理. 綁定使得
               指定紋理處於啟用狀態. 一次只能啟用一個紋理. 啟用的或 "被綁定" 的紋理
               是繪製多邊形時使用的紋理, 也是新紋理資料將載入其上, 所以在提供圖像
               資料前必須綁定紋理. 這意味著每個紋理至少被綁定一次以為 OpenGL ES
               提供此紋理的資料. 運行時, 可能再次綁定紋理(但不會再次提供圖像資料)
               以指示繪圖時要使用此紋理. 紋理綁定很簡單:

               glBindTexture(GL_TEXTURE_2D, texture[0]);

               因為我們使二維圖像建立紋理, 所以第一個參數永遠是 GL_TEXTURE_2D.
               標準 OpenGL 支持其他類型的紋理, 但目前分佈在 iPhone 上的 OpenGL ES
               版本只支持二維紋理, 坦白地說, 甚至在標準 OpenGL 中, 二維紋理的使用
               也遠比其他類型要多得多. 第二個參數是我們需要綁定的紋理名稱. 呼叫此
               函數後, 先前產生了紋理名稱的紋理將成為啟用的紋理.

           b. 開啓 ViewController.m 檔案, 修改如下:
....
-(void)setupView:(GLView *)view
{
....
    //@add for Texture Mapping (Turn necessary features on)
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_SRC_COLOR);
   
    //@add for Texture Mapping
    //
    // Bind the number of textures we need, in this case one.
    glGenTextures(1, &texture[0]);
    glBindTexture(GL_TEXTURE_2D, texture[0]);

    //@add
    glShadeModel(GL_SMOOTH);
   
    //@add for 啟動光效
    glEnable(GL_LIGHTING);
   
    //@add for 啟動第一個光源
    glEnable(GL_LIGHT0);
....
}
....

--------------------------------------------------------------------------------


      4. 配置圖像
           a. 在第一次綁定紋理後, 在 iPhone上, 必須設定兩個參數, 否則紋理將不會
               正常顯示:
               glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
               glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

           b. 必須設置這兩個參數的原因是預設狀態下 OpenGL 設置了使用所謂 mipmap. 
              Mipmap是一個圖像不同尺寸的組合, 它允許 OpenGL 選擇最為接近的尺寸
              版本以避免過多的插值計算並且在物體遠離觀察者時通過使用更小的紋理
              來更好地管理記憶體. 感謝向量單元和繪圖晶片, iPhone 在圖像插值方面做得
              很好, 所以我們不需要考慮 mipmap. 目前要討論的是怎樣讓 OpenGL ES 通過
              線性插值調整圖像到所需的尺寸. 因為 GL_TEXTURE_MIN_FILTER 用於紋理需要
              被收縮到適合多邊形的尺寸的情形, 而 GL_TEXTURE_MAG_FILTER 則用於紋理被
              放大到適合多邊形的尺寸的情況下, 所以必須進行兩次呼叫. 在兩種情況下, 
              我們傳遞 GL_LINEAR 以通知 OpenGL 以簡單的線性插值方法調整圖像.

           c.  開啓 ViewController.m 檔案, 修改如下:
....

-(void)setupView:(GLView *)view
{
....
    //@add for Texture Mapping (Turn necessary features on)
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_SRC_COLOR);
   
    //@add for Texture Mapping
    //
    // Bind the number of textures we need, in this case one.
    glGenTextures(1, &texture[0]);
    glBindTexture(GL_TEXTURE_2D, texture[0]);
    // Configuring the Image
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);


    //@add
    glShadeModel(GL_SMOOTH);
   
    //@add for 啟動光效
    glEnable(GL_LIGHTING);
   
    //@add for 啟動第一個光源
    glEnable(GL_LIGHT0);
....
}
....

--------------------------------------------------------------------------------

E. 載入圖像資料
      1. 使用 UIImage 的方法
          開啓 ViewController.m 檔案, 修改如下:
....
-(void)setupView:(GLView *)view

{
....
    //@add for Texture Mapping (Turn necessary features on)
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_SRC_COLOR);
   
    //@add for Texture Mapping
    //
    // Bind the number of textures we need, in this case one.
    glGenTextures(1, &texture[0]);
    glBindTexture(GL_TEXTURE_2D, texture[0]);
    // Configuring the Image
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);    

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);


//@Texture Mapping: Loading the Image Data
//
//@add: The PVRTC Approach   
#ifdef USE_PVRTC_TEXTURE   
    NSLog(@"USE_PVRTC_TEXTURE");   

//@add: the UIImage Approach  
//
// 使用任何 UIImage 支援的圖像資料然後轉換成 OpenGL ES 接受的資料格式
#else
    NSLog(@"USE_UIIMAGE_TEXTURE");
   
    NSString *path = [[NSBundle mainBundle] pathForResource:@"texture" ofType:@"png"];
    NSData *texData = [[NSData alloc] initWithContentsOfFile:path];
    UIImage *image = [[UIImage alloc] initWithData:texData];
   
    if (image == nil)
        NSLog(@"Do real error checking here");
   
    GLuint width = CGImageGetWidth(image.CGImage);
    GLuint height = CGImageGetHeight(image.CGImage);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    void *imageData = malloc( height * width * 4 );

    CGContextRef context = CGBitmapContextCreate( imageData, width, height, 8, 4 * width, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big );
       
    CGColorSpaceRelease( colorSpace );
    CGContextClearRect( context, CGRectMake( 0, 0, width, height ) );
    CGContextDrawImage( context, CGRectMake( 0, 0, width, height ), image.CGImage );
   
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
   
    CGContextRelease(context);
   
    free(imageData);
   
#endif

    //@add
    glShadeModel(GL_SMOOTH);
   
    //@add for 啟動光效
    glEnable(GL_LIGHTING);
   
    //@add for 啟動第一個光源
    glEnable(GL_LIGHT0);
....
}

--------------------------------------------------------------------------------

      2. 使用 PVRTC 的方法
          a. 說明:
              iPhone 的繪圖晶片(PowerVR MBX)對一種稱為 PVRTC 的壓縮技術提供
              硬體支援, Apple 推薦在開發 iPhone 應用程式時使用 PVRTC 紋理. 他們
              甚至提供了一篇很好的 技術筆記 描述了怎樣通過使用隨開發工具安裝
              的命令行程式將標準圖像文件轉換為 PVRTC 紋理的方法.

              你應該知道當使用 PVRTC 時與標準 JPEG 或 PNG 圖像相比有可能有些
               圖像質量會下降. 是否值得在你的程式中做出一些犧牲取決於一些因素,
               但使用 PVRTC 紋理可以節省大量的記憶體空間. 你想要手動指定圖像的
               高和寬, 雖然沒有 Objective-C 類別可以解析 PVRTC 資料獲取其寬和高
               的資訊, 但載入 PVRTC 資料到當前綁定的紋理實際上甚至比載入普通圖像
               文件更為簡單.

          b. 開啓 ViewController.m 檔案, 修改如下:
....
-(void)setupView:(GLView *)view

{
....
    //@add for Texture Mapping (Turn necessary features on)
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_SRC_COLOR);
   
    //@add for Texture Mapping
    //
    // Bind the number of textures we need, in this case one.
    glGenTextures(1, &texture[0]);
    glBindTexture(GL_TEXTURE_2D, texture[0]);
    // Configuring the Image
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);    

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);


//@Texture Mapping: Loading the Image Data
//
//@add: The PVRTC Approach   
//
// 使用預設的 texturetool 設置載入一個 512×512 的 PVRTC 紋理
#ifdef USE_PVRTC_TEXTURE   
    NSLog(@"USE_PVRTC_TEXTURE");   

    NSString *path = [[NSBundle mainBundle] pathForResource:@"texture" ofType:@"pvrtc"];

    NSData *texData = [[NSData alloc] initWithContentsOfFile:path];
   
    // This assumes that source PVRTC image is 4 bits per pixel and RGB not RGBA
    // If you use the default settings in texturetool, e.g.:
    //
    //         texturetool -e PVRTC -o texture.pvrtc texture.png
    //
    // then this code should work fine for you. Notice, the source image has had
    // its y-axis inverted to deal with the t-axis inversion issue.
    glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG, 512, 512, 0, [texData length], [texData bytes]);

//@add: the UIImage Approach  
//
// 使用任何 UIImage 支援的圖像資料然後轉換成 OpenGL ES 接受的資料格式
#else
    NSLog(@"USE_UIIMAGE_TEXTURE");
....
#endif

    //@add
    glShadeModel(GL_SMOOTH);
   
    //@add for 啟動光效
    glEnable(GL_LIGHTING);
   
    //@add for 啟動第一個光源
    glEnable(GL_LIGHT0);
....
}

          c. 備註:
             產生 PVRTC 檔案的方式如下:
             $ cd /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin

             $ sudo ./texturetool -e PVRTC -o /Lanli/texture.pvrtc /Lanli/texture.png
               (說明: 來源檔案: /Lanli/texture.png ; 輸出檔案: /Lanli/texture.pvrtc )

             $ chmod 755 /Lanli/texture.pvrtc

--------------------------------------------------------------------------------

      3. 紋理的限制
          a. 用於紋理的圖像寬和高必須為乘方, 比如 2, 4, 8, 16, 32, 64, 128, 256, 512,
              或 1024. 例如圖像可能為 64×128 或 512×512.

          b. 當使用 PVRTC 壓縮圖像時, 有一個額外的限制: 來源圖像必須是正方形,
              所以你的圖像應該為 2×2, 4×4 8×8, 16×16, 32×32, 64×64, 128×128,
              256×256, 等等. 如果你的紋理本身不是正方形, 那麼你只需為圖像加上黑邊
              使圖像成為正方形, 然後映射紋理使得你需要的部分顯示在多邊形上.

--------------------------------------------------------------------------------

F. 紋理坐標
     1. 說明
        a. 當紋理映射啟動後, 繪圖時, 你必須為 OpenGL ES 提供其他資料, 即頂點陣列
            中各頂點的紋理坐標. 紋理坐標定義了圖像的哪一部分將被映射到多邊形.

        b. 為了使用紋理坐標陣列, 我們必須啟動它:
             glEnableClientState(GL_TEXTURE_COORD_ARRAY);

        c. 接著, 傳遞紋理坐標:
             glTexCoordPointer(2, GL_FLOAT, 0, texCoords);

        d. 開啓 ViewController.m 檔案, 修改如下:
....
- (void)TextureMapping
{
    NSLog(@"ViewController => TextureMapping");
   
    static GLfloat rot = 0.0;
   
    glColor4f(0.0, 0.0, 0.0, 0.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
   
    static const Vertex3D vertices[] = {
        {-1.0,  1.0, -0.0},
        { 1.0,  1.0, -0.0},
        {-1.0, -1.0, -0.0},
        { 1.0, -1.0, -0.0}
    };
   
    static const Vector3D normals[] = {
        {0.0, 0.0, 1.0},
        {0.0, 0.0, 1.0},
        {0.0, 0.0, 1.0},
        {0.0, 0.0, 1.0}
    };
   
    static const GLfloat texCoords[] = {
        0.0, 1.0,
        1.0, 1.0,
        0.0, 0.0,
        1.0, 0.0
    };

    glLoadIdentity();
    glTranslatef(0.0, 0.0, -3.0);
    glRotatef(rot, 1.0, 1.0, 1.0);
   
    glVertexPointer(3, GL_FLOAT, 0, vertices);
    glNormalPointer(GL_FLOAT, 0, normals);
    glTexCoordPointer(2, GL_FLOAT, 0, texCoords);

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
   
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
   
    static NSTimeInterval lastDrawTime;
    if (lastDrawTime)
    {
        NSTimeInterval timeSinceLastDraw = [NSDate timeIntervalSinceReferenceDate] - lastDrawTime;
        rot +=  60 * timeSinceLastDraw;               
    }
    lastDrawTime = [NSDate timeIntervalSinceReferenceDate];
}
....

        e. 編譯並執行:
           原始紋理:

           執行結果: 發現圖像的 y 軸完全顛倒了.

        f. 使用 PVRTC 的方式:
           開啓 ViewController.m 檔案, 修改如下:
....
-(void)setupView:(GLView *)view
{
....
//@add: 使用 PVRTC 的方式:
#ifndef USE_PVRTC_TEXTURE
#define USE_PVRTC_TEXTURE   
#endif
   
//@Texture Mapping: Loading the Image Data
//
//@add: The PVRTC Approach   
//
// 使用預設的 texturetool 設置載入一個 512×512 的 PVRTC 紋理   
#ifdef USE_PVRTC_TEXTURE   
    NSLog(@"USE_PVRTC_TEXTURE");   
....
}
....


--------------------------------------------------------------------------------

     2. T-軸翻轉之謎
        a. 說明
            以 OpenGL 的角度來看, 我們並未做錯任何事情, 但結果卻是完全錯誤. 原因
            在於 iPhone 的特殊性.  iPhone 中用於 Core Graphics 的圖像坐標系統並非
            與 OpenGL ES 一致, 其 y 軸在螢幕從上到下而增加. 當然在 OpenGL ES
            正好相反, 它的 y 軸從下向上增加. 其結果就是我們早先傳遞給 OpenGL ES
            中的圖像資料從 OpenGL ES 的角度看完全顛倒了. 所以, 當我們使用標準的
            OpenGL ST 映射坐標映射圖像時, 我們得到了一個翻轉的圖像.

        b. 普通圖像的修正
            (1). 當使用非 PVRTC 圖像時, 你可以在傳遞資料到 OpenGL ES 之前就翻轉
                   圖像的坐標, 將下面兩行代碼到紋理載入中建立 OpenGL 環境的語法之後:
                   (這將翻轉繪製內容的坐標系統)
                   CGContextTranslateCTM (context, 0, height);
                   CGContextScaleCTM (context, 1.0, -1.0);

            (2). 開啓 ViewController.m 檔案, 修改如下:
....
-(void)setupView:(GLView *)view
{
....
#ifdef USE_PVRTC_TEXTURE
....
//@add: the UIImage Approach 
#else
....
    CGContextRef context = CGBitmapContextCreate( imageData, width, height, 8, 4 * width, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big );
   
    //@update: Flip the Y-axis
    CGContextTranslateCTM (context, 0, height);
    CGContextScaleCTM (context, 1.0, -1.0);
....
}
....

       c. PVRTC 圖像的修正
          由於沒有 UIKit 類別可以載入或處理 PVRTC 圖像, 所以沒有一個簡單的方法
          翻轉壓縮紋理的坐標系統. 當然, 我們還是有些方法處理這個問題. 一種方法是
          使用諸如 AcornPhotoshop 之類的程式中將圖像轉換為壓縮紋理前簡單地
          進行垂直翻轉. 這看似小詭計的方法在很多情況下是最好的解決方法, 因為所有
          的處理都是事前進行的, 所以運行時不需要額外的處理時間而且還允許壓縮和
          未壓縮圖像具有同樣的紋理坐標陣列. 另一種方法是將 t 軸的值減一. 儘管減法
          是很快的, 但其佔用的時間還是會累積, 所以在大部分情況下, 盡量要避免繪圖
          時進行的轉換工作. 不論是翻轉圖像或翻轉紋理坐標, 都要在顯示前進行載入時
          進行.

        d. 編譯並執行:

2012年4月11日 星期三

OpenGL ES 入門: 五. 材質之二

since: 2012/04/11
update: 2012/04/11

reference:
1. 原文:
iPhone Development: Procedural Spheres in OpenGL ES
iPhone Development: OpenGL ES From the Ground Up, Part 5: Living in a Material World

2. 翻譯:
從零開始學習OpenGL ES之五 – 材質

材質: OpenGL 材質

A. 說明
     1. 藉由定義材質反射光來定義 OpenGL ES 中的材質, 如果一個材質定義為反射
         紅光, 那麼在正常的白光下, 它將顯示紅色.

     2. 在 OpenGL 中 (至少在使用光滑著色處理和光效時), 材質是沒有顏色的. OpenGL
         具有分別定義材質是怎樣反射 OpenGL 光效三要素 (環境, 散射和鏡射) 的能力.
         另外, 它還具有指定材質自發光(emissive) 屬性的能力.

----------------------------------------------------------------------------------------

B. OpenGL 材質
     1. 指定材質
         a. 要在 OpenGL 創建一個材質, 我們通常必須多次呼叫 glMaterialf() 或者
              glMaterialfv() 函數以完全定義材質. 所有未定義的元素或屬性預設值0,
              或者以顏色來說為黑色.

         b. glMaterialf()glMaterialfv() 的參數說明:
              例如:

              GLfloat ambientAndDiffuse[] = {0.0, 0.1, 0.9, 1.0};

             
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, ambientAndDiffuse);


              (1). 第一個參數, 是用於指定是否材質影響多邊形的前, 後或兩者之列舉值
                     (
GL_ENUM). 實際上除了為了與 OpenGL 兼容, 第一個參數在 OpenGL
                     ES 中只有一個有效的選項:
GL_FRONT_AND_BACK, 它簡單地表示材質
                     適用於
任何繪製的多邊形. 常規 OpenGL 允許你通過傳遞 GL_FRONT,
                     GL_BACK, 或者 GL_FRONT_AND_BACK 來為正面和背面指定不同的
                     材質. 但是
OpenGL ES 僅支持 GL_FRONT_AND_BACK.

              (2). 第二個參數是指示正在設定材質的哪個元素或屬性GL_ENUM. 它們
                     像傳遞給
glLightfv() 的值一樣, 比如 GL_AMBIENT. (在此例為:
                     GL_AMBIENT_AND_DIFFUSE)

              (3). 最後的參數是 GL_FLOAT 或包括了實際屬性或元素的 GL_FLOAT
                     資料的指標. (在此例為: ambientAndDiffuse)

         c. 材質的最重要元素是環境光散射光, 因為它們決定了材質是怎樣反射大量
            光線
的. 上一篇文章使用的 "繪製球體" 的程式碼定義了正如太陽光或白熾燈
            產生的白色, 它具有平均分佈的各種波長和顏色的光. 如果光不是白色, 球體
            看上去會有不同的外觀. 例如, 反射至紅色材質的藍光將產生紫色陰影. 簡單
            起見, 我們只使用白色光. 當然, 你可以隨意改變光的顏色進行試驗看看光和
            材質是怎樣交互作用的. 大部分時候, 它們在 OpenGL ES 中的表示與現實
            生活中完全一樣.

--------------------------------------------------------------------------------------

     2. 環境光和散射光
          a. 說明:
             當討論 OpenGL 的材質時, 我們需要同時討論環境光散射光, 這是因為這兩個
             元素是一起工作從而決定物體被感知的顏色的. 材質怎樣反射這兩個元素決定
             了物體被感知的顏色. 大約 90% 或更多的情況下, 將材質的環境光和散射光
             參數設定成一樣. 這樣做, 使它們成為決定物體陰影和外觀的因素.

          b. 定義材質為藍色
              (1). 開啓 ViewController.m 檔案, 修改如下:
....
- (void)drawSpheres
{
....
    //@add 材質於
繪製之前
    GLfloat ambientAndDiffuse[] = {0.0, 0.1, 0.9, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, ambientAndDiffuse);
   
    // Draw code here ....
    glVertexPointer(3, GL_FLOAT, 0, sphereTriangleFanVertices);
    glNormalPointer(GL_FLOAT, 0, sphereTriangleFanNormals);
    glDrawArrays(GL_TRIANGLE_FAN, 0, sphereTriangleFanVertexCount);
   
    glVertexPointer(3, GL_FLOAT, 0, sphereTriangleStripVertices);
    glNormalPointer(GL_FLOAT, 0, sphereTriangleStripNormals);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, sphereTriangleStripVertexCount);
   
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
....
}
....

              (2). 編譯並執行:

        說明: 如同 glColor4f(), 設置材質指示隨後所有物體繪製的方式直到另一個材質
                  被指定. 此處由於環境光沒有散射光強,其下方只是稍暗.

          c. 分別設定材質環境光散射光的反射方式
               (1). 開啓 ViewController.m 檔案, 修改如下:
....
- (void)drawSpheres
{
....
    //@add 材質於繪製之前
    /*
    GLfloat ambientAndDiffuse[] = {0.0, 0.1, 0.9, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, ambientAndDiffuse);
    */
    //@update 材質
    //
    // 材質: 從環境光反射藍色
    GLfloat ambient[] = {0.0, 0.1, 0.9, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient);
    // 材質: 從散射光反射紅色
    GLfloat diffuse[] = {0.9, 0.0, 0.1, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse);

   
    // Draw code here ....
    glVertexPointer(3, GL_FLOAT, 0, sphereTriangleFanVertices);
    glNormalPointer(GL_FLOAT, 0, sphereTriangleFanNormals);
    glDrawArrays(GL_TRIANGLE_FAN, 0, sphereTriangleFanVertexCount);
   
    glVertexPointer(3, GL_FLOAT, 0, sphereTriangleStripVertices);
    glNormalPointer(GL_FLOAT, 0, sphereTriangleStripNormals);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, sphereTriangleStripVertexCount);
   
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
....
}
....


               (2). 編譯並執行:
        說明: 它看上去像我們投射了有色光到球體上, 原因是我們反射了與環境光
                  不一樣的定向光線.

          d. 大部分情況下, 如果你希望產生彩色光的效果, 你只需創建彩色光線然後使用
              GL_AMBIENT_AND_DIFFUSE 來指定材質顏色. 但是有時卻希望分別設置
              它們產生特殊效果或在不引起創建額外光線開銷的情況下假造一個分離的
              彩色點光源. 記住: 你每增加一個光源, 也就增加了每秒鐘的運算量
.

----------------------------------------------------------------------------------------

C. 高光和光澤
      1. 說明:
          你可以單獨設置場景中鏡射元素的反射方式, 從而控制鏡射 "熱點" 的亮度.
          一個稱為 GL_SHININESS 的參數與材質的鏡射元素一起定義了鏡射熱點的
          大小. 如果你設定了
材質的 GL_SPECULAR 值, 你還應該定義其反光度.
          反光度越高, 高光反射越小, 所以預設值 0.0 幾乎完全淹沒了散射光, 因此
          看上去很糟糕.

      2. 增加藍色球體的鏡射熱點:
           a. 開啓 ViewController.m 檔案, 修改如下: ....
- (void)drawSpheres
{
....
    //@add 材質於繪製之前
    GLfloat ambientAndDiffuse[] = {0.0, 0.1, 0.9, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, ambientAndDiffuse);
    //@update 材質
    //
    /*
    // 材質: 從環境光反射藍色
    GLfloat ambient[] = {0.0, 0.1, 0.9, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient);
    // 材質: 從散射光反射紅色
    GLfloat diffuse[] = {0.9, 0.0, 0.1, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse);

    */

    //@add 增加鏡射熱點
    // 使用了一個較暗的白色作為球體的鏡射值
    GLfloat specular[] = {0.3, 0.3, 0.3, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular);
    glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 25.0);

    // Draw code here ....
    glVertexPointer(3, GL_FLOAT, 0, sphereTriangleFanVertices);
    glNormalPointer(GL_FLOAT, 0, sphereTriangleFanNormals);
    glDrawArrays(GL_TRIANGLE_FAN, 0, sphereTriangleFanVertexCount);
   
    glVertexPointer(3, GL_FLOAT, 0, sphereTriangleStripVertices);
    glNormalPointer(GL_FLOAT, 0, sphereTriangleStripNormals);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, sphereTriangleStripVertexCount);
   
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
....
}
....


           b. 編譯並執行:

      3. 使球體顯得更具有光澤
          a. 說明:
              現在, 球體上有一個小的區域具有更強的反射光. 我們可以通過增強光或光的
              鏡射元素, 或者通過增加材質的反光度使這個點更亮. 我們還可以通過調整
              反光度改變鏡射的大小. 材質反光度越高, 鏡射越集中. 例如, 如果我們將反光度
              從 25.0 改為 50.0, 將得到一個更小的熱點, 它會使得球體顯得更具有光澤.

          b. 開啓 ViewController.m 檔案, 修改如下:
....
- (void)drawSpheres
{
....
    //@add 材質於繪製之前
    GLfloat ambientAndDiffuse[] = {0.0, 0.1, 0.9, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, ambientAndDiffuse);
    //@update 材質
    //
    /*
    // 材質: 從環境光反射藍色
    GLfloat ambient[] = {0.0, 0.1, 0.9, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient);
    // 材質: 從散射光反射紅色
    GLfloat diffuse[] = {0.9, 0.0, 0.1, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse);

    */

    //@add 增加鏡射熱點
    // 使用了一個較暗的白色作為球體的鏡射值
    GLfloat specular[] = {0.3, 0.3, 0.3, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular);
    glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 50.0);

    // Draw code here ....
....
}
....


          c. 編譯並執行:
        備註: 鏡射會使三角形邊緣突出, 鏡射通常在遊戲中使用的低面片物體上表現不佳.
                  在常規 OpenGL 中, 有一個稱為 著色器(shader) 的機制可以用來為低面片
                  物體產生較為理想的結果, 但目前iPhone上的 OpenGL ES 並不支持此功能
                  (譯者註: iPhone 3GS 支持 OpenGL ES 2.0, 有 shader 功能. 但有一個問題
                  就是 OpenGL ES 1.1 與 OpenGL ES 2.0 並不完全兼容) 在遊戲中如果你想
                  使低面片物體漂亮的唯一方法就是完全摒棄鏡射元素而使用紋理映射, 這將
                  在之後的文章中談到.

----------------------------------------------------------------------------------------

D. 自發光
      1. 說明:
          最後一個材質的重要屬性為自發光元素. 通過設定自發光元素, 使得材質看上去
          會發射我們指定的顏色. 它並不是真正在發光. 例如, 其周邊物體並不會被發射的
          光線影響. 如果你希望一個物體像燈泡一樣發光照亮其他物體, 由於在 OpenGL ES
          中只有光源會發光, 你需要將自發光元素和與物體同一位置處的實際光源結合起來.
          但是自發光元素可以使物體漂亮地發光.

      2. 開啓 ViewController.m 檔案, 修改如下: ....
- (void)drawSpheres
{
....
    //@add 材質於繪製之前
    GLfloat ambientAndDiffuse[] = {0.0, 0.1, 0.9, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, ambientAndDiffuse);
    //@update 材質
    //
    /*
    // 材質: 從環境光反射藍色
    GLfloat ambient[] = {0.0, 0.1, 0.9, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient);
    // 材質: 從散射光反射紅色
    GLfloat diffuse[] = {0.9, 0.0, 0.1, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse);

    */

    //@add 增加鏡射熱點
    // 使用了一個較暗的白色作為球體的鏡射值
    GLfloat specular[] = {0.3, 0.3, 0.3, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular);
    glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 50.0);

    //@add 增加綠色的自發光澤
    GLfloat emission[] = {0.0, 0.4, 0.0, 1.0};
    glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, emission);


    // Draw code here ....
....
}
....


      3. 編譯並執行:
        備註: 自發光元素影響整個材質, 因此 GL_EMISSION 的值將與落入物體指定
                  區域的任何類型的光相疊加. 請注意, 甚至上圖中的鏡射部分也成了一點
                  藍-綠色而不是純白色.
鏡射點處其效果是很微小的 但在只有環境光
                  被反射的底部效果更為明顯. 它實際影響整個物體.

2012年4月10日 星期二

OpenGL ES 入門: 五. 材質之一

since: 2012/04/10
update: 2012/04/13

reference:
1. 原文:
iPhone Development: Procedural Spheres in OpenGL ES
iPhone Development: OpenGL ES From the Ground Up, Part 5: Living in a Material World

2. 翻譯:
從零開始學習OpenGL ES之五 – 材質

材質: 繪製球體

A. 說明
      1. 在真正進入 "材質" 之前, 由於先前使用的二十面體, 無法在光效材質的相互
          影響下, 明顯的顯示鏡射元素(specular component)效果; 而最理想用來展示
          鏡射光
效果的形狀是球體.

      2. 接下來的球體, 將允許你指定在 "slices"(切片, 垂直向) 和  "stacks"(堆疊, 水平向)
          方面的 "解析度". 基本上是在緯度上(latitudinally)經度上(longitudinally)定義
          頂點
的數量.

----------------------------------------------------------------------------------------------

B. 繪製球體的前置作業
      1. 開啓 ConstantsAndMacros.h 檔案, 修改如下:
....
// PI
#define PI 3.14159265358979323846f

      2. 開啓 OpenGLESCommon.h 檔案, 修改如下:
....
//@add: 2D 紋理
#pragma mark -
#pragma mark Texture2D
#pragma mark -

typedef struct {
    GLfloat s;
    GLfloat t;
} Texture2D;

static inline Texture2D Texture2DMake(GLfloat inS, GLfloat inT)
{
    Texture2D ret;
    ret.s = inS;
    ret.t = inT;
   
    return ret;
}

      3. 開啓 GLView.h 檔案, 修改如下:
....
//@add for protocol
@protocol GLViewDelegate

@required
- (void)setupView:(GLView *)view;

@optional
//@update for drawing
- (void)drawView:(GLView *)view;
- (void)drawTriangle3D; // 畫三角形
- (void)drawSquare; // 畫正方形
- (void)drawVertexColor; // 畫頂點顏色
- (void)drawIcosahedron; // 畫二十面體
- (void)drawPerspective; // 畫透視多面體
- (void)drawLight; // 畫多面體光效
-(void)drawSpheres; // 畫球體

@end

      4. 開啓 GLView.m 檔案, 修改如下:
....
- (void)drawView
{
....
    //@update for drawing
    //[self.delegate drawTriangle3D]; // 畫三角形
    //[self.delegate drawSquare]; // 畫正方形
    //[self.delegate drawVertexColor]; // 畫頂點顏色
    //[self.delegate drawIcosahedron]; // 畫二十面體
    //[self.delegate drawPerspective]; // 畫透視多面體
    //[self.delegate drawLight]; // 畫多面體光效
    [self.delegate drawSpheres]; // 畫球體
....
}
....

      5. 開啓 ViewController.h 檔案, 修改如下:
#import <UIKit/UIKit.h>
//@add
#import "GLView.h"

//@interface ViewController : UIViewController
//@update
@interface ViewController : UIViewController <GLViewDelegate>
{
    //@add for draw Spheres
    Vertex3D    *sphereTriangleStripVertices;     // 構成球面的三角形區塊之頂點
    Vector3D    *sphereTriangleStripNormals;    // 構成球面的三角形區塊之法線
    GLuint      sphereTriangleStripVertexCount; // 構成球面的三角形區塊之頂點數量
   
    Vertex3D    *sphereTriangleFanVertices;       // 構成球面的三角形扇狀之頂點
    Vector3D    *sphereTriangleFanNormals;      // 構成球面的三角形扇狀之法線
    GLuint      sphereTriangleFanVertexCount;   // 構成球面的三角形扇狀之頂點數量
}

//@add: getSolidSphere
void getSolidSphere(Vertex3D **triangleStripVertexHandle,  
                    // Will hold vertices to be drawn as a triangle strip.
                    //      Calling code responsible for freeing if not NULL
                    //
                    // 會將持有的頂點繪製成三角形區塊的一部分;
                    // 當不再使用時必須要釋放掉(free)


                    Vector3D **triangleStripNormalHandle,  
                    // Will hold normals for vertices to be drawn as triangle
                    //      strip. Calling code is responsible for freeing if
                    //      not NULL
                    //
                    // 會將持有的頂點法線繪製成三角形區塊的一部分;
                    // 當不再使用時必須要釋放掉(free)

                   
                    GLuint *triangleStripVertexCount,      
                    // On return, will hold the number of vertices contained in
                    //      triangleStripVertices
                    //
                    // 這個回傳的指標, 會持有 "構成三角形區塊頂點" 的頂點數量.
                   
                    Vertex3D **triangleFanVertexHandle,    
                    // Will hold vertices to be drawn as a triangle fan. Calling
                    //      code responsible for freeing if not NULL
                    //
                    // 會將持有要繪製成三角形扇狀的頂點;
                    // 當不再使用時必須要釋放掉(free)

                   
                    Vector3D **triangleFanNormalHandle,    
                    // Will hold normals for vertices to be drawn as triangle
                    //      strip. Calling code is responsible for freeing if
                    //      not NULL
                    //
                    // 會將持有要繪製成三角形區塊的頂點法線;
                    // 當不再使用時必須要釋放掉(free)

                   
                    GLuint *triangleFanVertexCount,        
                    // On return, will hold the number of vertices contained in
                    //      the triangleFanVertices
                    //
                    // 這個回傳的指標, 會持有 "構成三角形扇狀頂點" 的頂點數量.
                   
                    GLfloat radius,                        
                    // The radius of the circle to be drawn
                    //
                    // 要繪製的圓之半徑
                   
                    GLuint slices,                         
                    // The number of slices, determines vertical "resolution"
                    //
                    // 切片的數量, 決定垂直 "解析度"
                   
                    GLuint stacks                         
                    // the number of stacks, determines horizontal "resolution"
                    //
                    // 堆疊的數量, 決定水平 "解析度"
);

@end

      6. 開啓 ViewController.m 檔案, 修改如下:
....
//@add: getSolidSphere
void getSolidSphere(Vertex3D **triangleStripVertexHandle,  
                    // 將會持有的頂點繪製成三角形區塊的一部分;
                    // 當不再使用時必須要釋放掉(free)
                   
                    Vector3D **triangleStripNormalHandle,  
                    // 將會持有的頂點法線繪製成三角形區塊的一部分;
                    // 當不再使用時必須要釋放掉(free)
                   
                    GLuint *triangleStripVertexCount,      
                    // 這個回傳的指標, 會持有 "構成三角形區塊頂點" 的頂點數量.
                   
                    Vertex3D **triangleFanVertexHandle,    
                    // 將會持有要繪製成三角形扇狀的頂點;
                    // 當不再使用時必須要釋放掉(free)
                   
                    Vector3D **triangleFanNormalHandle,    
                    // 將會持有要繪製成三角形區塊的頂點法線;
                    // 當不再使用時必須要釋放掉(free)
                   
                    GLuint *triangleFanVertexCount,        
                    // 這個回傳的指標, 會持有 "構成三角形扇狀頂點" 的頂點數量.
                   
                    GLfloat radius,                        
                    // 要繪製的圓之半徑
                   
                    GLuint slices,                         
                    // 切片的數量, 決定垂直 "解析度"
                   
                    GLuint stacks)
                    // 堆疊的數量, 決定水平 "解析度"
{
    NSLog(@"ViewController => getSolidSphere");
    // Draw code here
   
}
....
//@add for <GLViewDelegate> method
-(void)setupView:(GLView *)view
{
    NSLog(@"step 05. ViewController => setupView:");
   
    //@add: 計算二十面體的頂點法線
    computeVerticesNormal();
   
    const GLfloat zNear = 0.01, zFar = 1000.0, fieldOfView = 45.0; // 設定為 45 度視野
    GLfloat size;
    glEnable(GL_DEPTH_TEST);
    glMatrixMode(GL_PROJECTION);
    size = zNear * tanf(DEGREES_TO_RADIANS(fieldOfView) / 2.0);   
   
    CGRect rect = view.bounds;

    // 設定透視 viewport (基於視野角度計算錐台)
    glFrustumf(-size, size, -size / (rect.size.width / rect.size.height), size /
               (rect.size.width / rect.size.height), zNear, zFar);
   
    // 設定正交 viewport (基於視野角度計算錐台)   
    /*
    glOrthof(-1.0,                                // Left
             1.0,                                          // Right
             -1.0 / (rect.size.width / rect.size.height),   // Bottom
             1.0 / (rect.size.width / rect.size.height),   // Top
             0.01,                                         // Near
             10000.0);                                     // Far   
    */
   
    // 建構一個對應的座標系統
    glViewport(0, 0, rect.size.width, rect.size.height); 
   
    glMatrixMode(GL_MODELVIEW);
   
    //@add
    glShadeModel(GL_SMOOTH);
   
    //@add for 啟動光效
    glEnable(GL_LIGHTING);
   
    //@add for 啟動第一個光源
    glEnable(GL_LIGHT0);
   
    //@add for setup light
    //
    // 定義第一個光源的環境光
    // Define the ambient component of the first light
    /*
    const GLfloat light0Ambient[] = {0.1, 0.1, 0.1, 1.0};
    glLightfv(GL_LIGHT0, GL_AMBIENT, light0Ambient);
    */
    //@update
    /*
    static const Color3D light0Ambient[] = {{0.05, 0.05, 0.05, 1.0}};
    glLightfv(GL_LIGHT0, GL_AMBIENT, (const GLfloat *)light0Ambient);
     */
    //@update
    static const Color3D light0Ambient[] = {{0.2, 0.2, 0.2, 1.0}};
    glLightfv(GL_LIGHT0, GL_AMBIENT, (const GLfloat *)light0Ambient);
   
    // 定義第一個光源的散射光
    // Define the diffuse component of the first light
    /*
    const GLfloat light0Diffuse[] = {0.7, 0.7, 0.7, 1.0};
    glLightfv(GL_LIGHT0, GL_DIFFUSE, light0Diffuse);
    */
    //@update
    /*
    static const Color3D light0Diffuse[] = {{0.4, 0.4, 0.4, 1.0}};
    glLightfv(GL_LIGHT0, GL_DIFFUSE, (const GLfloat *)light0Diffuse);
    */
    //@update
    static const Color3D light0Diffuse[] = {{0.8, 0.8, 0.8, 1.0}};
    glLightfv(GL_LIGHT0, GL_DIFFUSE, (const GLfloat *)light0Diffuse);
   
    // 定義第一個光源的鏡射光與亮度
    // Define the specular component and shininess of the first light
    /*
    const GLfloat light0Specular[] = {0.7, 0.7, 0.7, 1.0};
    const GLfloat light0Shininess = 0.4;
    glLightfv(GL_LIGHT0, GL_SPECULAR, light0Specular);
    glLightfv(GL_LIGHT0, GL_SHININESS, &light0Shininess);
    */
    //@update
    /*
    static const Color3D light0Specular[] = {{0.7, 0.7, 0.7, 1.0}};
    glLightfv(GL_LIGHT0, GL_SPECULAR, (const GLfloat *)light0Specular);
    glLightf(GL_LIGHT0, GL_SHININESS, 0.4);
     */
    //@update
    static const Color3D light0Specular[] = {{0.6, 0.6, 0.6, 1.0}};
    glLightfv(GL_LIGHT0, GL_SPECULAR, (const GLfloat *)light0Specular);
   
    // 定義第一個光源的位置
    // Define the position of the first light
    /*
    const GLfloat light0Position[] = {0.0, 10.0, 10.0, 0.0};
    glLightfv(GL_LIGHT0, GL_POSITION, light0Position);
    */
    //@update
    static const Vertex3D light0Position[] = {{10.0, 10.0, 10.0}};
    glLightfv(GL_LIGHT0, GL_POSITION, (const GLfloat *)light0Position);   
   
    // 定義第一個光源的方向向量: 沿 z 軸而下
    // Define a direction vector for the light, this one points right down the Z axis
    /*
    const GLfloat light0Direction[] = {0.0, 0.0, -1.0};
    glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, light0Direction);
    */
    //@update
    // Calculate light vector so it points at the object
    static const Vertex3D objectPoint[] = {{0.0, 0.0, -3.0}};
    const Vertex3D lightVector = Vector3DMakeWithStartAndEndPoints(light0Position[0], objectPoint[0]);
    glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, (GLfloat *)&lightVector);
   
    // 定義第一個光源的遮光角: 限制角度為 90 度(使用 45 度遮光角)
    // Define a cutoff angle. This defines a 90 度 field of vision, since the cutoff
    // is number of degrees to each side of an imaginary line drawn from the light's
    // position along the vector supplied in GL_SPOT_DIRECTION above
    //glLightf(GL_LIGHT0, GL_SPOT_CUTOFF, 45.0);
    // @update
    // This defines a 50 度 field of vision
    glLightf(GL_LIGHT0, GL_SPOT_CUTOFF, 25.0);
   
    glLoadIdentity();
   
    // 清除緩存用的灰色
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
....
// 畫球體
- (void)drawSpheres
{
    NSLog(@"ViewController => drawSpheres");
    static GLfloat rot = 0.0;
   
    glLoadIdentity();
    glTranslatef(0.0f,0.0f,-3.0f); 
    glRotatef(rot,1.0f,1.0f,1.0f);
    glClearColor(0.7, 0.7, 0.7, 1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
    // Draw code here ....

 
    glDisableClientState(GL_VERTEX_ARRAY);

    glDisableClientState(GL_NORMAL_ARRAY);
   
    static NSTimeInterval lastDrawTime;
    if (lastDrawTime)
    {
        NSTimeInterval timeSinceLastDraw = [NSDate timeIntervalSinceReferenceDate] - lastDrawTime;
        rot+=50 * timeSinceLastDraw;               
    }
    lastDrawTime = [NSDate timeIntervalSinceReferenceDate];
}

....
//@add
- (void)dealloc
{
    if(sphereTriangleStripVertices)
        free(sphereTriangleStripVertices);

    if (sphereTriangleStripNormals)
        free(sphereTriangleStripNormals);
   
    if (sphereTriangleFanVertices)
        free(sphereTriangleFanVertices);

    if (sphereTriangleFanNormals)
        free(sphereTriangleFanNormals);
    //[super dealloc];
}
....

----------------------------------------------------------------------------------------------

C. 開始繪製球體
      1. 開啓 ViewController.m 檔案, 修改如下:
....
//@add: getSolidSphere
void getSolidSphere(Vertex3D **triangleStripVertexHandle,  
                    // 將會持有的頂點繪製成三角形區塊的一部分;
                    // 當不再使用時必須要釋放掉(free)

                   
                    Vector3D **triangleStripNormalHandle,  
                    // 將會持有的頂點法線繪製成三角形區塊的一部分;
                    // 當不再使用時必須要釋放掉(free)

                   
                    GLuint *triangleStripVertexCount,      
                    // 這個回傳的指標, 會持有 "構成三角形區塊頂點" 的頂點數量.
                   
                    Vertex3D **triangleFanVertexHandle,    
                    // 將會持有要繪製成三角形扇狀的頂點;
                    // 當不再使用時必須要釋放掉(free)

                   
                    Vector3D **triangleFanNormalHandle,    
                    // 將會持有要繪製成三角形區塊的頂點法線;
                    // 當不再使用時必須要釋放掉(free)

                   
                    GLuint *triangleFanVertexCount,        
                    // 這個回傳的指標, 會持有 "構成三角形扇狀頂點" 的頂點數量.
                   
                    GLfloat radius,                        
                    // 要繪製的圓之半徑
                   
                    GLuint slices,                         
                    // 切片的數量, 決定垂直 "解析度"
                   
                    GLuint stacks)
                    // 堆疊的數量, 決定水平 "解析度"
{
    NSLog(@"ViewController => getSolidSphere");
    // Draw code here
    GLfloat rho, drho, theta, dtheta;
    GLfloat x, y, z;
    GLfloat nsign=1.0;
    drho = PI / (GLfloat) stacks;
    dtheta = 2.0 * PI / (GLfloat) slices;
    Vertex3D *triangleStripVertices, *triangleFanVertices;
    Vector3D *triangleStripNormals, *triangleFanNormals;
   
    // Calculate the Triangle Fan for the endcaps
    *triangleFanVertexCount = slices+2;

    triangleFanVertices = (Vertex3D *) calloc(*triangleFanVertexCount, sizeof(Vertex3D));

    triangleFanVertices[0].x = 0.0;
    triangleFanVertices[0].y = 0.0;
    triangleFanVertices[0].z = nsign * radius;
    int counter = 1;
    for (int j = 0; j <= slices; j++)
    {
        theta = (j == slices) ? 0.0 : j * dtheta;
        x = -sin(theta) * sin(drho);
        y = cos(theta) * sin(drho);
        z = nsign * cos(drho);
        triangleFanVertices[counter].x = x * radius;
        triangleFanVertices[counter].y = y * radius;
        triangleFanVertices[counter++].z = z * radius;
    }
   
    // Normals for a sphere around the origin are darn easy
    // - just treat the vertex as a vector and normalize it.

    triangleFanNormals = (Vertex3D *) malloc(*triangleFanVertexCount * sizeof(Vertex3D));

    memcpy(triangleFanNormals, triangleFanVertices, *triangleFanVertexCount * sizeof(Vertex3D));

    for (int i = 0; i < *triangleFanVertexCount; i++)
        Vector3DNormalize(&triangleFanNormals[i]);
   
    // Calculate the triangle strip for the sphere body
    *triangleStripVertexCount = (slices + 1) * 2 * stacks;

    triangleStripVertices = (Vertex3D *) calloc(*triangleStripVertexCount, sizeof(Vertex3D));
    //
    counter = 0;

    for (int i = 0; i < stacks; i++) {
        rho = i * drho;
       
        for (int j = 0; j <= slices; j++)
        {
            /*
             0.0, 1.0,
             1.0, 1.0,
             0.0, 0.0,
             1.0, 0.0
             */
            theta = (j == slices) ? 0.0 : j * dtheta;
            x = -sin(theta) * sin(rho);
            y = cos(theta) * sin(rho);
            z = nsign * cos(rho);
            // TODO: Implement texture mapping if texture used
            //                TXTR_COORD(s, t);
            triangleStripVertices[counter].x = x * radius;
            triangleStripVertices[counter].y = y * radius;
            triangleStripVertices[counter++].z = z * radius;
            x = -sin(theta) * sin(rho + drho);
            y = cos(theta) * sin(rho + drho);
            z = nsign * cos(rho + drho);
            //                TXTR_COORD(s, t - dt);
            triangleStripVertices[counter].x = x * radius;
            triangleStripVertices[counter].y = y * radius;
            triangleStripVertices[counter++].z = z * radius;
        }
    }
   
    triangleStripNormals = (Vertex3D *) malloc(*triangleStripVertexCount * sizeof(Vertex3D));

    memcpy(triangleStripNormals, triangleStripVertices, *triangleStripVertexCount * sizeof(Vertex3D));

    for (int i = 0; i < *triangleStripVertexCount; i++)
        Vector3DNormalize(&triangleStripNormals[i]);
   
    *triangleStripVertexHandle = triangleStripVertices;
    *triangleStripNormalHandle = triangleStripNormals;
    *triangleFanVertexHandle = triangleFanVertices;
    *triangleFanNormalHandle = triangleFanNormals;
}
....
-(void)setupView:(GLView *)view
{
....
    //@add
    getSolidSphere(&sphereTriangleStripVertices, &sphereTriangleStripNormals, &sphereTriangleStripVertexCount, &sphereTriangleFanVertices, &sphereTriangleFanNormals, &sphereTriangleFanVertexCount, 1.0, 50, 50);
}
....
// 畫球體
- (void)drawSpheres
{
    NSLog(@"ViewController => drawSpheres");
    static GLfloat rot = 0.0;
   
    glLoadIdentity();
    glTranslatef(0.0f,0.0f,-3.0f); 
    glRotatef(rot,1.0f,1.0f,1.0f);
    glClearColor(0.7, 0.7, 0.7, 1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
   
    // Draw code here ....
    //@update
    glVertexPointer(3, GL_FLOAT, 0, sphereTriangleFanVertices);
    glNormalPointer(GL_FLOAT, 0, sphereTriangleFanNormals);
    glDrawArrays(GL_TRIANGLE_FAN, 0, sphereTriangleFanVertexCount);
   
    glVertexPointer(3, GL_FLOAT, 0, sphereTriangleStripVertices);
    glNormalPointer(GL_FLOAT, 0, sphereTriangleStripNormals);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, sphereTriangleStripVertexCount);
   
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
   
    static NSTimeInterval lastDrawTime;
    if (lastDrawTime)
    {
        NSTimeInterval timeSinceLastDraw = [NSDate timeIntervalSinceReferenceDate] - lastDrawTime;
        rot += 50 * timeSinceLastDraw;               
    }
    lastDrawTime = [NSDate timeIntervalSinceReferenceDate];
}
....

      2. 編譯並執行: