2012年4月9日 星期一

OpenGL ES 入門: 四. 光效之三

since: 2012/04/09
update: 2012/04/10

reference:
1. 原文:
iPhone Development: OpenGL ES From the Ground Up, Part 4: Let There Be Light!
iPhone Development: setupView: from Part IV Rewritten

2. 翻譯:
從零開始學習OpenGL ES之四 – 光效
從零開始學習OpenGL ES之四補遺 – setupView重寫

光效之三: 頂點法線

A. 說明
      1. 法線 (Normal) 是一個垂直於指定多邊形表面的向量(或直線).

      2. OpenGL 渲染一個形狀時並不需要知道法線, 但在你使用定向光線時需要用到.
          OpenGL 需要表面法線來確定光線是怎樣與各多邊形交互作用的.

      3. 但是因為我們使用 GL_SMOOTH 渲染, 所以 OpenGL ES 需要知道頂點法線
          (vertex normal)
而不是表面法線. 因為頂點法線要求你計算使用了該頂點的
          所有表面法線平均向量

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

B. 計算三角形的表面法線

     開啓 OpenGLESCommon.h 檔案, 修改如下:
....
#pragma mark -
#pragma mark Triangle3D
#pragma mark -
....
// 計算三角形的表面法線
//
// 向量外積(Vector Product 或 Cross Product):
// 假設現在有 u 向量與 v 向量, 藉由對 u, v 進行 cross product 可以幫我們找出
// 垂直於 u, v 兩個向量的另一個向量.
static inline Vector3D Triangle3DCalculateSurfaceNormal(Triangle3D triangle)
{
    // 取兩頂點值計算其標準化向量
    Vector3D u = Vector3DMakeWithStartAndEndPoints(triangle.v2, triangle.v1);
    Vector3D v = Vector3DMakeWithStartAndEndPoints(triangle.v3, triangle.v1);
   
    Vector3D ret;
    ret.x = (u.y * v.z) - (u.z * v.y);
    ret.y = (u.z * v.x) - (u.x * v.z);
    ret.z = (u.x * v.y) - (u.y * v.x);
   
    return ret;
}

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

C. 計算二十面體的各頂點法線
     1. 說明: 除非沒有替代方法, 否則儘量避免使用頂點法線的即時計算. 在這裡, 會在
                    ViewController 的 setupView: 中呼叫一個方法來計算二十面體的各頂點
                    法線, 它會循環處理每個頂點及三角形索引, 最後將結果儲存到指向
                     C struct 的位址裡(Vertex3D*) .

     2. 開啓 OpenGLESCommon.h 檔案, 修改如下:
....
#pragma mark -
#pragma mark Vector3D
#pragma mark -
....
static inline Vector3D Vector3DAdd(Vector3D vector1, Vector3D vector2)
{
    Vector3D ret;
    ret.x = vector1.x + vector2.x;
    ret.y = vector1.y + vector2.y;
    ret.z = vector1.z + vector2.z;
   
    return ret;
}
....
#pragma mark -
#pragma mark Used for icosahedron's vertices normal
#pragma mark -

//@add: Used for icosahedron's vertices normal
// 用來計算二十面體的頂點法線
//
static const Vertex3D vertices[]= {
    {0, -0.525731, 0.850651},             // vertices[0]
    {0.850651, 0, 0.525731},              // vertices[1]
    {0.850651, 0, -0.525731},             // vertices[2]
    {-0.850651, 0, -0.525731},            // vertices[3]
    {-0.850651, 0, 0.525731},             // vertices[4]
    {-0.525731, 0.850651, 0},             // vertices[5]
    {0.525731, 0.850651, 0},              // vertices[6]
    {0.525731, -0.850651, 0},             // vertices[7]
    {-0.525731, -0.850651, 0},            // vertices[8]
    {0, -0.525731, -0.850651},            // vertices[9]
    {0, 0.525731, -0.850651},             // vertices[10]
    {0, 0.525731, 0.850651}               // vertices[11]
};

static const GLubyte icosahedronFaces[] = {
    1, 2, 6,
    1, 7, 2,
    3, 4, 5,
    4, 3, 8,
    6, 5, 11,
    5, 6, 10,
    9, 10, 2,
    10, 9, 3,
    7, 8, 9,
    8, 7, 0,
    11, 0, 1,
    0, 11, 4,
    6, 2, 10,
    1, 6, 11,
    3, 5, 10,
    5, 4, 11,
    2, 7, 9,
    7, 1, 0,
    3, 9, 8,
    4, 8, 0,
};

//static const Vertex3D *normals = calloc(12, sizeof(Vertex3D));
//static Vertex3D normals[sizeof(Vertex3D)];

static Vertex3D *normals;

// 計算二十面體的頂點法線
static inline void computeVerticesNormal()
{
    NSLog(@"OpenGLESCommon => computeVerticesNormal()");

    NSMutableString *result = [NSMutableString string];
   
    // defined: static const Vertex3D vertices[]
    // defined: static const GLubyte icosahedronFaces[]
    Vector3D *surfaceNormals = calloc(20, sizeof(Vector3D));
   
    // Calculate the surface normal for each triangle
    for (int i = 0; i < 20; i++)
    {
        Vertex3D vertex1 = vertices[icosahedronFaces[(i*3)]];
        Vertex3D vertex2 = vertices[icosahedronFaces[(i*3)+1]];
        Vertex3D vertex3 = vertices[icosahedronFaces[(i*3)+2]];

        Triangle3D triangle = Triangle3DMake(vertex1, vertex2, vertex3);
        Vector3D surfaceNormal = Triangle3DCalculateSurfaceNormal(triangle);
        Vector3DNormalize(&surfaceNormal);
        surfaceNormals[i] = surfaceNormal;
    }
   
    //Vertex3D *normals = calloc(12, sizeof(Vertex3D));
    // defined: static Vertex3D *normals;
    normals = calloc(12, sizeof(Vertex3D));

    [result appendString:@"static const Vector3D normals[] = {\n"];
    
    for (int i = 0; i < 12; i++)
    {
        int faceCount = 0;
        for (int j = 0; j < 20; j++)
        {
            BOOL contains = NO;
            for (int k = 0; k < 3; k++)
            {
                if (icosahedronFaces[(j * 3) + k] == i)
                    contains = YES;
            }
            if (contains)
            {
                faceCount++;
                normals[i] = Vector3DAdd(normals[i], surfaceNormals[j]);
            }
        }
       
        normals[i].x /= (GLfloat)faceCount;
        normals[i].y /= (GLfloat)faceCount;
        normals[i].z /= (GLfloat)faceCount;
       
        [result appendFormat:@"\t{%f, %f, %f},\n", normals[i].x, normals[i].y, normals[i].z];
    }
   
    [result appendString:@"};\n"];
   
    NSLog(@"%@", result);
}

     3. 開啓 ViewController.m 檔案, 修改如下:
....
-(void)setupView:(GLView *)view
{
    NSLog(@"step 05. ViewController => setupView:");
   
    //@add: 計算二十面體的頂點法線
    computeVerticesNormal();
....

}
     4. 編譯並執行:
         得到預先計算好的頂點法線, 所以就不需要在 OpenGL 執行時進行計算了.
---------------------------------------------------------------------------------------
2012-04-09 13:51:41.146 OpenGLESBegin[1308:fb03]
OpenGLESCommon => computeVerticesNormal()

2012-04-09 13:51:41.197 OpenGLESBegin[1308:fb03]
static const Vector3D normals[] = {
    {0.000000, -0.417775, 0.675974},
    {0.675973, 0.000000, 0.417775},
    {0.675973, -0.000000, -0.417775},
    {-0.675973, 0.000000, -0.417775},
    {-0.675973, -0.000000, 0.417775},
    {-0.417775, 0.675974, 0.000000},
    {0.417775, 0.675973, -0.000000},
    {0.417775, -0.675974, 0.000000},
    {-0.417775, -0.675974, 0.000000},
    {0.000000, -0.417775, -0.675973},
    {0.000000, 0.417775, -0.675974},
    {0.000000, 0.417775, 0.675973},
};

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


D. 啟動法線陣列
      1. 開啓 ViewController.m 檔案, 修改如下:
....
// 畫多面體光效
- (void)drawLight
{
    NSLog(@"ViewController => drawLight");
    // Draw code here
    //@update
    //
    // 1. 建立了一個靜態變數來跟蹤物體的旋轉
    static GLfloat rot = 0.0;
   
    // This is the same result as using Vertex3D, just faster to type and
    // can be made const this way
    //
    // 2. 定義頂點陣列:
    //
    // 使用了一個與前不同的方法, 但結果是一樣的. 由於我們的幾何體根本不會變化,
    // 所以我們將其定義為 const, 這樣就不需要每一 frame 都分配/清除記憶體:
    /*
    static const Vertex3D vertices[]= {
        {0, -0.525731, 0.850651},             // vertices[0]
        {0.850651, 0, 0.525731},              // vertices[1]
        {0.850651, 0, -0.525731},             // vertices[2]
        {-0.850651, 0, -0.525731},            // vertices[3]
        {-0.850651, 0, 0.525731},             // vertices[4]
        {-0.525731, 0.850651, 0},             // vertices[5]
        {0.525731, 0.850651, 0},              // vertices[6]
        {0.525731, -0.850651, 0},             // vertices[7]
        {-0.525731, -0.850651, 0},            // vertices[8]
        {0, -0.525731, -0.850651},            // vertices[9]
        {0, 0.525731, -0.850651},             // vertices[10]
        {0, 0.525731, 0.850651}               // vertices[11]
    };
    */
    //@update: defined in OpenGLESCommon.h

    // 3. 建立一個顏色陣列:
    //
    // 創建一個 Color3D 物件陣列, 每項對應於前一個陣列的頂點:
    static const Color3D colors[] = {
        {1.0, 0.0, 0.0, 1.0},
        {1.0, 0.5, 0.0, 1.0},
        {1.0, 1.0, 0.0, 1.0},
        {0.5, 1.0, 0.0, 1.0},
        {0.0, 1.0, 0.0, 1.0},
        {0.0, 1.0, 0.5, 1.0},
        {0.0, 1.0, 1.0, 1.0},
        {0.0, 0.5, 1.0, 1.0},
        {0.0, 0.0, 1.0, 1.0},
        {0.5, 0.0, 1.0, 1.0},
        {1.0, 0.0, 1.0, 1.0},
        {1.0, 0.0, 0.5, 1.0}
    };
   
    // 4. 建立二十面體:
    //
    // 上述十二個頂點本身並未描述形狀. OpenGL 需要知道怎樣將它們聯繫在一起,
    // 所以我們建立了一個整數陣列 (GLubyte) 指向構成各三角形的頂點.
    //
    // 二十面體的第一個面的三個數是 1,2,6 代表繪製處於索引1 (0.850651, 0, 0.525731),
    // 2 (0.850651, 0, 0.525731), 和 6 (0.525731, 0.850651, 0) 之間的三角形.
    /*
    static const GLubyte icosahedronFaces[] = {
        1, 2, 6,
        1, 7, 2,
        3, 4, 5,
        4, 3, 8,
        6, 5, 11,
        5, 6, 10,
        9, 10, 2,
        10, 9, 3,
        7, 8, 9,
        8, 7, 0,
        11, 0, 1,
        0, 11, 4,
        6, 2, 10,
        1, 6, 11,
        3, 5, 10,
        5, 4, 11,
        2, 7, 9,
        7, 1, 0,
        3, 9, 8,
        4, 8, 0,
    };
    */
    //@update: defined in OpenGLESCommon.h
   
    // 二十面體的頂點法線
    /*
    static const Vector3D normals[] = {
        {0.000000, -0.417775, 0.675974},
        {0.675973, 0.000000, 0.417775},
        {0.675973, -0.000000, -0.417775},
        {-0.675973, 0.000000, -0.417775},
        {-0.675973, -0.000000, 0.417775},
        {-0.417775, 0.675974, 0.000000},
        {0.417775, 0.675973, -0.000000},
        {0.417775, -0.675974, 0.000000},
        {-0.417775, -0.675974, 0.000000},
        {0.000000, -0.417775, -0.675973},
        {0.000000, 0.417775, -0.675974},
        {0.000000, 0.417775, 0.675973},
    };
    */
    //@update: defined in OpenGLESCommon.h
    //                  & compute in setupView: computeVerticesNormal()

   
    // 5.
    //
    // 加載單元矩陣
    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_COLOR_ARRAY);
   
    //@add: 啟動法線陣列
    glEnableClientState(GL_NORMAL_ARRAY);
   
    // 提供頂點陣列內容給 OpenGL
    glVertexPointer(3, GL_FLOAT, 0, vertices);
   
    // 提供顏色陣列內容給 OpenGL
    glColorPointer(4, GL_FLOAT, 0, colors);
   
    //@add: 提供法線陣列內容給 OpenGL
    glNormalPointer(GL_FLOAT, 0, normals);
   
    // 6. 不使用 glDrawArrays(), 而是使用 glDrawElements():
    //
    // 備註: 如果你按繪製的正確次序提供頂點, 那麼你應該使用 glDrawArrays(),
    //       但是如果你提供一個陣列然後用另一個以索引值區分頂點次序的陣列的話,
    //       那麼你應該使用 glDrawElements().
    glDrawElements(GL_TRIANGLES, 60, GL_UNSIGNED_BYTE, icosahedronFaces);
   
    // 7. 執行禁止功能
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);
    //@add
    glDisableClientState(GL_NORMAL_ARRAY);
   
    // 8. 根據上一個 frame 繪製的時間增加旋轉變量值:
    static NSTimeInterval lastDrawTime;
    if (lastDrawTime)
    {
        NSTimeInterval timeSinceLastDraw = [NSDate timeIntervalSinceReferenceDate] - lastDrawTime;
        rot += 50 * timeSinceLastDraw;
    }
    lastDrawTime = [NSDate timeIntervalSinceReferenceDate];
}
....

      2. 編譯並執行:
         看到一個真實的三維旋轉物體, 顏色仍是灰暗的.

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

E. 建立簡單的材質
      1. 說明: 當你使用光效和平滑陰影時, OpenGL 期待你為多邊形提供材質(material)
                  
紋理(texture). 材質比在顏色陣列中提供簡單顏色要複雜得多. 材質像光
                   一樣由許多元素構成, 可以產生不同的表面效果. 物體的表面效果實際上是由
                   場景中的光和多邊形的材質決定的.

      2. 開啓 ViewController.m 檔案, 修改如下:
....
- (void)drawLight
{
....
    //@add: 啟動法線陣列
    glEnableClientState(GL_NORMAL_ARRAY);
   
    //@add: 啟動顏色材質
    // OpenGL 將使用我們提供的顏色陣列來創建簡單的材質
    glEnable(GL_COLOR_MATERIAL);
....
    // 7. 執行禁止功能
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);
    //@add
    glDisableClientState(GL_NORMAL_ARRAY);
    glDisableClientState(GL_COLOR_MATERIAL);
....
}
....

      3. 編譯並執行:

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

F. 重寫 setupView:
(update: 2012/04/10)
     1. 說明:
         a. 在此將使用 OpenGLESCommon.h 中定義的 Vertex3D, Vector3D Color3D
             資料結構
來重寫 setupView: 方法. 因為使用頂點, 顏色和三角形的術語比可變
             長度浮點數陣列更容易理解.

         b. 另外, 減少了環境光元素的數量並將光源向右移動了一點.

         c. 並且, 使用 Vector3DMakeWithStartAndEndPoints() 將移動的光源指向二十
             面體. 這樣做使得光效更為生動一點
.

     2. 開啓 ViewController.m 檔案, 修改如下:
....
//@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 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);
   
    // 定義第一個光源的散射光
    // 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);
   
    // 定義第一個光源的鏡射光與亮度
    // 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);
   
    // 定義第一個光源的位置
    // 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 50field of vision
    glLightf(GL_LIGHT0, GL_SPOT_CUTOFF, 25.0);
   
    glLoadIdentity();
   
    // 清除緩存用的灰色
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
....

     3. 編譯並執行

沒有留言:

張貼留言

注意:只有此網誌的成員可以留言。